Merge branch 'master' of https://git.genarrative.world/GenarrativeAI/Genarrative
This commit is contained in:
@@ -53,6 +53,7 @@ export type CreativeImageInputPanelProps = {
|
||||
aiRedraw: boolean;
|
||||
promptReferenceImages: CreativeImageInputReferenceImage[];
|
||||
promptReferenceLimit?: number;
|
||||
imageLimitHint?: string | null;
|
||||
imageModelPicker?: ReactNode;
|
||||
error?: string | null;
|
||||
inputError?: string | null;
|
||||
@@ -96,6 +97,7 @@ export function CreativeImageInputPanel({
|
||||
aiRedraw,
|
||||
promptReferenceImages,
|
||||
promptReferenceLimit = DEFAULT_PROMPT_REFERENCE_LIMIT,
|
||||
imageLimitHint = null,
|
||||
imageModelPicker = null,
|
||||
error = null,
|
||||
inputError = null,
|
||||
@@ -276,6 +278,11 @@ export function CreativeImageInputPanel({
|
||||
</div>
|
||||
</div>
|
||||
{mainImageMeta ? <div className="mt-3 shrink-0">{mainImageMeta}</div> : null}
|
||||
{imageLimitHint ? (
|
||||
<div className="mt-2 shrink-0 text-center text-[11px] font-semibold text-[var(--platform-text-soft)]">
|
||||
{imageLimitHint}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showPrompt ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contra
|
||||
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
@@ -61,6 +62,9 @@ type CustomWorldCreationHubProps = {
|
||||
squareHoleItems?: SquareHoleWorkSummary[];
|
||||
onOpenSquareHoleDetail?: (item: SquareHoleWorkSummary) => void;
|
||||
onDeleteSquareHole?: ((item: SquareHoleWorkSummary) => void) | null;
|
||||
jumpHopItems?: JumpHopWorkSummaryResponse[];
|
||||
onOpenJumpHopDetail?: (item: JumpHopWorkSummaryResponse) => void;
|
||||
onDeleteJumpHop?: ((item: JumpHopWorkSummaryResponse) => void) | null;
|
||||
puzzleItems?: PuzzleWorkSummary[];
|
||||
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
|
||||
onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null;
|
||||
@@ -169,6 +173,9 @@ export function CustomWorldCreationHub({
|
||||
squareHoleItems = [],
|
||||
onOpenSquareHoleDetail,
|
||||
onDeleteSquareHole = null,
|
||||
jumpHopItems = [],
|
||||
onOpenJumpHopDetail,
|
||||
onDeleteJumpHop = null,
|
||||
puzzleItems = [],
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle = null,
|
||||
@@ -201,6 +208,7 @@ export function CustomWorldCreationHub({
|
||||
bigFishItems,
|
||||
match3dItems,
|
||||
squareHoleItems: isSquareHoleCreationVisible ? squareHoleItems : [],
|
||||
jumpHopItems,
|
||||
puzzleItems,
|
||||
babyObjectMatchItems,
|
||||
barkBattleItems,
|
||||
@@ -210,6 +218,7 @@ export function CustomWorldCreationHub({
|
||||
canDeleteMatch3D: Boolean(onDeleteMatch3D),
|
||||
canDeleteSquareHole:
|
||||
isSquareHoleCreationVisible && Boolean(onDeleteSquareHole),
|
||||
canDeleteJumpHop: Boolean(onDeleteJumpHop),
|
||||
canDeletePuzzle: Boolean(onDeletePuzzle),
|
||||
canDeleteBabyObjectMatch: Boolean(onDeleteBabyObjectMatch),
|
||||
canDeleteBarkBattle: Boolean(onDeleteBarkBattle),
|
||||
@@ -223,6 +232,8 @@ export function CustomWorldCreationHub({
|
||||
onDeleteMatch3D: onDeleteMatch3D ?? undefined,
|
||||
onOpenSquareHoleDetail,
|
||||
onDeleteSquareHole: onDeleteSquareHole ?? undefined,
|
||||
onOpenJumpHopDetail: onOpenJumpHopDetail ?? undefined,
|
||||
onDeleteJumpHop: onDeleteJumpHop ?? undefined,
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle: onDeletePuzzle ?? undefined,
|
||||
onClaimPuzzlePointIncentive: onClaimPuzzlePointIncentive ?? undefined,
|
||||
@@ -249,6 +260,7 @@ export function CustomWorldCreationHub({
|
||||
onDeleteBabyObjectMatch,
|
||||
onDeleteBarkBattle,
|
||||
onDeleteVisualNovel,
|
||||
onDeleteJumpHop,
|
||||
onClaimPuzzlePointIncentive,
|
||||
onOpenBigFishDetail,
|
||||
onOpenDraft,
|
||||
@@ -262,7 +274,9 @@ export function CustomWorldCreationHub({
|
||||
getWorkState,
|
||||
puzzleItems,
|
||||
rpgLibraryEntries,
|
||||
squareHoleItems,
|
||||
onOpenSquareHoleDetail,
|
||||
onOpenJumpHopDetail,
|
||||
jumpHopItems,
|
||||
visualNovelItems,
|
||||
],
|
||||
);
|
||||
@@ -310,6 +324,9 @@ export function CustomWorldCreationHub({
|
||||
case 'square-hole':
|
||||
onOpenSquareHoleDetail?.(item.source.item);
|
||||
return;
|
||||
case 'jump-hop':
|
||||
onOpenJumpHopDetail?.(item.source.item);
|
||||
return;
|
||||
case 'rpg':
|
||||
if (item.status === 'draft') {
|
||||
onOpenDraft(item.source.item);
|
||||
|
||||
@@ -59,6 +59,7 @@ const CREATION_WORK_KIND_FALLBACK_COVER: Record<CreationWorkShelfKind, string> =
|
||||
'big-fish': '/creation-type-references/big-fish.webp',
|
||||
match3d: '/creation-type-references/match3d.webp',
|
||||
'square-hole': '/creation-type-references/square-hole.webp',
|
||||
'jump-hop': '/creation-type-references/jump-hop.webp',
|
||||
puzzle: '/creation-type-references/puzzle.webp',
|
||||
'baby-object-match': '/creation-type-references/creative-agent.webp',
|
||||
'bark-battle': '/creation-type-references/bark-battle.webp',
|
||||
|
||||
@@ -7,12 +7,14 @@ import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/p
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
||||
import {
|
||||
buildBabyObjectMatchPublicWorkCode,
|
||||
buildCustomWorldPublicWorkCode,
|
||||
buildBarkBattlePublicWorkCode,
|
||||
buildBigFishPublicWorkCode,
|
||||
buildJumpHopPublicWorkCode,
|
||||
buildMatch3DPublicWorkCode,
|
||||
buildPuzzlePublicWorkCode,
|
||||
buildSquareHolePublicWorkCode,
|
||||
@@ -31,6 +33,7 @@ export type CreationWorkShelfKind =
|
||||
| 'big-fish'
|
||||
| 'match3d'
|
||||
| 'square-hole'
|
||||
| 'jump-hop'
|
||||
| 'puzzle'
|
||||
| 'baby-object-match'
|
||||
| 'bark-battle'
|
||||
@@ -83,6 +86,10 @@ export type CreationWorkShelfSource =
|
||||
kind: 'square-hole';
|
||||
item: SquareHoleWorkSummary;
|
||||
}
|
||||
| {
|
||||
kind: 'jump-hop';
|
||||
item: JumpHopWorkSummaryResponse;
|
||||
}
|
||||
| {
|
||||
kind: 'puzzle';
|
||||
item: PuzzleWorkSummary;
|
||||
@@ -137,6 +144,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
bigFishItems: BigFishWorkSummary[];
|
||||
match3dItems?: Match3DWorkSummary[];
|
||||
squareHoleItems?: SquareHoleWorkSummary[];
|
||||
jumpHopItems?: JumpHopWorkSummaryResponse[];
|
||||
puzzleItems: PuzzleWorkSummary[];
|
||||
babyObjectMatchItems?: BabyObjectMatchDraft[];
|
||||
barkBattleItems?: BarkBattleWorkSummary[];
|
||||
@@ -145,6 +153,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteBigFish?: boolean;
|
||||
canDeleteMatch3D?: boolean;
|
||||
canDeleteSquareHole?: boolean;
|
||||
canDeleteJumpHop?: boolean;
|
||||
canDeletePuzzle?: boolean;
|
||||
canDeleteBabyObjectMatch?: boolean;
|
||||
canDeleteBarkBattle?: boolean;
|
||||
@@ -158,6 +167,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDeleteMatch3D?: (item: Match3DWorkSummary) => void;
|
||||
onOpenSquareHoleDetail?: (item: SquareHoleWorkSummary) => void;
|
||||
onDeleteSquareHole?: (item: SquareHoleWorkSummary) => void;
|
||||
onOpenJumpHopDetail?: (item: JumpHopWorkSummaryResponse) => void;
|
||||
onDeleteJumpHop?: (item: JumpHopWorkSummaryResponse) => void;
|
||||
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
|
||||
onDeletePuzzle?: (item: PuzzleWorkSummary) => void;
|
||||
onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void;
|
||||
@@ -177,6 +188,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
bigFishItems,
|
||||
match3dItems = [],
|
||||
squareHoleItems = [],
|
||||
jumpHopItems = [],
|
||||
puzzleItems,
|
||||
babyObjectMatchItems = [],
|
||||
barkBattleItems = [],
|
||||
@@ -185,6 +197,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteBigFish = false,
|
||||
canDeleteMatch3D = false,
|
||||
canDeleteSquareHole = false,
|
||||
canDeleteJumpHop = false,
|
||||
canDeletePuzzle = false,
|
||||
canDeleteBabyObjectMatch = false,
|
||||
canDeleteBarkBattle = false,
|
||||
@@ -198,6 +211,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDeleteMatch3D,
|
||||
onOpenSquareHoleDetail,
|
||||
onDeleteSquareHole,
|
||||
onOpenJumpHopDetail,
|
||||
onDeleteJumpHop,
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle,
|
||||
onClaimPuzzlePointIncentive,
|
||||
@@ -236,6 +251,12 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDelete: onDeleteSquareHole,
|
||||
}),
|
||||
),
|
||||
...jumpHopItems.map((item) =>
|
||||
mapJumpHopWorkToShelfItem(item, canDeleteJumpHop, {
|
||||
onOpen: onOpenJumpHopDetail,
|
||||
onDelete: onDeleteJumpHop,
|
||||
}),
|
||||
),
|
||||
...puzzleItems.map((item) =>
|
||||
mapPuzzleWorkToShelfItem(item, canDeletePuzzle, {
|
||||
onOpen: onOpenPuzzleDetail,
|
||||
@@ -749,6 +770,51 @@ function mapSquareHoleWorkToShelfItem(
|
||||
};
|
||||
}
|
||||
|
||||
function mapJumpHopWorkToShelfItem(
|
||||
item: JumpHopWorkSummaryResponse,
|
||||
canDelete: boolean,
|
||||
adapter: WorkShelfAdapter<JumpHopWorkSummaryResponse>,
|
||||
): CreationWorkShelfItem {
|
||||
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
||||
const publicWorkCode =
|
||||
status === 'published' ? buildJumpHopPublicWorkCode(item.profileId) : null;
|
||||
const coverImageSrc = normalizeCoverImageSrc(item.coverImageSrc);
|
||||
return {
|
||||
id: item.workId,
|
||||
kind: 'jump-hop',
|
||||
status,
|
||||
title: item.workTitle,
|
||||
summary: item.workDescription,
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc,
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
publicWorkCode,
|
||||
sharePath:
|
||||
publicWorkCode && status === 'published'
|
||||
? buildPublicWorkStagePath('work-detail', publicWorkCode)
|
||||
: null,
|
||||
openActionLabel: status === 'published' ? '查看详情' : '继续创作',
|
||||
canDelete,
|
||||
canShare: status === 'published' && Boolean(publicWorkCode),
|
||||
badges: [
|
||||
buildStatusBadge(status),
|
||||
{ id: 'type', label: '跳一跳', tone: 'neutral' },
|
||||
],
|
||||
metrics:
|
||||
status === 'published'
|
||||
? buildPublishedMetrics({
|
||||
playCount: item.playCount,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
})
|
||||
: [],
|
||||
actions: buildWorkShelfActions(item, adapter),
|
||||
source: { kind: 'jump-hop', item },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function resolveAuthorDisplayName(
|
||||
...sources: Array<unknown>
|
||||
|
||||
@@ -115,8 +115,10 @@ import { resolveWorkNotFoundRecoveryAction } from '../../routing/runtimeNotFound
|
||||
import {
|
||||
ApiClientError,
|
||||
BACKGROUND_AUTH_REQUEST_OPTIONS,
|
||||
getStoredAccessToken,
|
||||
} from '../../services/apiClient';
|
||||
import {
|
||||
ensureRuntimeGuestToken,
|
||||
getPublicAuthUserByCode,
|
||||
getPublicAuthUserById,
|
||||
} from '../../services/authService';
|
||||
@@ -127,6 +129,7 @@ import {
|
||||
publishBarkBattleWork,
|
||||
updateBarkBattleDraftConfig,
|
||||
} from '../../services/bark-battle-creation';
|
||||
import { startBarkBattleRun } from '../../services/bark-battle-runtime';
|
||||
import {
|
||||
createBigFishCreationSession,
|
||||
executeBigFishCreationAction,
|
||||
@@ -177,9 +180,10 @@ import {
|
||||
type JumpHopRunResponse,
|
||||
type JumpHopSessionResponse,
|
||||
type JumpHopSessionSnapshotResponse,
|
||||
type JumpHopWorkProfileResponse,
|
||||
type JumpHopWorkspaceCreateRequest,
|
||||
JumpHopWorkProfileResponse,
|
||||
JumpHopWorkspaceCreateRequest,
|
||||
} from '../../services/jump-hop/jumpHopClient';
|
||||
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
|
||||
import {
|
||||
@@ -407,6 +411,7 @@ import { PlatformFeedbackView } from './PlatformFeedbackView';
|
||||
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
|
||||
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
|
||||
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
|
||||
import { usePlatformDesktopLayout } from './platformEntryResponsive';
|
||||
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
|
||||
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
|
||||
|
||||
@@ -549,8 +554,34 @@ const AGENT_RESULT_STRUCTURAL_BLOCKER_CODES = new Set([
|
||||
]);
|
||||
const RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS =
|
||||
BACKGROUND_AUTH_REQUEST_OPTIONS;
|
||||
const PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS =
|
||||
const RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS =
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
async function buildRecommendRuntimeGuestOptions() {
|
||||
const { token } = await ensureRuntimeGuestToken();
|
||||
return {
|
||||
...RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
runtimeGuestToken: token,
|
||||
};
|
||||
}
|
||||
function shouldUseRecommendRuntimeGuestAuth(
|
||||
authUi: { user?: { id?: string } | null } | null | undefined,
|
||||
) {
|
||||
return !authUi?.user?.id?.trim() && !getStoredAccessToken();
|
||||
}
|
||||
async function buildRecommendRuntimeAuthOptions(
|
||||
authUi: { user?: { id?: string } | null } | null | undefined,
|
||||
embedded?: boolean,
|
||||
) {
|
||||
if (!embedded) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (shouldUseRecommendRuntimeGuestAuth(authUi)) {
|
||||
return buildRecommendRuntimeGuestOptions();
|
||||
}
|
||||
|
||||
return RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS;
|
||||
}
|
||||
const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
|
||||
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
|
||||
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
|
||||
@@ -1943,7 +1974,7 @@ function hasRecoverableGeneratedPuzzleDraft(
|
||||
);
|
||||
}
|
||||
|
||||
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem) {
|
||||
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem): string[] {
|
||||
switch (item.source.kind) {
|
||||
case 'rpg':
|
||||
return collectDraftNoticeKeys('rpg', [
|
||||
@@ -1972,6 +2003,13 @@ function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem) {
|
||||
item.source.item.profileId,
|
||||
item.source.item.sourceSessionId,
|
||||
]);
|
||||
case 'jump-hop':
|
||||
return collectDraftNoticeKeys('jump-hop', [
|
||||
item.id,
|
||||
item.source.item.workId,
|
||||
item.source.item.profileId,
|
||||
item.source.item.sourceSessionId,
|
||||
]);
|
||||
case 'puzzle':
|
||||
return collectDraftNoticeKeys('puzzle', [
|
||||
item.id,
|
||||
@@ -2057,6 +2095,39 @@ function buildPendingBigFishWorks(
|
||||
}));
|
||||
}
|
||||
|
||||
function buildPendingJumpHopWorks(
|
||||
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||
existingItems: readonly JumpHopWorkSummaryResponse[],
|
||||
): JumpHopWorkSummaryResponse[] {
|
||||
if (!pending) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(pending)
|
||||
.filter(([sessionId]) =>
|
||||
existingItems.every((item) => item.sourceSessionId !== sessionId),
|
||||
)
|
||||
.map(([sessionId, state]) => ({
|
||||
runtimeKind: 'jump-hop',
|
||||
workId: `jump-hop-work-${sessionId}`,
|
||||
profileId: `jump-hop-profile-${sessionId}`,
|
||||
ownerUserId: '',
|
||||
sourceSessionId: sessionId,
|
||||
workTitle: '跳一跳草稿',
|
||||
workDescription: '正在生成跳一跳玩法草稿。',
|
||||
themeTags: [],
|
||||
difficulty: 'standard',
|
||||
stylePreset: 'minimal-blocks',
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
playCount: 0,
|
||||
updatedAt: state.updatedAt,
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: state.status === 'generating' ? 'generating' : 'ready',
|
||||
}));
|
||||
}
|
||||
|
||||
function buildPendingMatch3DWorks(
|
||||
pending: Record<string, PendingDraftShelfState> | undefined,
|
||||
existingItems: readonly Match3DWorkSummary[],
|
||||
@@ -2666,7 +2737,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
authUi?.platformTheme === 'dark'
|
||||
? 'platform-theme--dark'
|
||||
: 'platform-theme--light';
|
||||
const isDesktopLayout = usePlatformDesktopLayout();
|
||||
const [showCreationTypeModal, setShowCreationTypeModal] = useState(false);
|
||||
const [draftGenerationPointNotice, setDraftGenerationPointNotice] = useState<{
|
||||
title: string;
|
||||
message: string;
|
||||
} | null>(null);
|
||||
const [selectedDetailEntry, setSelectedDetailEntry] =
|
||||
useState<CustomWorldLibraryEntry<CustomWorldProfile> | null>(null);
|
||||
const [selectedPublicWorkDetail, setSelectedPublicWorkDetail] =
|
||||
@@ -2727,6 +2803,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [jumpHopGalleryEntries, setJumpHopGalleryEntries] = useState<
|
||||
JumpHopGalleryCardResponse[]
|
||||
>([]);
|
||||
const [jumpHopWorks, setJumpHopWorks] = useState<
|
||||
JumpHopWorkSummaryResponse[]
|
||||
>([]);
|
||||
const [jumpHopRuntimeReturnStage, setJumpHopRuntimeReturnStage] =
|
||||
useState<JumpHopRuntimeReturnStage>('jump-hop-result');
|
||||
const [jumpHopGenerationState, setJumpHopGenerationState] =
|
||||
@@ -2943,6 +3022,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
creationEntryTypes,
|
||||
'big-fish',
|
||||
);
|
||||
const isJumpHopCreationVisible = isPlatformCreationTypeVisible(
|
||||
creationEntryTypes,
|
||||
'jump-hop',
|
||||
);
|
||||
const isSquareHoleCreationVisible = isPlatformCreationTypeVisible(
|
||||
creationEntryTypes,
|
||||
'square-hole',
|
||||
@@ -3244,7 +3327,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
[draftGenerationNotices],
|
||||
);
|
||||
const ensureEnoughDraftGenerationPointsFromServer = useCallback(
|
||||
async (pointsCost: number, setError: (message: string | null) => void) => {
|
||||
async (pointsCost: number) => {
|
||||
try {
|
||||
const latestDashboard = await getPlatformProfileDashboard(
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
@@ -3252,25 +3335,26 @@ export function PlatformEntryFlowShellImpl({
|
||||
platformBootstrap.setProfileDashboard(latestDashboard);
|
||||
const walletBalance = resolveProfileWalletBalance(latestDashboard);
|
||||
if (walletBalance >= pointsCost) {
|
||||
setDraftGenerationPointNotice(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
setError(
|
||||
`泥点不足,本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
|
||||
setDraftGenerationPointNotice(
|
||||
{
|
||||
title: '泥点不足',
|
||||
message: `本次需要 ${pointsCost} 泥点,当前 ${walletBalance} 泥点。`,
|
||||
},
|
||||
);
|
||||
enterCreateTab();
|
||||
selectionStageRef.current = 'platform';
|
||||
setSelectionStage('platform');
|
||||
return false;
|
||||
} catch {
|
||||
setError('读取泥点余额失败,请稍后重试。');
|
||||
enterCreateTab();
|
||||
selectionStageRef.current = 'platform';
|
||||
setSelectionStage('platform');
|
||||
setDraftGenerationPointNotice({
|
||||
title: '读取泥点余额失败',
|
||||
message: '请稍后重试。',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[enterCreateTab, platformBootstrap, setSelectionStage],
|
||||
[platformBootstrap],
|
||||
);
|
||||
|
||||
const resolveBigFishErrorMessage = useCallback(
|
||||
@@ -3293,6 +3377,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
resolveRpgCreationErrorMessage(error, fallback),
|
||||
[],
|
||||
);
|
||||
const resolveBarkBattleErrorMessage = useCallback(
|
||||
(error: unknown, fallback: string) =>
|
||||
resolveRpgCreationErrorMessage(error, fallback),
|
||||
[],
|
||||
);
|
||||
|
||||
const refreshBigFishShelf = useCallback(async () => {
|
||||
setIsBigFishLoadingLibrary(true);
|
||||
@@ -3392,6 +3481,22 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshJumpHopShelf = useCallback(async () => {
|
||||
if (!isJumpHopCreationVisible) {
|
||||
setJumpHopWorks([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const worksResponse = await jumpHopClient.listWorks();
|
||||
setJumpHopWorks(worksResponse.items);
|
||||
return worksResponse.items;
|
||||
} catch {
|
||||
setJumpHopWorks([]);
|
||||
return [];
|
||||
}
|
||||
}, [isJumpHopCreationVisible]);
|
||||
|
||||
const refreshWoodenFishGallery = useCallback(async () => {
|
||||
try {
|
||||
const galleryResponse = await woodenFishClient.listGallery();
|
||||
@@ -3601,6 +3706,22 @@ export function PlatformEntryFlowShellImpl({
|
||||
selectionStage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!platformBootstrap.canReadProtectedData) {
|
||||
setJumpHopWorks([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (platformBootstrap.platformTab === 'create' || selectionStage === 'platform') {
|
||||
void refreshJumpHopShelf();
|
||||
}
|
||||
}, [
|
||||
platformBootstrap.canReadProtectedData,
|
||||
platformBootstrap.platformTab,
|
||||
refreshJumpHopShelf,
|
||||
selectionStage,
|
||||
]);
|
||||
|
||||
const sessionController = useRpgCreationSessionController({
|
||||
userId: authUi?.user?.id,
|
||||
openLoginModal: authUi?.openLoginModal,
|
||||
@@ -3948,6 +4069,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
],
|
||||
[bigFishWorks, pendingDraftShelfItems],
|
||||
);
|
||||
const jumpHopShelfItems = useMemo(
|
||||
() => [
|
||||
...buildPendingJumpHopWorks(
|
||||
pendingDraftShelfItems['jump-hop'],
|
||||
jumpHopWorks,
|
||||
),
|
||||
...jumpHopWorks,
|
||||
],
|
||||
[jumpHopWorks, pendingDraftShelfItems],
|
||||
);
|
||||
const match3dShelfItems = useMemo(
|
||||
() => [
|
||||
...buildPendingMatch3DWorks(pendingDraftShelfItems.match3d, match3dWorks),
|
||||
@@ -4023,6 +4154,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
...bigFishShelfItems.flatMap((item) =>
|
||||
collectDraftNoticeKeys('big-fish', [item.workId, item.sourceSessionId]),
|
||||
),
|
||||
...jumpHopShelfItems.flatMap((item) =>
|
||||
collectDraftNoticeKeys('jump-hop', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]),
|
||||
),
|
||||
...match3dShelfItems.flatMap((item) =>
|
||||
collectDraftNoticeKeys('match3d', [
|
||||
item.workId,
|
||||
@@ -4065,6 +4203,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
babyObjectMatchDrafts,
|
||||
barkBattleShelfItems,
|
||||
bigFishShelfItems,
|
||||
jumpHopShelfItems,
|
||||
creationHubItems,
|
||||
isSquareHoleCreationVisible,
|
||||
match3dShelfItems,
|
||||
@@ -5164,30 +5303,27 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleError(null);
|
||||
return ensureEnoughDraftGenerationPointsFromServer(
|
||||
PUZZLE_DRAFT_GENERATION_POINT_COST,
|
||||
(message) => {
|
||||
setPuzzleCreationError(message);
|
||||
setPuzzleError(message);
|
||||
},
|
||||
);
|
||||
}, [
|
||||
ensureEnoughDraftGenerationPointsFromServer,
|
||||
setPuzzleCreationError,
|
||||
setPuzzleError,
|
||||
]);
|
||||
const preflightMatch3DDraftGeneration = useCallback(async () => {
|
||||
setMatch3DError(null);
|
||||
return ensureEnoughDraftGenerationPointsFromServer(
|
||||
MATCH3D_DRAFT_GENERATION_POINT_COST,
|
||||
setMatch3DError,
|
||||
);
|
||||
}, [ensureEnoughDraftGenerationPointsFromServer, setMatch3DError]);
|
||||
}, [ensureEnoughDraftGenerationPointsFromServer]);
|
||||
const preflightBarkBattleDraftGeneration = useCallback(async () => {
|
||||
setBarkBattleError(null);
|
||||
return ensureEnoughDraftGenerationPointsFromServer(
|
||||
BARK_BATTLE_DRAFT_GENERATION_POINT_COST,
|
||||
setBarkBattleError,
|
||||
);
|
||||
}, [ensureEnoughDraftGenerationPointsFromServer]);
|
||||
const draftGenerationPointNoticeDescription = draftGenerationPointNotice
|
||||
? draftGenerationPointNotice.title === '读取泥点余额失败'
|
||||
? '当前表单不会丢失,关闭后可继续编辑,稍后再试。'
|
||||
: '当前表单不会丢失,关闭后可继续编辑或补足泥点再继续。'
|
||||
: undefined;
|
||||
const recoverCompletedPuzzleDraftGeneration = useCallback(
|
||||
async ({
|
||||
sessionId,
|
||||
@@ -7276,11 +7412,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: targetProfileId,
|
||||
mode: 'play' as const,
|
||||
};
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startVisualNovelRun(
|
||||
targetProfileId,
|
||||
startRunPayload,
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: await startVisualNovelRun(targetProfileId, startRunPayload);
|
||||
setVisualNovelWork(workDetail);
|
||||
@@ -7306,6 +7446,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
authUi,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
setSelectionStage,
|
||||
@@ -7327,9 +7468,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
setVisualNovelError(null);
|
||||
setIsVisualNovelBusy(true);
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'visual-novel'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const nextRun = await streamVisualNovelRuntimeAction(
|
||||
visualNovelRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
);
|
||||
setVisualNovelRun(nextRun);
|
||||
} catch (error) {
|
||||
@@ -7341,6 +7487,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
isVisualNovelBusy,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsVisualNovelBusy,
|
||||
@@ -7551,6 +7699,22 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopSession(response.session);
|
||||
setJumpHopWork(response.work ?? null);
|
||||
setJumpHopGenerationState(readyState);
|
||||
if (response.work) {
|
||||
setJumpHopWorks((current) =>
|
||||
[response.work!.summary, ...current.filter((item) => item.workId !== response.work!.summary.workId)],
|
||||
);
|
||||
markPendingDraftReady('jump-hop', created.session.sessionId, false);
|
||||
markDraftReady(
|
||||
'jump-hop',
|
||||
[
|
||||
created.session.sessionId,
|
||||
response.work.summary.workId,
|
||||
response.work.summary.profileId,
|
||||
],
|
||||
false,
|
||||
);
|
||||
void refreshJumpHopShelf().catch(() => undefined);
|
||||
}
|
||||
setSelectionStage('jump-hop-result');
|
||||
} catch (error) {
|
||||
const errorMessage = resolveRpgCreationErrorMessage(
|
||||
@@ -7665,6 +7829,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
try {
|
||||
const response = await jumpHopClient.publishWork(profileId);
|
||||
setJumpHopWork(response.item);
|
||||
setJumpHopWorks((current) =>
|
||||
[response.item.summary, ...current.filter((item) => item.workId !== response.item.summary.workId)],
|
||||
);
|
||||
void refreshJumpHopShelf().catch(() => undefined);
|
||||
openPublishShareModal({
|
||||
title: response.item.summary.workTitle || '跳一跳',
|
||||
publicWorkCode: buildJumpHopPublicWorkCode(
|
||||
@@ -7729,12 +7897,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
setJumpHopError(null);
|
||||
setJumpHopRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
jumpHopClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||
jumpHopClient.startRun(
|
||||
normalizedProfileId,
|
||||
options.embedded ? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS : {},
|
||||
),
|
||||
jumpHopClient.startRun(normalizedProfileId, runtimeGuestOptions),
|
||||
]);
|
||||
if (detail?.item) {
|
||||
setJumpHopWork(detail.item);
|
||||
@@ -7759,7 +7928,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsJumpHopBusy(false);
|
||||
}
|
||||
},
|
||||
[setSelectionStage],
|
||||
[authUi, setSelectionStage],
|
||||
);
|
||||
|
||||
const restartJumpHopRuntimeRun = useCallback(async () => {
|
||||
@@ -8066,9 +8235,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
setWoodenFishError(null);
|
||||
setWoodenFishRuntimeReturnStage(options.returnStage ?? 'work-detail');
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const [detail, runResponse] = await Promise.all([
|
||||
woodenFishClient.getWorkDetail(normalizedProfileId).catch(() => null),
|
||||
woodenFishClient.startRun(normalizedProfileId),
|
||||
options.embedded
|
||||
? woodenFishClient.startRun(normalizedProfileId, runtimeGuestOptions)
|
||||
: woodenFishClient.startRun(normalizedProfileId),
|
||||
]);
|
||||
if (detail?.item) {
|
||||
setWoodenFishWork(detail.item);
|
||||
@@ -8093,7 +8268,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
},
|
||||
[setSelectionStage],
|
||||
[authUi, setSelectionStage],
|
||||
);
|
||||
|
||||
const checkpointWoodenFishRuntimeRun = useCallback(
|
||||
@@ -8496,16 +8671,23 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: item.profileId,
|
||||
levelId: levelId ?? null,
|
||||
};
|
||||
const authMode = options.embedded
|
||||
? 'isolated'
|
||||
: (options.authMode ?? 'default');
|
||||
const canUseRuntimeGuestAuth =
|
||||
options.embedded || options.authMode === 'isolated';
|
||||
const useRuntimeGuestAuth =
|
||||
canUseRuntimeGuestAuth && shouldUseRecommendRuntimeGuestAuth(authUi);
|
||||
const runtimeGuestOptions = useRuntimeGuestAuth
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const authMode = useRuntimeGuestAuth ? 'isolated' : 'default';
|
||||
const runtimeAuthOptions = useRuntimeGuestAuth
|
||||
? runtimeGuestOptions
|
||||
: canUseRuntimeGuestAuth
|
||||
? RECOMMEND_PUZZLE_BACKGROUND_AUTH_OPTIONS
|
||||
: {};
|
||||
const { run } =
|
||||
authMode === 'isolated'
|
||||
? await startPuzzleRun(
|
||||
startRunPayload,
|
||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
||||
)
|
||||
: await startPuzzleRun(startRunPayload);
|
||||
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||
: await startPuzzleRun(startRunPayload, runtimeAuthOptions);
|
||||
setSelectedPuzzleDetail(item);
|
||||
setPuzzleRun(run);
|
||||
setPuzzleRuntimeAuthMode(authMode);
|
||||
@@ -8548,6 +8730,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isPuzzleBusy,
|
||||
authUi,
|
||||
resolvePuzzleErrorMessage,
|
||||
setIsPuzzleBusy,
|
||||
setPuzzleError,
|
||||
@@ -8600,10 +8783,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
runtimeProfile.generatedBackgroundAsset,
|
||||
{ expireSeconds: 300 },
|
||||
);
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runtimeOptions = {
|
||||
...(options.embedded
|
||||
? RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS
|
||||
: {}),
|
||||
...runtimeGuestOptions,
|
||||
...(typeof options.itemTypeCountOverride === 'number'
|
||||
? { itemTypeCountOverride: options.itemTypeCountOverride }
|
||||
: {}),
|
||||
@@ -8648,6 +8833,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isMatch3DBusy,
|
||||
authUi,
|
||||
match3dFlow,
|
||||
match3dRuntimeAdapter,
|
||||
resolveMatch3DErrorMessage,
|
||||
@@ -8671,11 +8857,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
setSquareHoleError(null);
|
||||
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startSquareHoleRun(
|
||||
profile.profileId,
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
)
|
||||
? await startSquareHoleRun(profile.profileId, runtimeGuestOptions)
|
||||
: await startSquareHoleRun(profile.profileId);
|
||||
setSquareHoleRun(run);
|
||||
setSquareHoleRuntimeReturnStage(returnStage);
|
||||
@@ -8707,6 +8894,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
},
|
||||
[
|
||||
isSquareHoleBusy,
|
||||
authUi,
|
||||
resolveSquareHoleErrorMessage,
|
||||
setSelectionStage,
|
||||
setSquareHoleError,
|
||||
@@ -8827,9 +9015,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
bigFishInputInFlightRef.current = true;
|
||||
try {
|
||||
const runtimeGuestOptions =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? await buildRecommendRuntimeAuthOptions(authUi, true)
|
||||
: {};
|
||||
const { run } = await submitBigFishRuntimeInput(
|
||||
bigFishRun.runId,
|
||||
payload,
|
||||
runtimeGuestOptions,
|
||||
);
|
||||
setBigFishRun(run);
|
||||
} catch (error) {
|
||||
@@ -8840,7 +9033,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
bigFishInputInFlightRef.current = false;
|
||||
}
|
||||
},
|
||||
[bigFishRun, resolveBigFishErrorMessage, setBigFishError],
|
||||
[
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
bigFishRun,
|
||||
resolveBigFishErrorMessage,
|
||||
setBigFishError,
|
||||
],
|
||||
);
|
||||
|
||||
const reportBigFishObservedPlayTime = useCallback(() => {
|
||||
@@ -8853,10 +9052,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBigFishRuntimeStartedAt(null);
|
||||
const reportPromise =
|
||||
activeRecommendRuntimeKind === 'big-fish'
|
||||
? recordBigFishPlay(
|
||||
sessionId,
|
||||
{ elapsedMs },
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
? buildRecommendRuntimeAuthOptions(authUi, true).then(
|
||||
(runtimeAuthOptions) =>
|
||||
recordBigFishPlay(sessionId, { elapsedMs }, runtimeAuthOptions),
|
||||
)
|
||||
: recordBigFishPlay(sessionId, { elapsedMs });
|
||||
void reportPromise.catch((error) => {
|
||||
@@ -8866,6 +9064,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
}, [
|
||||
activeRecommendRuntimeKind,
|
||||
authUi,
|
||||
bigFishRun?.sessionId,
|
||||
bigFishRuntimeStartedAt,
|
||||
resolveBigFishErrorMessage,
|
||||
@@ -9041,12 +9240,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
profileId: currentLevel.profileId,
|
||||
levelId: resolvePuzzleRestartLevelId(currentRun, detailItem),
|
||||
};
|
||||
const runtimeGuestOptions =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const { run } =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await startPuzzleRun(
|
||||
startRunPayload,
|
||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
||||
)
|
||||
? await startPuzzleRun(startRunPayload, runtimeGuestOptions)
|
||||
: await startPuzzleRun(startRunPayload);
|
||||
setSelectedPuzzleDetail(detailItem);
|
||||
puzzleRunRef.current = run;
|
||||
@@ -9169,10 +9369,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const submitLeaderboardPromise =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? submitPuzzleLeaderboard(
|
||||
puzzleRun.runId,
|
||||
payload,
|
||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
||||
? buildRecommendRuntimeGuestOptions().then((runtimeGuestOptions) =>
|
||||
submitPuzzleLeaderboard(puzzleRun.runId, payload, runtimeGuestOptions),
|
||||
)
|
||||
: submitPuzzleLeaderboard(puzzleRun.runId, payload);
|
||||
|
||||
@@ -9229,6 +9427,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
const runtimeGuestOptions =
|
||||
puzzleRuntimeAuthMode === 'isolated'
|
||||
? await buildRecommendRuntimeGuestOptions()
|
||||
: {};
|
||||
const targetProfileId = _target?.profileId?.trim() ?? '';
|
||||
if (puzzleRun.nextLevelMode === 'similarWorks' && targetProfileId) {
|
||||
const itemPromise =
|
||||
@@ -9244,7 +9446,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
{
|
||||
targetProfileId,
|
||||
},
|
||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: advancePuzzleNextLevel(puzzleRun.runId, {
|
||||
targetProfileId,
|
||||
@@ -9269,7 +9471,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
? await advancePuzzleNextLevel(
|
||||
puzzleRun.runId,
|
||||
{},
|
||||
PUBLIC_PUZZLE_RUNTIME_AUTH_OPTIONS,
|
||||
runtimeGuestOptions,
|
||||
)
|
||||
: await advancePuzzleNextLevel(puzzleRun.runId);
|
||||
setPuzzleRun(run);
|
||||
@@ -10354,6 +10556,43 @@ export function PlatformEntryFlowShellImpl({
|
||||
[openPublicWorkDetail, setJumpHopError, setSelectionStage],
|
||||
);
|
||||
|
||||
const openJumpHopDraft = useCallback(
|
||||
async (item: JumpHopWorkSummaryResponse) => {
|
||||
markDraftNoticeSeen(
|
||||
collectDraftNoticeKeys('jump-hop', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]),
|
||||
);
|
||||
|
||||
if (item.publicationStatus === 'published') {
|
||||
void openJumpHopPublicWorkDetail(item.profileId);
|
||||
return;
|
||||
}
|
||||
|
||||
setJumpHopError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setIsJumpHopBusy(true);
|
||||
try {
|
||||
const detail = await jumpHopClient.getWorkDetail(item.profileId);
|
||||
setJumpHopSession(null);
|
||||
setJumpHopRun(null);
|
||||
setJumpHopWork(detail.item);
|
||||
setJumpHopRuntimeReturnStage('jump-hop-result');
|
||||
enterCreateTab();
|
||||
setSelectionStage('jump-hop-result');
|
||||
} catch (error) {
|
||||
setJumpHopError(
|
||||
resolveRpgCreationErrorMessage(error, '读取跳一跳草稿失败。'),
|
||||
);
|
||||
} finally {
|
||||
setIsJumpHopBusy(false);
|
||||
}
|
||||
},
|
||||
[enterCreateTab, markDraftNoticeSeen, openPublicWorkDetail, setSelectionStage],
|
||||
);
|
||||
|
||||
const openWoodenFishPublicWorkDetail = useCallback(
|
||||
async (profileId: string) => {
|
||||
setIsPublicWorkDetailBusy(true);
|
||||
@@ -11120,11 +11359,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBigFishRuntimeReturnStage(returnStage);
|
||||
setBigFishRun(null);
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const { run } = options.embedded
|
||||
? await startBigFishRuntimeRun(
|
||||
sessionId,
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
)
|
||||
? await startBigFishRuntimeRun(sessionId, runtimeGuestOptions)
|
||||
: await startBigFishRuntimeRun(sessionId);
|
||||
setBigFishRuntimeStartedAt(Date.now());
|
||||
setBigFishRun(run);
|
||||
@@ -11135,11 +11375,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
}
|
||||
const recordPlayPromise = options.embedded
|
||||
? recordBigFishPlay(
|
||||
sessionId,
|
||||
{ elapsedMs: 0 },
|
||||
RECOMMEND_RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
)
|
||||
? recordBigFishPlay(sessionId, { elapsedMs: 0 }, runtimeGuestOptions)
|
||||
: recordBigFishPlay(sessionId, { elapsedMs: 0 });
|
||||
void recordPlayPromise.catch((error) => {
|
||||
setBigFishError(
|
||||
@@ -11154,13 +11390,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
|
||||
[authUi, bigFishFlow, resolveBigFishErrorMessage, setBigFishError, setSelectionStage],
|
||||
);
|
||||
|
||||
const startBarkBattleRunFromWork = useCallback(
|
||||
(
|
||||
async (
|
||||
item: BarkBattleWorkSummary,
|
||||
returnStage: BarkBattleRuntimeReturnStage = 'work-detail',
|
||||
options: { embedded?: boolean } = {},
|
||||
) => {
|
||||
if (item.status !== 'published') {
|
||||
setBarkBattleError('汪汪声浪作品发布后才能进入正式玩法。');
|
||||
@@ -11172,17 +11409,34 @@ export function PlatformEntryFlowShellImpl({
|
||||
setBarkBattleRuntimeMode('published');
|
||||
setBarkBattlePublishedConfig(mapBarkBattleWorkToPublishedConfig(item));
|
||||
setBarkBattleRuntimeReturnStage(returnStage);
|
||||
selectionStageRef.current = 'bark-battle-runtime';
|
||||
setSelectionStage('bark-battle-runtime');
|
||||
pushAppHistoryPath(
|
||||
buildPublicWorkStagePath(
|
||||
'bark-battle-runtime',
|
||||
buildBarkBattlePublicWorkCode(item.workId),
|
||||
),
|
||||
);
|
||||
return true;
|
||||
try {
|
||||
const runtimeGuestOptions = await buildRecommendRuntimeAuthOptions(
|
||||
authUi,
|
||||
options.embedded,
|
||||
);
|
||||
const runResponse = options.embedded
|
||||
? await startBarkBattleRun(item.workId, {}, runtimeGuestOptions)
|
||||
: await startBarkBattleRun(item.workId);
|
||||
void runResponse;
|
||||
selectionStageRef.current = 'bark-battle-runtime';
|
||||
if (!options.embedded) {
|
||||
setSelectionStage('bark-battle-runtime');
|
||||
pushAppHistoryPath(
|
||||
buildPublicWorkStagePath(
|
||||
'bark-battle-runtime',
|
||||
buildBarkBattlePublicWorkCode(item.workId),
|
||||
),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
setBarkBattleError(
|
||||
resolveBarkBattleErrorMessage(error, '启动汪汪声浪玩法失败。'),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[setSelectionStage],
|
||||
[authUi, resolveBarkBattleErrorMessage, setSelectionStage],
|
||||
);
|
||||
|
||||
const startSelectedPublicWork = useCallback(() => {
|
||||
@@ -11454,7 +11708,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
'当前汪汪声浪作品信息不完整,暂时无法进入玩法。',
|
||||
);
|
||||
} else {
|
||||
started = startBarkBattleRunFromWork(work, 'platform');
|
||||
started = await startBarkBattleRunFromWork(work, 'platform', {
|
||||
embedded: true,
|
||||
});
|
||||
}
|
||||
} else if (isEdutainmentGalleryEntry(entry)) {
|
||||
started = await startBabyObjectMatchRuntimeFromEntry(
|
||||
@@ -11548,6 +11804,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const recommendRuntimeContent = useMemo(() => {
|
||||
if (
|
||||
isDesktopLayout ||
|
||||
selectionStage !== 'platform' ||
|
||||
platformBootstrap.platformTab !== 'home' ||
|
||||
!activeRecommendRuntimeKind
|
||||
@@ -11954,10 +12211,12 @@ export function PlatformEntryFlowShellImpl({
|
||||
visualNovelSession,
|
||||
visualNovelWork,
|
||||
checkpointWoodenFishRuntimeRun,
|
||||
isDesktopLayout,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDesktopLayout ||
|
||||
selectionStage !== 'platform' ||
|
||||
platformBootstrap.platformTab !== 'home' ||
|
||||
platformBootstrap.isLoadingPlatform
|
||||
@@ -12013,6 +12272,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
match3dRun,
|
||||
platformBootstrap.isLoadingPlatform,
|
||||
platformBootstrap.platformTab,
|
||||
isDesktopLayout,
|
||||
puzzleRun,
|
||||
recommendRuntimeEntries,
|
||||
selectRecommendRuntimeEntry,
|
||||
@@ -13119,6 +13379,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
deletingWorkId={deletingCreationWorkId}
|
||||
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
|
||||
bigFishItems={isBigFishCreationVisible ? bigFishShelfItems : []}
|
||||
jumpHopItems={isJumpHopCreationVisible ? jumpHopShelfItems : []}
|
||||
onOpenBigFishDetail={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
@@ -13128,6 +13389,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onOpenJumpHopDetail={
|
||||
isJumpHopCreationVisible
|
||||
? (item) => {
|
||||
runProtectedAction(() => {
|
||||
void openJumpHopDraft(item);
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onDeleteBigFish={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
@@ -13135,6 +13405,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
: null
|
||||
}
|
||||
onDeleteJumpHop={null}
|
||||
match3dItems={match3dShelfItems}
|
||||
onOpenMatch3DDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
@@ -13241,6 +13512,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
isLoadingPlatform={platformBootstrap.isLoadingPlatform}
|
||||
isLoadingDashboard={platformBootstrap.isLoadingDashboard}
|
||||
hasUnreadDraftUpdate={hasUnreadDraftUpdates}
|
||||
isDesktopLayout={isDesktopLayout}
|
||||
isResumingSaveWorldKey={platformBootstrap.isResumingSaveWorldKey}
|
||||
platformError={
|
||||
platformBootstrap.isLoadingPlatform
|
||||
@@ -15466,6 +15738,29 @@ export function PlatformEntryFlowShellImpl({
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<UnifiedModal
|
||||
open={Boolean(draftGenerationPointNotice)}
|
||||
title={draftGenerationPointNotice?.title ?? '泥点提示'}
|
||||
description={draftGenerationPointNoticeDescription}
|
||||
onClose={() => setDraftGenerationPointNotice(null)}
|
||||
closeOnBackdrop
|
||||
size="sm"
|
||||
overlayClassName={`platform-theme ${platformThemeClass} !items-center`}
|
||||
panelClassName="platform-remap-surface rounded-[1.75rem]"
|
||||
footer={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDraftGenerationPointNotice(null)}
|
||||
className="platform-button platform-button--primary min-h-0 rounded-full px-4 py-2 text-sm"
|
||||
>
|
||||
知道了
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="text-sm leading-6 text-[var(--platform-text-base)]">
|
||||
{draftGenerationPointNotice?.message}
|
||||
</div>
|
||||
</UnifiedModal>
|
||||
<PublishShareModal
|
||||
open={Boolean(publishSharePayload)}
|
||||
payload={publishSharePayload}
|
||||
|
||||
@@ -315,3 +315,36 @@ test('groups visible platform creation types by backend category metadata', () =
|
||||
]);
|
||||
expect(groups[1]?.items.map((item) => item.id)).toEqual(['visual-novel']);
|
||||
});
|
||||
|
||||
test('falls back when backend creation type category metadata is missing', () => {
|
||||
const cards = derivePlatformCreationTypes([
|
||||
{
|
||||
id: 'legacy-entry',
|
||||
title: '历史入口',
|
||||
subtitle: '旧数据缺少分类字段',
|
||||
badge: '可创建',
|
||||
imageSrc: '/creation-type-references/puzzle.webp',
|
||||
visible: true,
|
||||
open: true,
|
||||
sortOrder: 10,
|
||||
categoryId: undefined as unknown as string,
|
||||
categoryLabel: undefined as unknown as string,
|
||||
categorySortOrder: 0,
|
||||
updatedAtMicros: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(cards[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 'legacy-entry',
|
||||
categoryId: 'recent',
|
||||
categoryLabel: '最近创作',
|
||||
}),
|
||||
);
|
||||
expect(groupVisiblePlatformCreationTypes(cards)).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'recent',
|
||||
label: '最近创作',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -55,13 +55,13 @@ export function isPlatformCreationTypeOpen(
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeCategoryId(value: string) {
|
||||
const normalized = value.trim();
|
||||
function normalizeCategoryId(value: string | null | undefined) {
|
||||
const normalized = typeof value === 'string' ? value.trim() : '';
|
||||
return normalized || FALLBACK_CREATION_CATEGORY_ID;
|
||||
}
|
||||
|
||||
function normalizeCategoryLabel(value: string) {
|
||||
const normalized = value.trim();
|
||||
function normalizeCategoryLabel(value: string | null | undefined) {
|
||||
const normalized = typeof value === 'string' ? value.trim() : '';
|
||||
return normalized || FALLBACK_CREATION_CATEGORY_LABEL;
|
||||
}
|
||||
|
||||
|
||||
47
src/components/platform-entry/platformEntryResponsive.ts
Normal file
47
src/components/platform-entry/platformEntryResponsive.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const PLATFORM_DESKTOP_LAYOUT_QUERY = '(min-width: 1024px)';
|
||||
|
||||
export function getInitialPlatformDesktopLayout() {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof window.matchMedia !== 'function'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.matchMedia(PLATFORM_DESKTOP_LAYOUT_QUERY).matches;
|
||||
}
|
||||
|
||||
export function usePlatformDesktopLayout() {
|
||||
const [isDesktopLayout, setIsDesktopLayout] = useState(
|
||||
getInitialPlatformDesktopLayout,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof window.matchMedia !== 'function'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia(PLATFORM_DESKTOP_LAYOUT_QUERY);
|
||||
const updateLayout = (event?: MediaQueryListEvent) => {
|
||||
setIsDesktopLayout(event?.matches ?? mediaQuery.matches);
|
||||
};
|
||||
|
||||
updateLayout();
|
||||
|
||||
// 平台页只挂载当前断点外壳,避免隐藏的移动端/桌面端内容重复抢占查询。
|
||||
if (typeof mediaQuery.addEventListener === 'function') {
|
||||
mediaQuery.addEventListener('change', updateLayout);
|
||||
return () => mediaQuery.removeEventListener('change', updateLayout);
|
||||
}
|
||||
|
||||
mediaQuery.addListener(updateLayout);
|
||||
return () => mediaQuery.removeListener(updateLayout);
|
||||
}, []);
|
||||
|
||||
return isDesktopLayout;
|
||||
}
|
||||
@@ -551,9 +551,9 @@ test('puzzle workspace hides prompt and cost when AI redraw is off', async () =>
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: 'first-level.png',
|
||||
pictureDescription: 'first-level.png',
|
||||
referenceImageSrc: '/generated-puzzle-assets/reference/first-level.png',
|
||||
referenceImageSrc: 'data:image/png;base64,uploaded-square',
|
||||
referenceImageSrcs: [],
|
||||
referenceImageAssetObjectId: 'asset-reference-first-level.png',
|
||||
referenceImageAssetObjectId: null,
|
||||
referenceImageAssetObjectIds: [],
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: false,
|
||||
@@ -616,22 +616,10 @@ test('puzzle workspace submits history image when AI redraw is off', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('puzzle workspace submits uploaded reference image when AI redraw is on', async () => {
|
||||
test('puzzle workspace submits uploaded reference image as data URL when AI redraw is on', async () => {
|
||||
const onCreateFromForm = vi.fn();
|
||||
const uploadedDataUrl = 'data:image/png;base64,uploaded-square';
|
||||
stubReferenceImageUpload(uploadedDataUrl);
|
||||
vi.mocked(puzzleAssetClient.uploadReferenceImage).mockResolvedValue({
|
||||
assetObjectId: 'asset-reference-main-1',
|
||||
assetKind: 'puzzle_cover_image',
|
||||
objectKey: 'generated-puzzle-assets/reference/main-1.png',
|
||||
imageSrc: '/generated-puzzle-assets/reference/main-1.png',
|
||||
ownerUserId: 'user-1',
|
||||
ownerLabel: '账号 user-1',
|
||||
profileId: null,
|
||||
entityId: null,
|
||||
createdAt: '1713686400.000000Z',
|
||||
updatedAt: '1713686400.000000Z',
|
||||
});
|
||||
|
||||
render(
|
||||
<PuzzleAgentWorkspace
|
||||
@@ -651,9 +639,7 @@ test('puzzle workspace submits uploaded reference image when AI redraw is on', a
|
||||
await waitFor(() => {
|
||||
expect(screen.getByAltText('拼图图片')).toBeTruthy();
|
||||
});
|
||||
expect(puzzleAssetClient.uploadReferenceImage).toHaveBeenCalledWith({
|
||||
file: expect.any(File),
|
||||
});
|
||||
expect(puzzleAssetClient.uploadReferenceImage).not.toHaveBeenCalled();
|
||||
fireEvent.change(screen.getByLabelText('画面AI重绘要求(提示词)'), {
|
||||
target: { value: '保留上传画面的主体和构图,改成雨夜灯街。' },
|
||||
});
|
||||
@@ -663,9 +649,9 @@ test('puzzle workspace submits uploaded reference image when AI redraw is on', a
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: '保留上传画面的主体和构图,改成雨夜灯街。',
|
||||
pictureDescription: '保留上传画面的主体和构图,改成雨夜灯街。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrc: 'data:image/png;base64,uploaded-square',
|
||||
referenceImageSrcs: [],
|
||||
referenceImageAssetObjectId: 'asset-reference-main-1',
|
||||
referenceImageAssetObjectId: null,
|
||||
referenceImageAssetObjectIds: [],
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: true,
|
||||
@@ -754,12 +740,12 @@ test('puzzle workspace uploads prompt references as asset object ids', async ()
|
||||
seedText: '一只猫在雨夜灯牌下回头。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrcs: [],
|
||||
referenceImageAssetObjectId: null,
|
||||
referenceImageAssetObjectIds: [
|
||||
'asset-reference-prompt-1',
|
||||
'asset-reference-prompt-2',
|
||||
referenceImageSrcs: [
|
||||
'data:image/png;base64,reference-1',
|
||||
'data:image/png;base64,reference-2',
|
||||
],
|
||||
referenceImageAssetObjectId: null,
|
||||
referenceImageAssetObjectIds: [],
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: true,
|
||||
});
|
||||
@@ -842,15 +828,15 @@ test('puzzle workspace uploads prompt reference images from the description box'
|
||||
seedText: '一只猫在雨夜灯牌下回头。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrcs: [],
|
||||
referenceImageAssetObjectId: null,
|
||||
referenceImageAssetObjectIds: [
|
||||
'asset-reference-reference-1.png',
|
||||
'asset-reference-reference-2.png',
|
||||
'asset-reference-reference-3.png',
|
||||
'asset-reference-reference-4.png',
|
||||
'asset-reference-reference-5.png',
|
||||
referenceImageSrcs: [
|
||||
'data:image/png;base64,reference-1',
|
||||
'data:image/png;base64,reference-2',
|
||||
'data:image/png;base64,reference-3',
|
||||
'data:image/png;base64,reference-4',
|
||||
'data:image/png;base64,reference-5',
|
||||
],
|
||||
referenceImageAssetObjectId: null,
|
||||
referenceImageAssetObjectIds: [],
|
||||
imageModel: 'gpt-image-2',
|
||||
aiRedraw: true,
|
||||
});
|
||||
|
||||
@@ -16,11 +16,9 @@ import { getPuzzleHistoryAssetReferenceLabel } from '../../services/puzzle-works
|
||||
import {
|
||||
cropPuzzleReferenceImageDataUrl,
|
||||
isPuzzleReferenceImageSquare,
|
||||
puzzleReferenceImageDataUrlToFile,
|
||||
readPuzzleReferenceImageAsDataUrl,
|
||||
readPuzzleReferenceImageForUpload,
|
||||
} from '../../services/puzzleReferenceImage';
|
||||
import { puzzleAssetClient } from '../../services/puzzle-works/puzzleAssetClient';
|
||||
import {
|
||||
CreativeImageInputPanel,
|
||||
type CreativeImageInputReferenceImage,
|
||||
@@ -409,11 +407,10 @@ export function PuzzleAgentWorkspace({
|
||||
return;
|
||||
}
|
||||
|
||||
const asset = await puzzleAssetClient.uploadReferenceImage({ file });
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
referenceImageSrc: asset.imageSrc || uploadImage.dataUrl,
|
||||
referenceImageAssetObjectId: asset.assetObjectId,
|
||||
referenceImageSrc: uploadImage.dataUrl,
|
||||
referenceImageAssetObjectId: '',
|
||||
referenceImageLabel: file.name.trim() || '本地拼图图片',
|
||||
}));
|
||||
setReferenceImageError(null);
|
||||
@@ -441,18 +438,12 @@ export function PuzzleAgentWorkspace({
|
||||
|
||||
try {
|
||||
const images = await Promise.all(
|
||||
files.slice(0, remainingSlots).map(async (file, index) => {
|
||||
const [imageSrc, asset] = await Promise.all([
|
||||
readPuzzleReferenceImageAsDataUrl(file),
|
||||
puzzleAssetClient.uploadReferenceImage({ file }),
|
||||
]);
|
||||
return {
|
||||
id: `prompt-upload:${Date.now()}:${index}:${file.name}`,
|
||||
label: file.name.trim() || `参考图 ${index + 1}`,
|
||||
imageSrc: asset.imageSrc || imageSrc,
|
||||
assetObjectId: asset.assetObjectId,
|
||||
};
|
||||
}),
|
||||
files.slice(0, remainingSlots).map(async (file, index) => ({
|
||||
id: `prompt-upload:${Date.now()}:${index}:${file.name}`,
|
||||
label: file.name.trim() || `参考图 ${index + 1}`,
|
||||
imageSrc: await readPuzzleReferenceImageAsDataUrl(file),
|
||||
assetObjectId: null,
|
||||
})),
|
||||
);
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
@@ -515,15 +506,10 @@ export function PuzzleAgentWorkspace({
|
||||
cropY: currentCropState.cropRect.y,
|
||||
cropSize: currentCropState.cropRect.size,
|
||||
});
|
||||
const file = puzzleReferenceImageDataUrlToFile(
|
||||
dataUrl,
|
||||
currentCropState.fileName,
|
||||
);
|
||||
const asset = await puzzleAssetClient.uploadReferenceImage({ file });
|
||||
setFormState((current) => ({
|
||||
...current,
|
||||
referenceImageSrc: asset.imageSrc || dataUrl,
|
||||
referenceImageAssetObjectId: asset.assetObjectId,
|
||||
referenceImageSrc: dataUrl,
|
||||
referenceImageAssetObjectId: '',
|
||||
referenceImageLabel: currentCropState.label,
|
||||
}));
|
||||
setCropState(null);
|
||||
@@ -651,6 +637,7 @@ export function PuzzleAgentWorkspace({
|
||||
aiRedraw={formState.aiRedraw}
|
||||
promptReferenceImages={formState.referenceImageSrcs}
|
||||
promptReferenceLimit={PUZZLE_PROMPT_REFERENCE_IMAGE_LIMIT}
|
||||
imageLimitHint="图片≤6MB"
|
||||
imageModelPicker={
|
||||
<PuzzleImageModelPicker
|
||||
value={formState.imageModel}
|
||||
|
||||
@@ -840,6 +840,7 @@ function PuzzleLevelDetailDialog({
|
||||
aiRedraw={aiRedraw}
|
||||
promptReferenceImages={promptReferenceImages}
|
||||
promptReferenceLimit={PUZZLE_LEVEL_PROMPT_REFERENCE_LIMIT}
|
||||
imageLimitHint="图片≤6MB"
|
||||
imageModelPicker={
|
||||
<PuzzleImageModelPicker
|
||||
value={imageModel}
|
||||
|
||||
@@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
|
||||
@@ -178,6 +179,35 @@ import {
|
||||
type SelectionStage,
|
||||
} from './RpgEntryFlowShell';
|
||||
|
||||
const authServiceMocks = vi.hoisted(() => ({
|
||||
ensureRuntimeGuestToken: vi.fn(async () => ({
|
||||
token: 'runtime-guest-token',
|
||||
expiresAt: '2099-01-01T00:00:00.000Z',
|
||||
})),
|
||||
getPublicAuthUserByCode: vi.fn(
|
||||
async (publicUserCode: string): Promise<PublicUserSummary> => ({
|
||||
id: `public-user-${publicUserCode}`,
|
||||
publicUserCode,
|
||||
displayName: '公开作者',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
),
|
||||
getPublicAuthUserById: vi.fn(
|
||||
async (userId: string): Promise<PublicUserSummary> => ({
|
||||
id: userId,
|
||||
publicUserCode: `code-${userId}`,
|
||||
displayName: '公开作者',
|
||||
avatarUrl: null,
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/authService', () => ({
|
||||
ensureRuntimeGuestToken: authServiceMocks.ensureRuntimeGuestToken,
|
||||
getPublicAuthUserByCode: authServiceMocks.getPublicAuthUserByCode,
|
||||
getPublicAuthUserById: authServiceMocks.getPublicAuthUserById,
|
||||
}));
|
||||
|
||||
async function clickFirstButtonByName(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
name: string | RegExp,
|
||||
@@ -276,6 +306,11 @@ const ISOLATED_RUNTIME_AUTH_OPTIONS = {
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
};
|
||||
const RECOMMEND_RUNTIME_AUTH_OPTIONS = {
|
||||
...ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
runtimeGuestToken: 'runtime-guest-token',
|
||||
};
|
||||
const LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS = ISOLATED_RUNTIME_AUTH_OPTIONS;
|
||||
|
||||
function getPlatformTabPanel(tab: string) {
|
||||
const panel = document.getElementById(`platform-tab-panel-${tab}`);
|
||||
@@ -1085,6 +1120,10 @@ vi.mock('../bark-battle-creation/BarkBattleConfigEditor', () => ({
|
||||
}) => (
|
||||
<div className="bark-battle-config-editor-mock">
|
||||
<div>汪汪声浪配置表单</div>
|
||||
<label>
|
||||
作品标题
|
||||
<input aria-label="汪汪作品标题" defaultValue="汪汪测试杯" />
|
||||
</label>
|
||||
<div data-testid="bark-battle-editor-back-state">
|
||||
{showBackButton ? 'back-visible' : 'back-hidden'}
|
||||
</div>
|
||||
@@ -2235,6 +2274,10 @@ function TestWrapper({
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(authServiceMocks.ensureRuntimeGuestToken).mockResolvedValue({
|
||||
token: 'runtime-guest-token',
|
||||
expiresAt: '2099-01-01T00:00:00.000Z',
|
||||
});
|
||||
vi.mocked(
|
||||
match3dGeneratedModelCache.hasMatch3DGeneratedImageAsset,
|
||||
).mockImplementation((assets) =>
|
||||
@@ -3581,11 +3624,20 @@ test('bark battle form checks mud points before creating image assets', async ()
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await findCreationTypeButton('汪汪声浪'));
|
||||
const titleInput = await screen.findByLabelText('汪汪作品标题');
|
||||
await user.clear(titleInput);
|
||||
await user.type(titleInput, '自定义声浪杯');
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 3 泥点,当前 2 泥点。'),
|
||||
within(noticeDialog).getByText('本次需要 3 泥点,当前 2 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('汪汪声浪配置表单')).toBeTruthy();
|
||||
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
|
||||
expect((screen.getByLabelText('汪汪作品标题') as HTMLInputElement).value).toBe(
|
||||
'自定义声浪杯',
|
||||
);
|
||||
expect(createBarkBattleDraft).not.toHaveBeenCalled();
|
||||
expect(generateAllBarkBattleImageAssets).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -4302,11 +4354,15 @@ test('puzzle form checks mud points before creating a draft', async () => {
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(await findCreationTypeButton('拼图'));
|
||||
await user.click(await screen.findByRole('button', { name: '生成草稿' }));
|
||||
|
||||
const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 2 泥点,当前 1 泥点。'),
|
||||
within(noticeDialog).getByText('本次需要 2 泥点,当前 1 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
expect(executePuzzleAgentAction).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -4323,14 +4379,17 @@ test('match3d form checks mud points before creating a draft', async () => {
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreateTemplateHub(user);
|
||||
await user.click(screen.getByRole('tab', { name: '抓大鹅' }));
|
||||
await user.click(await findCreationTypeButton('抓大鹅'));
|
||||
await user.click(
|
||||
await screen.findByRole('button', { name: '生成抓大鹅草稿' }),
|
||||
);
|
||||
|
||||
const noticeDialog = await screen.findByRole('dialog', { name: '泥点不足' });
|
||||
expect(
|
||||
await screen.findByText('泥点不足,本次需要 10 泥点,当前 9 泥点。'),
|
||||
within(noticeDialog).getByText('本次需要 10 泥点,当前 9 泥点。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('抓大鹅工作区:missing-session')).toBeTruthy();
|
||||
expect(screen.queryByRole('tablist', { name: '玩法模板分类' })).toBeNull();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.executeAction).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -6121,11 +6180,59 @@ test('home recommendation starts embedded puzzle without global auth reset on lo
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('home recommendation keeps logged-in puzzle start on default auth instead of guest token', async () => {
|
||||
const publishedPuzzleWork = {
|
||||
workId: 'puzzle-work-public-2',
|
||||
profileId: 'puzzle-profile-public-2',
|
||||
ownerUserId: 'user-2',
|
||||
sourceSessionId: 'puzzle-session-public-2',
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '星桥机关',
|
||||
summary: '旋转碎片并接通星桥机关。',
|
||||
themeTags: ['机关', '星桥'],
|
||||
coverImageSrc: null,
|
||||
coverAssetId: null,
|
||||
publicationStatus: 'published',
|
||||
updatedAt: '2026-04-25T09:00:00.000Z',
|
||||
publishedAt: '2026-04-25T09:00:00.000Z',
|
||||
playCount: 3,
|
||||
likeCount: 0,
|
||||
publishReady: true,
|
||||
} satisfies PuzzleWorkSummary;
|
||||
|
||||
vi.mocked(listPuzzleGallery).mockResolvedValue({
|
||||
items: [publishedPuzzleWork],
|
||||
});
|
||||
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
|
||||
item: publishedPuzzleWork,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(startPuzzleRun).toHaveBeenCalledWith(
|
||||
{
|
||||
profileId: 'puzzle-profile-public-2',
|
||||
levelId: null,
|
||||
},
|
||||
expect.objectContaining({
|
||||
authImpact: 'local',
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(vi.mocked(startPuzzleRun).mock.calls[0]?.[1]).not.toHaveProperty(
|
||||
'runtimeGuestToken',
|
||||
);
|
||||
});
|
||||
|
||||
test('home recommendation Match3D runtime keeps profile generated models when card summary is stale', async () => {
|
||||
const match3dCard: Match3DWorkSummary = {
|
||||
workId: 'match3d-work-card-1',
|
||||
@@ -6184,7 +6291,7 @@ test('home recommendation Match3D runtime keeps profile generated models when ca
|
||||
await waitFor(() => {
|
||||
expect(match3dServerRuntimeAdapterMock.startRun).toHaveBeenCalledWith(
|
||||
'match3d-profile-card-1',
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
LOGGED_IN_RECOMMEND_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
@@ -6503,7 +6610,13 @@ test('home recommendation surfaces start failure instead of staying in loading s
|
||||
expect(
|
||||
await screen.findByText('作品暂时无法进入,请稍后再试。'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('加载中...')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('home'))
|
||||
.queryByText('加载中...')
|
||||
?.closest('.platform-recommend-runtime-panel'),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
test('published big fish works stay hidden from platform home and game category channel', async () => {
|
||||
@@ -7135,7 +7248,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
vi.mocked(listProfileSaveArchives).mockClear();
|
||||
vi.mocked(listProfileSaveArchives).mockRejectedValueOnce(
|
||||
@@ -7159,7 +7271,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
elapsedMs: 18_000,
|
||||
nickname: '测试玩家',
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7180,7 +7291,6 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedFirstLevel.runId,
|
||||
{},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(
|
||||
@@ -7343,7 +7453,6 @@ test('formal puzzle similar work keeps current run level progression', async ()
|
||||
expect(advancePuzzleNextLevel).toHaveBeenCalledWith(
|
||||
clearedThirdLevel.runId,
|
||||
{ targetProfileId: 'puzzle-profile-similar-2' },
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(startPuzzleRun).not.toHaveBeenCalled();
|
||||
@@ -7527,7 +7636,6 @@ test('recommend puzzle remix return restarts recommendation instead of stale loa
|
||||
profileId: 'puzzle-profile-public-1',
|
||||
levelId: null,
|
||||
},
|
||||
ISOLATED_RUNTIME_AUTH_OPTIONS,
|
||||
);
|
||||
});
|
||||
expect(screen.queryByText('正在进入拼图关卡')).toBeNull();
|
||||
|
||||
@@ -804,6 +804,7 @@ function renderLoggedOutHomeView(
|
||||
>
|
||||
> = {},
|
||||
activeTab: RpgEntryHomeViewProps['activeTab'] = 'home',
|
||||
isDesktopLayout = false,
|
||||
) {
|
||||
return render(
|
||||
<AuthUiContext.Provider
|
||||
@@ -827,6 +828,7 @@ function renderLoggedOutHomeView(
|
||||
>
|
||||
<RpgEntryHomeView
|
||||
activeTab={activeTab}
|
||||
isDesktopLayout={isDesktopLayout}
|
||||
onTabChange={vi.fn()}
|
||||
hasSavedGame={false}
|
||||
savedSnapshot={null}
|
||||
@@ -954,6 +956,7 @@ function renderStatefulLoggedOutHomeView(
|
||||
| 'onSelectPreviousRecommendEntry'
|
||||
>
|
||||
> = {},
|
||||
isDesktopLayout = false,
|
||||
) {
|
||||
const authSpies = {
|
||||
openLoginModal: vi.fn(),
|
||||
@@ -985,6 +988,7 @@ function renderStatefulLoggedOutHomeView(
|
||||
>
|
||||
<RpgEntryHomeView
|
||||
activeTab={activeTab}
|
||||
isDesktopLayout={isDesktopLayout}
|
||||
onTabChange={setActiveTab}
|
||||
hasSavedGame={false}
|
||||
savedSnapshot={null}
|
||||
@@ -2803,7 +2807,7 @@ test('logged out mobile shell defaults to discover tab', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test('logged out recommend tab opens recommend runtime directly', async () => {
|
||||
test('logged out recommend tab opens embedded runtime without login modal', async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container, openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
@@ -2818,10 +2822,8 @@ test('logged out recommend tab opens recommend runtime directly', async () => {
|
||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
|
||||
expect(openLoginModal).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
container.querySelector('.platform-recommend-cover-only'),
|
||||
).toBeNull();
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(container.querySelector('.platform-recommend-cover-only')).toBeNull();
|
||||
expect(container.querySelector('.platform-mobile-topbar')).toBeNull();
|
||||
expect(
|
||||
container.querySelector('.platform-mobile-entry-shell--recommend'),
|
||||
@@ -2831,7 +2833,7 @@ test('logged out recommend tab opens recommend runtime directly', async () => {
|
||||
expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('logged out recommend meta keeps gallery detail gated', async () => {
|
||||
test('logged out recommend runtime keeps detail callback idle', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
const { openLoginModal } = renderStatefulLoggedOutHomeView({
|
||||
@@ -2847,9 +2849,9 @@ test('logged out recommend meta keeps gallery detail gated', async () => {
|
||||
await user.click(
|
||||
within(bottomNav as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
await user.click(screen.getByLabelText('奇幻拼图 作品信息'));
|
||||
|
||||
expect(openLoginModal).toHaveBeenCalledTimes(1);
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2858,16 +2860,15 @@ test('logged out desktop recommend page renders runtime directly', () => {
|
||||
renderLoggedOutHomeView(vi.fn(), {
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
});
|
||||
}, 'home', true);
|
||||
|
||||
expect(document.querySelector('.platform-recommend-cover-only')).toBeNull();
|
||||
expect(screen.queryByText('今日游戏')).toBeNull();
|
||||
expect(screen.queryByText('作品分类')).toBeNull();
|
||||
expect(screen.getByTestId('recommend-runtime')).toBeTruthy();
|
||||
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
|
||||
expect(screen.getByText('今日游戏')).toBeTruthy();
|
||||
expect(screen.getByText('作品分类')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('logged out recommend page can enter runtime without login gate', () => {
|
||||
mockDesktopLayout();
|
||||
const openLoginModal = vi.fn();
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
renderLoggedOutHomeView(openLoginModal, {
|
||||
@@ -2883,6 +2884,35 @@ test('logged out recommend page can enter runtime without login gate', () => {
|
||||
expect(onOpenGalleryDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('logged out desktop recommend rail enters runtime without login modal', async () => {
|
||||
mockDesktopLayout();
|
||||
const user = userEvent.setup();
|
||||
const openLoginModal = vi.fn();
|
||||
|
||||
const { container } = renderLoggedOutHomeView(
|
||||
openLoginModal,
|
||||
{
|
||||
latestEntries: [puzzlePublicEntry],
|
||||
activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1',
|
||||
},
|
||||
'category',
|
||||
true,
|
||||
);
|
||||
|
||||
const desktopRail = container.querySelector('.platform-desktop-rail');
|
||||
if (!desktopRail) {
|
||||
throw new Error('缺少桌面侧边栏');
|
||||
}
|
||||
|
||||
await user.click(
|
||||
within(desktopRail as HTMLElement).getByRole('button', { name: '推荐' }),
|
||||
);
|
||||
|
||||
expect(openLoginModal).not.toHaveBeenCalled();
|
||||
expect(screen.queryByTestId('recommend-runtime')).toBeNull();
|
||||
expect(container.querySelector('.platform-desktop-shell')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('logged in recommend page uses gated recommend detail callback', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onOpenGalleryDetail = vi.fn();
|
||||
|
||||
@@ -131,6 +131,7 @@ import {
|
||||
findPublicWorkForHistoryEntry,
|
||||
isEdutainmentEntryEnabled,
|
||||
} from '../platform-entry/platformEdutainmentVisibility';
|
||||
import { getInitialPlatformDesktopLayout } from '../platform-entry/platformEntryResponsive';
|
||||
import { ResolvedAssetImage } from '../ResolvedAssetImage';
|
||||
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
|
||||
import {
|
||||
@@ -165,6 +166,7 @@ export type PlatformHomeTab =
|
||||
| 'profile';
|
||||
export interface RpgEntryHomeViewProps {
|
||||
activeTab: PlatformHomeTab;
|
||||
isDesktopLayout?: boolean;
|
||||
onTabChange: (tab: PlatformHomeTab) => void;
|
||||
hasSavedGame: boolean;
|
||||
savedSnapshot: HydratedSavedGameSnapshot | null;
|
||||
@@ -234,7 +236,6 @@ const DESKTOP_PAGE_STAGE_CLASS =
|
||||
'platform-page-stage platform-remap-surface min-w-0 space-y-5 pb-4';
|
||||
const DESKTOP_DISCOVER_PAGE_STAGE_CLASS =
|
||||
'platform-remap-surface min-w-0 space-y-5 pb-4';
|
||||
const DESKTOP_LAYOUT_QUERY = '(min-width: 1024px)';
|
||||
const PLATFORM_HOME_TABS: PlatformHomeTab[] = [
|
||||
'home',
|
||||
'category',
|
||||
@@ -416,46 +417,6 @@ const PLATFORM_RANKING_TABS: Array<{
|
||||
emptyText: '公开广场暂时还没有点赞作品。',
|
||||
},
|
||||
];
|
||||
function usePlatformDesktopLayout() {
|
||||
const [isDesktopLayout, setIsDesktopLayout] = useState(() => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof window.matchMedia !== 'function'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return window.matchMedia(DESKTOP_LAYOUT_QUERY).matches;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
typeof window.matchMedia !== 'function'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia(DESKTOP_LAYOUT_QUERY);
|
||||
const updateLayout = (event?: MediaQueryListEvent) => {
|
||||
setIsDesktopLayout(event?.matches ?? mediaQuery.matches);
|
||||
};
|
||||
|
||||
updateLayout();
|
||||
|
||||
// 平台页只挂载当前断点外壳,避免隐藏的移动端/桌面端内容重复抢占查询。
|
||||
if (typeof mediaQuery.addEventListener === 'function') {
|
||||
mediaQuery.addEventListener('change', updateLayout);
|
||||
return () => mediaQuery.removeEventListener('change', updateLayout);
|
||||
}
|
||||
|
||||
mediaQuery.addListener(updateLayout);
|
||||
return () => mediaQuery.removeListener(updateLayout);
|
||||
}, []);
|
||||
|
||||
return isDesktopLayout;
|
||||
}
|
||||
|
||||
function ResolvedAssetBackdrop({
|
||||
src,
|
||||
fallbackSrc,
|
||||
@@ -3978,6 +3939,7 @@ function ProfilePlayedWorksModal({
|
||||
|
||||
export function RpgEntryHomeView({
|
||||
activeTab,
|
||||
isDesktopLayout: isDesktopLayoutProp,
|
||||
onTabChange,
|
||||
saveEntries,
|
||||
saveError,
|
||||
@@ -4137,7 +4099,8 @@ export function RpgEntryHomeView({
|
||||
const [isSavingAvatar, setIsSavingAvatar] = useState(false);
|
||||
const isAuthenticated = Boolean(authUi?.user);
|
||||
const edutainmentEntryEnabled = isEdutainmentEntryEnabled();
|
||||
const isDesktopLayout = usePlatformDesktopLayout();
|
||||
const [fallbackDesktopLayout] = useState(getInitialPlatformDesktopLayout);
|
||||
const isDesktopLayout = isDesktopLayoutProp ?? fallbackDesktopLayout;
|
||||
const openRecommendGalleryDetail =
|
||||
onOpenRecommendGalleryDetail ?? onOpenGalleryDetail;
|
||||
const generalFeaturedEntries = useMemo(
|
||||
@@ -5494,7 +5457,7 @@ export function RpgEntryHomeView({
|
||||
{recommendRuntimeError}
|
||||
</button>
|
||||
</section>
|
||||
) : isStartingRecommendEntry || !recommendRuntimeContent ? (
|
||||
) : isStartingRecommendEntry ? (
|
||||
<section className="platform-recommend-runtime-panel">
|
||||
<div className="platform-recommend-runtime-state">加载中...</div>
|
||||
</section>
|
||||
@@ -6670,10 +6633,7 @@ export function RpgEntryHomeView({
|
||||
);
|
||||
|
||||
const tabContentById = {
|
||||
home:
|
||||
!isAuthenticated || !isDesktopLayout
|
||||
? mobileRecommendContent
|
||||
: desktopHomeContent,
|
||||
home: isDesktopLayout ? desktopHomeContent : mobileRecommendContent,
|
||||
category: categoryContent,
|
||||
create: createContent,
|
||||
saves: savesContent,
|
||||
@@ -6885,12 +6845,6 @@ export function RpgEntryHomeView({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAuthenticated && tab === 'home') {
|
||||
onTabChange(tab);
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
@@ -7049,12 +7003,6 @@ export function RpgEntryHomeView({
|
||||
emphasized={tab === 'create'}
|
||||
showDot={tab === 'saves' && hasUnreadDraftUpdate}
|
||||
onClick={() => {
|
||||
if (!isAuthenticated && tab === 'home') {
|
||||
onTabChange(tab);
|
||||
authUi?.openLoginModal();
|
||||
return;
|
||||
}
|
||||
|
||||
onTabChange(tab);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -351,7 +351,7 @@ export function useRpgEntryBootstrap(
|
||||
!hasInitialAgentSession &&
|
||||
!hasExplicitPlatformTabSelectionRef.current
|
||||
) {
|
||||
// 中文注释:新用户先进入发现页;推荐页只在用户主动点击后作为登录门禁入口。
|
||||
// 中文注释:新用户先进入发现页;推荐页可直接进入,真正受保护的动作再单独做登录门禁。
|
||||
setPlatformTabState(isAuthenticated ? 'home' : 'category');
|
||||
}
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user