387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
272 lines
7.9 KiB
TypeScript
272 lines
7.9 KiB
TypeScript
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 (
|
|
<div className="platform-ui-shell platform-viewport-shell platform-theme platform-theme--dark flex h-screen items-center justify-center bg-[image:var(--platform-body-fill)] p-4 font-sans text-[var(--platform-text-strong)]">
|
|
<div className="platform-subpanel rounded-2xl px-5 py-4 text-sm text-zinc-300">
|
|
正在启动
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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<RpgRuntimeAppIntent | null>(null);
|
|
const [, setHostRuntimeRevision] = useState(0);
|
|
const [isRuntimeActive, setIsRuntimeActive] = useState(() =>
|
|
isRpgRuntimeRoute(window.location.pathname),
|
|
);
|
|
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(
|
|
resolveInitialAppSelectionStage,
|
|
);
|
|
const [runtimeReturnStage, setRuntimeReturnStage] =
|
|
useState<SelectionStage>('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<RpgRuntimeAppIntent, 'token'>) => {
|
|
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 (
|
|
<Suspense fallback={<RuntimeLoadingFallback />}>
|
|
<RpgRuntimeApp
|
|
initialIntent={runtimeIntent}
|
|
onExitRuntime={() => {
|
|
setIsRuntimeActive(false);
|
|
setSelectionStage(runtimeReturnStage);
|
|
}}
|
|
/>
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`platform-ui-shell platform-viewport-shell platform-theme ${platformThemeClass} flex flex-col overflow-hidden ${platformShellSurfaceClass} font-sans text-[var(--platform-text-strong)]`}
|
|
>
|
|
<PlatformEntryFlowShell
|
|
selectionStage={selectionStage}
|
|
setSelectionStage={setSelectionStage}
|
|
initialPublicWorkCode={initialPublicWorkCode}
|
|
hasSavedGame={false}
|
|
savedSnapshot={null}
|
|
handleContinueGame={handleContinueGame}
|
|
handleStartNewGame={() => {}}
|
|
handleCustomWorldSelect={handleCustomWorldSelect}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|