import {
lazy,
Suspense,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useAuthUi } from './components/auth/AuthUiContext';
import { PlatformEntryFlowShell } from './components/platform-entry/PlatformEntryFlowShell';
import { getInitialPlatformDesktopLayout } from './components/platform-entry/platformEntryResponsive';
import type {
CustomWorldRuntimeLaunchOptions,
SelectionStage,
} from './components/platform-entry/platformEntryTypes';
import { useHostNavigationCanGoBack } from './hooks/useHostNavigationCanGoBack';
import type { HydratedSavedGameSnapshot } from './persistence/runtimeSnapshotTypes';
import {
APP_RUNTIME_ROUTES,
isAppHistoryState,
normalizeAppPath,
pushAppHistoryPath,
readPublicWorkCodeFromLocationSearch,
replaceAppHistoryPath,
resolveInitialSelectionStageFromPath,
resolvePathForSelectionStage,
shouldRedirectEditorCanvasWithoutProject,
} from './routing/appPageRoutes';
import type { RpgRuntimeAppIntent } from './RpgRuntimeApp';
import {
resolveAppTitleForSelectionStage,
syncAppTitle,
} from './services/appTitle';
import {
refreshNativeAppHostRuntime,
subscribeHostRuntimeChange,
} from './services/host-bridge/hostBridge';
import type { CustomWorldProfile } from './types';
const RpgRuntimeApp = lazy(async () => {
const module = await import('./RpgRuntimeApp');
return {
default: module.RpgRuntimeApp,
};
});
function RuntimeLoadingFallback() {
return (
);
}
function isRpgRuntimeRoute(pathname: string) {
const normalizedPath = normalizeAppPath(pathname);
return (
normalizedPath === APP_RUNTIME_ROUTES['rpg-character-select'] ||
normalizedPath === APP_RUNTIME_ROUTES['rpg-adventure']
);
}
function resolveInitialAppSelectionStage() {
if (
shouldRedirectEditorCanvasWithoutProject(
window.location.pathname,
window.location.search,
)
) {
replaceAppHistoryPath('/creation');
return 'creation-home';
}
return resolveInitialSelectionStageFromPath(
window.location.pathname,
getInitialPlatformDesktopLayout(),
);
}
export default function App() {
const authUi = useAuthUi();
const runtimeIntentTokenRef = useRef(0);
const hasHostNavigationAnchorRef = useRef(
isAppHistoryState(window.history.state),
);
const hostNavigation = useHostNavigationCanGoBack();
const [runtimeIntent, setRuntimeIntent] =
useState(null);
const [, setHostRuntimeRevision] = useState(0);
const [isRuntimeActive, setIsRuntimeActive] = useState(() =>
isRpgRuntimeRoute(window.location.pathname),
);
const [selectionStage, setRawSelectionStage] = useState(
resolveInitialAppSelectionStage,
);
const [runtimeReturnStage, setRuntimeReturnStage] =
useState('platform');
const [initialPublicWorkCode] = useState(() =>
readPublicWorkCodeFromLocationSearch(window.location.search),
);
const setSelectionStage = useCallback(
(stage: SelectionStage, options?: { path?: string }) => {
setRawSelectionStage(stage);
pushAppHistoryPath(options?.path ?? resolvePathForSelectionStage(stage));
},
[],
);
useEffect(() => {
const unsubscribe = subscribeHostRuntimeChange(() => {
setHostRuntimeRevision((revision) => revision + 1);
});
void refreshNativeAppHostRuntime();
return unsubscribe;
}, []);
useEffect(() => {
const syncStageFromHistory = () => {
hasHostNavigationAnchorRef.current = isAppHistoryState(
window.history.state,
);
if (
shouldRedirectEditorCanvasWithoutProject(
window.location.pathname,
window.location.search,
)
) {
replaceAppHistoryPath('/creation');
setIsRuntimeActive(false);
setRawSelectionStage('creation-home');
return;
}
if (isRpgRuntimeRoute(window.location.pathname)) {
setIsRuntimeActive(true);
return;
}
setIsRuntimeActive(false);
setRawSelectionStage(
resolveInitialSelectionStageFromPath(
window.location.pathname,
getInitialPlatformDesktopLayout(),
),
);
};
window.addEventListener('popstate', syncStageFromHistory);
return () => window.removeEventListener('popstate', syncStageFromHistory);
}, []);
useEffect(() => {
if (
!hostNavigation.isSupported ||
hostNavigation.canGoBack ||
isRuntimeActive ||
selectionStage === 'platform' ||
isAppHistoryState(window.history.state) ||
hasHostNavigationAnchorRef.current
) {
return;
}
const currentPath = normalizeAppPath(window.location.pathname);
const currentSearch = window.location.search;
hasHostNavigationAnchorRef.current = true;
replaceAppHistoryPath('/');
pushAppHistoryPath(`${currentPath}${currentSearch}`);
}, [
hostNavigation.canGoBack,
hostNavigation.isSupported,
isRuntimeActive,
selectionStage,
]);
const createRuntimeIntent = useCallback(
(intent: Omit) => {
runtimeIntentTokenRef.current += 1;
setRuntimeIntent({
...intent,
token: runtimeIntentTokenRef.current,
});
setIsRuntimeActive(true);
},
[],
);
const handleContinueGame = useCallback(
(snapshot?: HydratedSavedGameSnapshot | null) => {
createRuntimeIntent({
kind: 'snapshot',
snapshot: snapshot ?? null,
});
},
[createRuntimeIntent],
);
const handleCustomWorldSelect = useCallback(
(
customWorldProfile: CustomWorldProfile,
options?: CustomWorldRuntimeLaunchOptions,
) => {
// 中文注释:作品测试需要在结束测试后精确返回启动它的结果页;
// 正式进入世界仍保持既有平台首页返回语义。
setRuntimeReturnStage(options?.returnStage ?? 'platform');
createRuntimeIntent({
kind: 'custom-world',
profile: customWorldProfile,
mode: options?.mode ?? 'play',
disablePersistence: options?.disablePersistence,
exitToResult: options?.returnStage === 'custom-world-result',
});
},
[createRuntimeIntent],
);
const platformThemeClass =
authUi?.platformTheme === 'dark'
? 'platform-theme--dark'
: 'platform-theme--light';
const isImageEditorStage = selectionStage === 'image-editor';
const platformShellSurfaceClass = isImageEditorStage
? 'bg-white p-0'
: 'bg-[image:var(--platform-body-fill)] p-2 sm:p-4';
useEffect(() => {
syncAppTitle(
isRuntimeActive
? 'RPG 运行中 - 陶泥儿'
: resolveAppTitleForSelectionStage(selectionStage),
);
}, [isRuntimeActive, selectionStage]);
if (isRuntimeActive) {
return (
}>
{
setIsRuntimeActive(false);
setSelectionStage(runtimeReturnStage);
}}
/>
);
}
return (
{}}
handleCustomWorldSelect={handleCustomWorldSelect}
/>
);
}