feat: integrate jump-hop shelf and asset flow
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;
|
||||
@@ -95,6 +96,7 @@ export function CreativeImageInputPanel({
|
||||
aiRedraw,
|
||||
promptReferenceImages,
|
||||
promptReferenceLimit = DEFAULT_PROMPT_REFERENCE_LIMIT,
|
||||
imageLimitHint = null,
|
||||
imageModelPicker = null,
|
||||
error = null,
|
||||
inputError = null,
|
||||
@@ -274,6 +276,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,11 +7,13 @@ 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,
|
||||
buildBarkBattlePublicWorkCode,
|
||||
buildBigFishPublicWorkCode,
|
||||
buildJumpHopPublicWorkCode,
|
||||
buildMatch3DPublicWorkCode,
|
||||
buildPuzzlePublicWorkCode,
|
||||
buildSquareHolePublicWorkCode,
|
||||
@@ -30,6 +32,7 @@ export type CreationWorkShelfKind =
|
||||
| 'big-fish'
|
||||
| 'match3d'
|
||||
| 'square-hole'
|
||||
| 'jump-hop'
|
||||
| 'puzzle'
|
||||
| 'baby-object-match'
|
||||
| 'bark-battle'
|
||||
@@ -82,6 +85,10 @@ export type CreationWorkShelfSource =
|
||||
kind: 'square-hole';
|
||||
item: SquareHoleWorkSummary;
|
||||
}
|
||||
| {
|
||||
kind: 'jump-hop';
|
||||
item: JumpHopWorkSummaryResponse;
|
||||
}
|
||||
| {
|
||||
kind: 'puzzle';
|
||||
item: PuzzleWorkSummary;
|
||||
@@ -136,6 +143,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
bigFishItems: BigFishWorkSummary[];
|
||||
match3dItems?: Match3DWorkSummary[];
|
||||
squareHoleItems?: SquareHoleWorkSummary[];
|
||||
jumpHopItems?: JumpHopWorkSummaryResponse[];
|
||||
puzzleItems: PuzzleWorkSummary[];
|
||||
babyObjectMatchItems?: BabyObjectMatchDraft[];
|
||||
barkBattleItems?: BarkBattleWorkSummary[];
|
||||
@@ -144,6 +152,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteBigFish?: boolean;
|
||||
canDeleteMatch3D?: boolean;
|
||||
canDeleteSquareHole?: boolean;
|
||||
canDeleteJumpHop?: boolean;
|
||||
canDeletePuzzle?: boolean;
|
||||
canDeleteBabyObjectMatch?: boolean;
|
||||
canDeleteBarkBattle?: boolean;
|
||||
@@ -157,6 +166,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;
|
||||
@@ -176,6 +187,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
bigFishItems,
|
||||
match3dItems = [],
|
||||
squareHoleItems = [],
|
||||
jumpHopItems = [],
|
||||
puzzleItems,
|
||||
babyObjectMatchItems = [],
|
||||
barkBattleItems = [],
|
||||
@@ -184,6 +196,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteBigFish = false,
|
||||
canDeleteMatch3D = false,
|
||||
canDeleteSquareHole = false,
|
||||
canDeleteJumpHop = false,
|
||||
canDeletePuzzle = false,
|
||||
canDeleteBabyObjectMatch = false,
|
||||
canDeleteBarkBattle = false,
|
||||
@@ -197,6 +210,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDeleteMatch3D,
|
||||
onOpenSquareHoleDetail,
|
||||
onDeleteSquareHole,
|
||||
onOpenJumpHopDetail,
|
||||
onDeleteJumpHop,
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle,
|
||||
onClaimPuzzlePointIncentive,
|
||||
@@ -235,6 +250,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,
|
||||
@@ -745,6 +766,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>
|
||||
|
||||
@@ -177,9 +177,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 {
|
||||
@@ -1853,7 +1854,7 @@ function hasRecoverableGeneratedPuzzleDraft(
|
||||
);
|
||||
}
|
||||
|
||||
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem) {
|
||||
function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem): string[] {
|
||||
switch (item.source.kind) {
|
||||
case 'rpg':
|
||||
return collectDraftNoticeKeys('rpg', [
|
||||
@@ -1882,6 +1883,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,
|
||||
@@ -1967,6 +1975,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[],
|
||||
@@ -2637,6 +2678,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] =
|
||||
@@ -2855,6 +2899,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
creationEntryTypes,
|
||||
'big-fish',
|
||||
);
|
||||
const isJumpHopCreationVisible = isPlatformCreationTypeVisible(
|
||||
creationEntryTypes,
|
||||
'jump-hop',
|
||||
);
|
||||
const isSquareHoleCreationVisible = isPlatformCreationTypeVisible(
|
||||
creationEntryTypes,
|
||||
'square-hole',
|
||||
@@ -3304,6 +3352,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();
|
||||
@@ -3513,6 +3577,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,
|
||||
@@ -3860,6 +3940,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
],
|
||||
[bigFishWorks, pendingDraftShelfItems],
|
||||
);
|
||||
const jumpHopShelfItems = useMemo(
|
||||
() => [
|
||||
...buildPendingJumpHopWorks(
|
||||
pendingDraftShelfItems['jump-hop'],
|
||||
jumpHopWorks,
|
||||
),
|
||||
...jumpHopWorks,
|
||||
],
|
||||
[jumpHopWorks, pendingDraftShelfItems],
|
||||
);
|
||||
const match3dShelfItems = useMemo(
|
||||
() => [
|
||||
...buildPendingMatch3DWorks(pendingDraftShelfItems.match3d, match3dWorks),
|
||||
@@ -3935,6 +4025,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,
|
||||
@@ -3977,6 +4074,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
babyObjectMatchDrafts,
|
||||
barkBattleShelfItems,
|
||||
bigFishShelfItems,
|
||||
jumpHopShelfItems,
|
||||
creationHubItems,
|
||||
isSquareHoleCreationVisible,
|
||||
match3dShelfItems,
|
||||
@@ -7312,6 +7410,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(
|
||||
@@ -7426,6 +7540,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(
|
||||
@@ -10121,6 +10239,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);
|
||||
@@ -12842,6 +12997,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
deletingWorkId={deletingCreationWorkId}
|
||||
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
|
||||
bigFishItems={isBigFishCreationVisible ? bigFishShelfItems : []}
|
||||
jumpHopItems={isJumpHopCreationVisible ? jumpHopShelfItems : []}
|
||||
onOpenBigFishDetail={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
@@ -12851,6 +13007,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onOpenJumpHopDetail={
|
||||
isJumpHopCreationVisible
|
||||
? (item) => {
|
||||
runProtectedAction(() => {
|
||||
void openJumpHopDraft(item);
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onDeleteBigFish={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
@@ -12858,6 +13023,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
: null
|
||||
}
|
||||
onDeleteJumpHop={null}
|
||||
match3dItems={match3dShelfItems}
|
||||
onOpenMatch3DDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkMutationResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
JumpHopWorksResponse,
|
||||
JumpHopWorkspaceCreateRequest,
|
||||
JumpHopWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/jumpHop';
|
||||
@@ -41,6 +42,7 @@ export type {
|
||||
JumpHopWorkDetailResponse,
|
||||
JumpHopWorkMutationResponse,
|
||||
JumpHopWorkProfileResponse,
|
||||
JumpHopWorksResponse,
|
||||
JumpHopWorkspaceCreateRequest,
|
||||
};
|
||||
export type CreateJumpHopSessionRequest = {
|
||||
@@ -199,6 +201,17 @@ export async function getJumpHopGalleryDetail(publicWorkCode: string) {
|
||||
return normalizeJumpHopWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
export async function listJumpHopWorks() {
|
||||
return requestJson<JumpHopWorksResponse>(
|
||||
JUMP_HOP_WORKS_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取跳一跳作品列表失败',
|
||||
{
|
||||
retry: JUMP_HOP_RUNTIME_READ_RETRY,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function publishJumpHopWork(profileId: string) {
|
||||
const response = await requestJson<JumpHopWorkMutationResponse>(
|
||||
`${JUMP_HOP_WORKS_API_BASE}/${encodeURIComponent(profileId)}/publish`,
|
||||
@@ -267,6 +280,7 @@ export const jumpHopClient = {
|
||||
getGalleryDetail: getJumpHopGalleryDetail,
|
||||
getWorkDetail: getJumpHopWorkDetail,
|
||||
listGallery: listJumpHopGallery,
|
||||
listWorks: listJumpHopWorks,
|
||||
publishWork: publishJumpHopWork,
|
||||
restartRun: restartJumpHopRuntimeRun,
|
||||
startRun: startJumpHopRuntimeRun,
|
||||
|
||||
24
src/services/puzzle-works/puzzleAssetClient.test.ts
Normal file
24
src/services/puzzle-works/puzzleAssetClient.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
||||
validatePuzzleReferenceImageFile,
|
||||
} from './puzzleAssetClient';
|
||||
|
||||
describe('puzzle reference image upload validation', () => {
|
||||
test('limits uploads to 6MB', () => {
|
||||
expect(PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES).toBe(6 * 1024 * 1024);
|
||||
});
|
||||
|
||||
test('rejects files that exceed the upload limit with a precise message', () => {
|
||||
const file = new File([
|
||||
'x'.repeat(PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES + 1),
|
||||
], 'too-large.png', { type: 'image/png' });
|
||||
|
||||
expect(() => validatePuzzleReferenceImageFile(file)).toThrow(
|
||||
'参考图过大,请压缩后再上传(当前 6.0MB,最多 6MB)。',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ASSET_API_PATHS } from '../../editor/shared/editorApiClient';
|
||||
import { requestJson } from '../apiClient';
|
||||
import {
|
||||
PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
||||
validatePuzzleReferenceImageFile,
|
||||
} from '../puzzleReferenceImage';
|
||||
|
||||
export type PuzzleHistoryAsset = {
|
||||
assetObjectId: string;
|
||||
@@ -40,8 +44,6 @@ type ConfirmAssetObjectResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
const PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES = 12 * 1024 * 1024;
|
||||
|
||||
const MIME_BY_EXTENSION: Record<string, string> = {
|
||||
jpeg: 'image/jpeg',
|
||||
jpg: 'image/jpeg',
|
||||
@@ -58,14 +60,9 @@ function resolvePuzzleImageContentType(file: File) {
|
||||
return MIME_BY_EXTENSION[extension] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
function validatePuzzleReferenceImageFile(file: File) {
|
||||
function validatePuzzleReferenceImageUploadFile(file: File) {
|
||||
const contentType = resolvePuzzleImageContentType(file);
|
||||
if (file.size <= 0) {
|
||||
throw new Error('参考图文件为空,请重新选择。');
|
||||
}
|
||||
if (file.size > PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES) {
|
||||
throw new Error('参考图过大,请压缩后再上传。');
|
||||
}
|
||||
validatePuzzleReferenceImageFile(file);
|
||||
if (!contentType.startsWith('image/')) {
|
||||
throw new Error('参考图必须是图片文件。');
|
||||
}
|
||||
@@ -96,7 +93,7 @@ async function postDirectUploadFile(
|
||||
export async function uploadPuzzleReferenceImage(payload: {
|
||||
file: File;
|
||||
}): Promise<PuzzleReferenceAsset> {
|
||||
validatePuzzleReferenceImageFile(payload.file);
|
||||
validatePuzzleReferenceImageUploadFile(payload.file);
|
||||
const contentType = resolvePuzzleImageContentType(payload.file);
|
||||
const uploadedAt = Date.now();
|
||||
const ticket = await requestJson<DirectUploadTicketResponse>(
|
||||
@@ -157,7 +154,12 @@ export async function uploadPuzzleReferenceImage(payload: {
|
||||
|
||||
export const puzzleReferenceAssetTestUtils = {
|
||||
maxUploadBytes: PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
||||
validateFile: validatePuzzleReferenceImageFile,
|
||||
validateFile: validatePuzzleReferenceImageUploadFile,
|
||||
};
|
||||
|
||||
export {
|
||||
PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES,
|
||||
validatePuzzleReferenceImageUploadFile as validatePuzzleReferenceImageFile,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('readPuzzleReferenceImageAsDataUrl', () => {
|
||||
const dataUrl = await readPuzzleReferenceImageAsDataUrl(file);
|
||||
|
||||
expect(dataUrl).toBe(`data:image/jpeg;base64,${'C'.repeat(1000)}`);
|
||||
expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 1536, 1152);
|
||||
expect(drawImage).toHaveBeenCalledWith(expect.anything(), 0, 0, 1024, 768);
|
||||
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.84);
|
||||
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.76);
|
||||
expect(toDataURL).toHaveBeenCalledWith('image/jpeg', 0.68);
|
||||
@@ -114,7 +114,7 @@ describe('readPuzzleReferenceImageAsDataUrl', () => {
|
||||
});
|
||||
|
||||
await expect(readPuzzleReferenceImageAsDataUrl(file)).rejects.toThrow(
|
||||
'参考图过大,请换一张尺寸更小的图片。',
|
||||
'参考图过大,请压缩后再上传(当前 10.0MB,最多 6MB)。',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
const PUZZLE_REFERENCE_IMAGE_MAX_EDGE = 1024;
|
||||
const PUZZLE_REFERENCE_IMAGE_COMPRESS_TRIGGER_BYTES = 1536 * 1024;
|
||||
export const PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES = 6 * 1024 * 1024;
|
||||
export const PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH = 10 * 1024 * 1024;
|
||||
const PUZZLE_REFERENCE_IMAGE_SQUARE_TOLERANCE = 1;
|
||||
|
||||
export function formatPuzzleReferenceImageUploadBytes(bytes: number) {
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
export function buildPuzzleReferenceImageTooLargeMessage(actualBytes: number) {
|
||||
return `参考图过大,请压缩后再上传(当前 ${formatPuzzleReferenceImageUploadBytes(actualBytes)},最多 6MB)。`;
|
||||
}
|
||||
|
||||
export function validatePuzzleReferenceImageFile(file: File) {
|
||||
if (file.size <= 0) {
|
||||
throw new Error('参考图文件为空,请重新选择。');
|
||||
}
|
||||
if (file.size > PUZZLE_REFERENCE_IMAGE_MAX_UPLOAD_BYTES) {
|
||||
throw new Error(buildPuzzleReferenceImageTooLargeMessage(file.size));
|
||||
}
|
||||
if (file.type.trim() && !file.type.trim().startsWith('image/')) {
|
||||
throw new Error('参考图必须是图片文件。');
|
||||
}
|
||||
}
|
||||
|
||||
type PuzzleReferenceImageSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -36,7 +57,7 @@ function readFileAsDataUrl(file: File) {
|
||||
|
||||
function ensureReferenceImageWithinLimit(dataUrl: string) {
|
||||
if (dataUrl.length > PUZZLE_REFERENCE_IMAGE_MAX_DATA_URL_LENGTH) {
|
||||
throw new Error('参考图过大,请换一张尺寸更小的图片。');
|
||||
throw new Error(buildPuzzleReferenceImageTooLargeMessage(dataUrl.length));
|
||||
}
|
||||
return dataUrl;
|
||||
}
|
||||
@@ -130,6 +151,7 @@ async function compressReferenceImageDataUrl(file: File, dataUrl: string) {
|
||||
}
|
||||
|
||||
export async function readPuzzleReferenceImageAsDataUrl(file: File) {
|
||||
validatePuzzleReferenceImageFile(file);
|
||||
const dataUrl = await readFileAsDataUrl(file);
|
||||
try {
|
||||
const compressedDataUrl = await compressReferenceImageDataUrl(
|
||||
@@ -150,6 +172,7 @@ export async function readPuzzleReferenceImageAsDataUrl(file: File) {
|
||||
export async function readPuzzleReferenceImageForUpload(
|
||||
file: File,
|
||||
): Promise<PuzzleReferenceImageReadResult> {
|
||||
validatePuzzleReferenceImageFile(file);
|
||||
const dataUrl = await readFileAsDataUrl(file);
|
||||
const image = await loadReferenceImage(dataUrl);
|
||||
const size = resolveReferenceImageNaturalSize(image);
|
||||
|
||||
Reference in New Issue
Block a user