418 lines
13 KiB
TypeScript
418 lines
13 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
||
|
||
import type {
|
||
CustomWorldWorkSummary,
|
||
} from '../../../packages/shared/src/contracts/customWorldAgent';
|
||
import type {
|
||
CustomWorldGalleryCard,
|
||
CustomWorldLibraryEntry,
|
||
PlatformBrowseHistoryEntry,
|
||
PlatformBrowseHistoryWriteEntry,
|
||
ProfileDashboardSummary,
|
||
ProfileSaveArchiveSummary,
|
||
} from '../../../packages/shared/src/contracts/runtime';
|
||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||
import type { AuthUser } from '../../services/authService';
|
||
import { listRpgCreationWorks } from '../../services/rpg-creation/index';
|
||
import {
|
||
listRpgEntryWorldGallery,
|
||
listRpgEntryWorldLibrary,
|
||
listRpgProfileBrowseHistory,
|
||
listRpgProfileSaveArchives,
|
||
resumeRpgProfileSaveArchive,
|
||
upsertRpgProfileBrowseHistory,
|
||
} from '../../services/rpg-entry';
|
||
import {
|
||
RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||
type RuntimeRequestOptions,
|
||
} from '../../services/rpg-runtime/rpgRuntimeRequest';
|
||
import type { CustomWorldProfile } from '../../types';
|
||
import type { PlatformHomeTab } from './RpgEntryHomeView';
|
||
import { resolveRpgEntryErrorMessage } from './rpgEntryShared';
|
||
|
||
const PLATFORM_BOOTSTRAP_AUTH_OPTIONS = RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||
|
||
type UseRpgEntryBootstrapParams = {
|
||
user: AuthUser | null | undefined;
|
||
canAccessProtectedData?: boolean | undefined;
|
||
getProfileDashboard: (
|
||
options?: RuntimeRequestOptions,
|
||
) => Promise<ProfileDashboardSummary | null>;
|
||
handleContinueGame: (
|
||
snapshot?: HydratedSavedGameSnapshot | null,
|
||
) => void;
|
||
hasInitialAgentSession: boolean;
|
||
};
|
||
|
||
export function useRpgEntryBootstrap(
|
||
params: UseRpgEntryBootstrapParams,
|
||
) {
|
||
const {
|
||
user,
|
||
canAccessProtectedData = Boolean(user),
|
||
getProfileDashboard,
|
||
handleContinueGame,
|
||
hasInitialAgentSession,
|
||
} = params;
|
||
const isAuthenticated = Boolean(user);
|
||
const canReadProtectedData = Boolean(user) && canAccessProtectedData;
|
||
const platformTabBootstrapUserIdRef = useRef<string | null | undefined>(
|
||
undefined,
|
||
);
|
||
const hasExplicitPlatformTabSelectionRef = useRef(false);
|
||
|
||
const [savedCustomWorldEntries, setSavedCustomWorldEntries] = useState<
|
||
CustomWorldLibraryEntry<CustomWorldProfile>[]
|
||
>([]);
|
||
const [customWorldWorkEntries, setCustomWorldWorkEntries] = useState<
|
||
CustomWorldWorkSummary[]
|
||
>([]);
|
||
const [publishedGalleryEntries, setPublishedGalleryEntries] = useState<
|
||
CustomWorldGalleryCard[]
|
||
>([]);
|
||
const [historyEntries, setHistoryEntries] = useState<
|
||
PlatformBrowseHistoryEntry[]
|
||
>([]);
|
||
const [saveEntries, setSaveEntries] = useState<ProfileSaveArchiveSummary[]>([]);
|
||
const [platformTab, setPlatformTabState] =
|
||
useState<PlatformHomeTab>('category');
|
||
const [platformError, setPlatformError] = useState<string | null>(null);
|
||
const [dashboardError, setDashboardError] = useState<string | null>(null);
|
||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||
const [saveError, setSaveError] = useState<string | null>(null);
|
||
const [isLoadingPlatform, setIsLoadingPlatform] = useState(false);
|
||
const [isLoadingDashboard, setIsLoadingDashboard] = useState(false);
|
||
const [isResumingSaveWorldKey, setIsResumingSaveWorldKey] = useState<
|
||
string | null
|
||
>(null);
|
||
const [profileDashboard, setProfileDashboard] =
|
||
useState<ProfileDashboardSummary | null>(null);
|
||
|
||
const setPlatformTab = useCallback((nextTab: PlatformHomeTab) => {
|
||
// 区分“平台首屏默认落点”和“用户/流程显式切换”。
|
||
// 一旦显式切过 Tab,就不能再被首屏异步请求回刷成首页或存档。
|
||
hasExplicitPlatformTabSelectionRef.current = true;
|
||
setPlatformTabState(nextTab);
|
||
}, []);
|
||
|
||
const refreshProfileDashboard = useCallback(async () => {
|
||
if (!user || !canReadProtectedData) {
|
||
setProfileDashboard(null);
|
||
setDashboardError(null);
|
||
setIsLoadingDashboard(false);
|
||
return;
|
||
}
|
||
|
||
setIsLoadingDashboard(true);
|
||
setDashboardError(null);
|
||
|
||
try {
|
||
setProfileDashboard(
|
||
await getProfileDashboard(PLATFORM_BOOTSTRAP_AUTH_OPTIONS),
|
||
);
|
||
} catch (error) {
|
||
setDashboardError(
|
||
resolveRpgEntryErrorMessage(error, '读取个人数据看板失败。'),
|
||
);
|
||
} finally {
|
||
setIsLoadingDashboard(false);
|
||
}
|
||
}, [canReadProtectedData, getProfileDashboard, user]);
|
||
|
||
const refreshCustomWorldWorks = useCallback(async () => {
|
||
if (!user || !canReadProtectedData) {
|
||
setCustomWorldWorkEntries([]);
|
||
return [];
|
||
}
|
||
|
||
const nextItems = await listRpgCreationWorks(
|
||
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
|
||
);
|
||
setCustomWorldWorkEntries(nextItems);
|
||
return nextItems;
|
||
}, [canReadProtectedData, user]);
|
||
|
||
const refreshPublishedGallery = useCallback(async () => {
|
||
const nextEntries = await listRpgEntryWorldGallery();
|
||
setPublishedGalleryEntries(nextEntries);
|
||
return nextEntries;
|
||
}, []);
|
||
|
||
const refreshSavedCustomWorldLibrary = useCallback(async () => {
|
||
if (!user || !canReadProtectedData) {
|
||
setSavedCustomWorldEntries([]);
|
||
return [];
|
||
}
|
||
|
||
const nextEntries = await listRpgEntryWorldLibrary(
|
||
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
|
||
);
|
||
setSavedCustomWorldEntries(nextEntries);
|
||
return nextEntries;
|
||
}, [canReadProtectedData, user]);
|
||
|
||
const refreshSaveArchives = useCallback(async () => {
|
||
if (!user || !canReadProtectedData) {
|
||
setSaveEntries([]);
|
||
setSaveError(null);
|
||
return [];
|
||
}
|
||
|
||
setSaveError(null);
|
||
|
||
try {
|
||
const nextEntries = await listRpgProfileSaveArchives(
|
||
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
|
||
);
|
||
setSaveEntries(nextEntries);
|
||
return nextEntries;
|
||
} catch (error) {
|
||
setSaveError(resolveRpgEntryErrorMessage(error, '读取存档列表失败。'));
|
||
return [];
|
||
}
|
||
}, [canReadProtectedData, user]);
|
||
|
||
const appendBrowseHistoryEntry = useCallback(
|
||
async (entry: PlatformBrowseHistoryWriteEntry) => {
|
||
setHistoryError(null);
|
||
|
||
try {
|
||
const syncedEntries = await upsertRpgProfileBrowseHistory(
|
||
entry,
|
||
PLATFORM_BOOTSTRAP_AUTH_OPTIONS,
|
||
);
|
||
setHistoryEntries(syncedEntries);
|
||
} catch (error) {
|
||
setHistoryError(
|
||
resolveRpgEntryErrorMessage(error, '写入浏览历史失败。'),
|
||
);
|
||
}
|
||
},
|
||
[],
|
||
);
|
||
|
||
const handleResumeSaveEntry = useCallback(
|
||
async (entry: ProfileSaveArchiveSummary) => {
|
||
if (!user || !canReadProtectedData || isResumingSaveWorldKey) {
|
||
return;
|
||
}
|
||
|
||
setIsResumingSaveWorldKey(entry.worldKey);
|
||
setSaveError(null);
|
||
|
||
try {
|
||
const resumedArchive = await resumeRpgProfileSaveArchive(entry.worldKey);
|
||
setSaveEntries((currentEntries) =>
|
||
currentEntries.map((currentEntry) =>
|
||
currentEntry.worldKey === resumedArchive.entry.worldKey
|
||
? resumedArchive.entry
|
||
: currentEntry,
|
||
),
|
||
);
|
||
handleContinueGame(resumedArchive.snapshot);
|
||
} catch (error) {
|
||
setSaveError(resolveRpgEntryErrorMessage(error, '恢复存档失败。'));
|
||
} finally {
|
||
setIsResumingSaveWorldKey(null);
|
||
}
|
||
},
|
||
[canReadProtectedData, handleContinueGame, isResumingSaveWorldKey, user],
|
||
);
|
||
|
||
useEffect(() => {
|
||
let isActive = true;
|
||
const nextPlatformBootstrapUserId = user?.id ?? null;
|
||
const shouldApplyInitialPlatformTab =
|
||
platformTabBootstrapUserIdRef.current !== nextPlatformBootstrapUserId;
|
||
|
||
if (shouldApplyInitialPlatformTab) {
|
||
// 在请求发出前先占位,避免首屏请求未完成时用户切了 Tab,
|
||
// 返回结果又被误判成“还没初始化过”并强制跳回默认页。
|
||
platformTabBootstrapUserIdRef.current = nextPlatformBootstrapUserId;
|
||
}
|
||
|
||
void (async () => {
|
||
setHistoryEntries([]);
|
||
setHistoryError(null);
|
||
setSaveError(null);
|
||
setIsLoadingPlatform(true);
|
||
setPlatformError(null);
|
||
setIsLoadingDashboard(canReadProtectedData);
|
||
setDashboardError(null);
|
||
if (!canReadProtectedData) {
|
||
setSavedCustomWorldEntries([]);
|
||
setCustomWorldWorkEntries([]);
|
||
setSaveEntries([]);
|
||
setProfileDashboard(null);
|
||
}
|
||
|
||
try {
|
||
const [
|
||
libraryEntriesResult,
|
||
workEntriesResult,
|
||
galleryEntriesResult,
|
||
dashboardResult,
|
||
historyResult,
|
||
saveArchivesResult,
|
||
] = await Promise.allSettled([
|
||
canReadProtectedData
|
||
? listRpgEntryWorldLibrary(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
|
||
: Promise.resolve([]),
|
||
canReadProtectedData
|
||
? listRpgCreationWorks(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
|
||
: Promise.resolve([]),
|
||
listRpgEntryWorldGallery(),
|
||
canReadProtectedData
|
||
? getProfileDashboard(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
|
||
: Promise.resolve(null),
|
||
canReadProtectedData
|
||
? listRpgProfileBrowseHistory(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
|
||
: Promise.resolve([]),
|
||
canReadProtectedData
|
||
? listRpgProfileSaveArchives(PLATFORM_BOOTSTRAP_AUTH_OPTIONS)
|
||
: Promise.resolve([]),
|
||
]);
|
||
|
||
if (!isActive) {
|
||
return;
|
||
}
|
||
|
||
if (libraryEntriesResult.status === 'fulfilled') {
|
||
setSavedCustomWorldEntries(libraryEntriesResult.value);
|
||
} else {
|
||
setSavedCustomWorldEntries([]);
|
||
}
|
||
|
||
if (workEntriesResult.status === 'fulfilled') {
|
||
setCustomWorldWorkEntries(workEntriesResult.value);
|
||
} else {
|
||
setCustomWorldWorkEntries([]);
|
||
}
|
||
|
||
if (galleryEntriesResult.status === 'fulfilled') {
|
||
setPublishedGalleryEntries(galleryEntriesResult.value);
|
||
} else {
|
||
// 中文注释:公开广场只影响首页展示,失败时降级为空列表;
|
||
// 私有作品库和创作作品列表的受保护失败才需要阻塞提示。
|
||
setPublishedGalleryEntries([]);
|
||
}
|
||
|
||
if (
|
||
(canReadProtectedData &&
|
||
libraryEntriesResult.status === 'rejected') ||
|
||
(canReadProtectedData &&
|
||
workEntriesResult.status === 'rejected')
|
||
) {
|
||
const platformFailure =
|
||
libraryEntriesResult.status === 'rejected'
|
||
? libraryEntriesResult.reason
|
||
: workEntriesResult.status === 'rejected'
|
||
? workEntriesResult.reason
|
||
: null;
|
||
setPlatformError(
|
||
resolveRpgEntryErrorMessage(platformFailure, '读取平台数据失败。'),
|
||
);
|
||
}
|
||
|
||
if (dashboardResult.status === 'fulfilled') {
|
||
setProfileDashboard(dashboardResult.value);
|
||
} else if (canReadProtectedData) {
|
||
setProfileDashboard(null);
|
||
setDashboardError(
|
||
resolveRpgEntryErrorMessage(
|
||
dashboardResult.reason,
|
||
'读取个人数据看板失败。',
|
||
),
|
||
);
|
||
}
|
||
|
||
if (historyResult.status === 'fulfilled') {
|
||
setHistoryEntries(historyResult.value);
|
||
} else if (canReadProtectedData) {
|
||
setHistoryError(
|
||
resolveRpgEntryErrorMessage(historyResult.reason, '读取浏览历史失败。'),
|
||
);
|
||
}
|
||
|
||
if (saveArchivesResult.status === 'fulfilled') {
|
||
setSaveEntries(saveArchivesResult.value);
|
||
} else if (canReadProtectedData) {
|
||
setSaveEntries([]);
|
||
setSaveError(
|
||
resolveRpgEntryErrorMessage(
|
||
saveArchivesResult.reason,
|
||
'读取存档列表失败。',
|
||
),
|
||
);
|
||
}
|
||
|
||
if (
|
||
shouldApplyInitialPlatformTab &&
|
||
!hasInitialAgentSession &&
|
||
!hasExplicitPlatformTabSelectionRef.current
|
||
) {
|
||
// 中文注释:新用户先进入发现页;推荐页只在用户主动点击后作为登录门禁入口。
|
||
setPlatformTabState(isAuthenticated ? 'home' : 'category');
|
||
}
|
||
} finally {
|
||
if (isActive) {
|
||
setIsLoadingPlatform(false);
|
||
setIsLoadingDashboard(false);
|
||
}
|
||
}
|
||
})();
|
||
|
||
return () => {
|
||
isActive = false;
|
||
};
|
||
}, [
|
||
canReadProtectedData,
|
||
getProfileDashboard,
|
||
hasInitialAgentSession,
|
||
isAuthenticated,
|
||
user,
|
||
]);
|
||
|
||
return {
|
||
isAuthenticated,
|
||
canReadProtectedData,
|
||
platformTab,
|
||
setPlatformTab,
|
||
savedCustomWorldEntries,
|
||
setSavedCustomWorldEntries,
|
||
customWorldWorkEntries,
|
||
setCustomWorldWorkEntries,
|
||
publishedGalleryEntries,
|
||
setPublishedGalleryEntries,
|
||
historyEntries,
|
||
setHistoryEntries,
|
||
saveEntries,
|
||
setSaveEntries,
|
||
platformError,
|
||
setPlatformError,
|
||
dashboardError,
|
||
setDashboardError,
|
||
historyError,
|
||
setHistoryError,
|
||
saveError,
|
||
setSaveError,
|
||
isLoadingPlatform,
|
||
isLoadingDashboard,
|
||
isResumingSaveWorldKey,
|
||
profileDashboard,
|
||
setProfileDashboard,
|
||
refreshProfileDashboard,
|
||
refreshCustomWorldWorks,
|
||
refreshPublishedGallery,
|
||
refreshSavedCustomWorldLibrary,
|
||
refreshSaveArchives,
|
||
appendBrowseHistoryEntry,
|
||
handleResumeSaveEntry,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 兼容旧创作链命名,避免并行工作包在本轮迁移后断开导入。
|
||
*/
|
||
export const useRpgCreationPlatformBootstrap = useRpgEntryBootstrap;
|