0876361557
显式项目读取失败时返回项目页 403 和 404 项目访问错误不再停留在画布 补充无效项目 ID 回项目页测试
765 lines
22 KiB
TypeScript
765 lines
22 KiB
TypeScript
import {
|
|
type RefObject,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import { ApiClientError } from '../../services/apiClient';
|
|
import { replaceAppHistoryPath } from '../../routing/appPageRoutes';
|
|
import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient';
|
|
import { putEditorProjectCoverCache } from '../../services/image-editor/editorProjectCoverCache';
|
|
import {
|
|
createEditorProjectResource,
|
|
type EditorProjectResourceSnapshot,
|
|
type EditorProjectSnapshot,
|
|
loadEditorProject,
|
|
loadOrCreateRecentEditorProject,
|
|
saveEditorProjectLayout,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import {
|
|
canvasDisplayViewportToViewport,
|
|
type CanvasLayerResourceMetadata,
|
|
hydrateLayer,
|
|
isInlineEditorMediaSource,
|
|
serializeCanvasLayout,
|
|
splitCanvasLayoutItems,
|
|
viewportToCanvasDisplayViewport,
|
|
} from './ImageCanvasEditorModel';
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasLayer,
|
|
CanvasViewport,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
buildProjectCoverSnapshotSignature,
|
|
createProjectCoverSnapshotBlob,
|
|
PROJECT_COVER_SNAPSHOT_ASSET_KIND,
|
|
PROJECT_COVER_SNAPSHOT_OBJECT_ASSET_KIND,
|
|
PROJECT_COVER_SNAPSHOT_SIZE,
|
|
} from './ImageCanvasProjectCoverSnapshotModel';
|
|
|
|
type ProjectResourceOptions = {
|
|
onCreated?: (resourceId: string) => void;
|
|
snapshotLayers?: CanvasLayer[];
|
|
};
|
|
|
|
type PendingProjectResourceLayer = {
|
|
layer: CanvasLayer;
|
|
options: ProjectResourceOptions;
|
|
};
|
|
|
|
type PendingProjectLayoutSave = {
|
|
projectId: string;
|
|
input: Parameters<typeof saveEditorProjectLayout>[1];
|
|
};
|
|
|
|
type CachedEditorProjectSnapshot = {
|
|
project: EditorProjectSnapshot;
|
|
cachedAt: number;
|
|
};
|
|
|
|
type ImageCanvasProjectPersistenceRefs = {
|
|
layersRef: RefObject<CanvasLayer[]>;
|
|
viewportRef: RefObject<CanvasViewport>;
|
|
canvasGenerationDialogsRef: RefObject<CanvasGenerationDialogState[]>;
|
|
};
|
|
|
|
type ImageCanvasProjectPersistenceSetters = {
|
|
setProjectTitle: (title: string) => void;
|
|
setProjectRenameValue: (title: string) => void;
|
|
setViewport: (viewport: CanvasViewport) => void;
|
|
setLayers: (layers: CanvasLayer[]) => void;
|
|
selectSingleLayer: (layerId: string | null) => void;
|
|
setLayerCounter: (value: number) => void;
|
|
restoreCanvasGenerationDialogs: (
|
|
dialogs: CanvasGenerationDialogState[],
|
|
) => void;
|
|
};
|
|
|
|
function isEditorAuthError(error: unknown) {
|
|
return (
|
|
error instanceof ApiClientError &&
|
|
error.status === 401
|
|
);
|
|
}
|
|
|
|
function isEditorProjectAccessError(error: unknown) {
|
|
return (
|
|
error instanceof ApiClientError &&
|
|
(error.status === 403 || error.status === 404)
|
|
);
|
|
}
|
|
|
|
function isLocalProjectResourceId(resourceId: string) {
|
|
return resourceId.startsWith('local-');
|
|
}
|
|
|
|
function resolveProjectResourceCreateImageSrc(layer: CanvasLayer) {
|
|
const objectKey = layer.objectKey?.trim();
|
|
if (objectKey) {
|
|
return `/${objectKey.replace(/^\/+/u, '')}`;
|
|
}
|
|
if (isInlineEditorMediaSource(layer.src)) {
|
|
return null;
|
|
}
|
|
return layer.src;
|
|
}
|
|
|
|
const EDITOR_PROJECT_SESSION_CACHE_PREFIX =
|
|
'genarrative.imageCanvas.projectSnapshot.v1:';
|
|
const EDITOR_PROJECT_RECENT_SESSION_CACHE_KEY = `${EDITOR_PROJECT_SESSION_CACHE_PREFIX}recent`;
|
|
|
|
function getEditorProjectSessionStorage() {
|
|
try {
|
|
return globalThis.sessionStorage ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function editorProjectSessionCacheKey(projectId: string) {
|
|
return `${EDITOR_PROJECT_SESSION_CACHE_PREFIX}${encodeURIComponent(projectId)}`;
|
|
}
|
|
|
|
function projectSnapshotContainsInlineMedia(project: EditorProjectSnapshot) {
|
|
try {
|
|
return /"(?:data:image\/|data:video\/|data:audio\/|blob:)/iu.test(
|
|
JSON.stringify(project),
|
|
);
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function readEditorProjectSessionCache(projectId: string | null) {
|
|
const storage = getEditorProjectSessionStorage();
|
|
if (!storage) {
|
|
return null;
|
|
}
|
|
const key = projectId
|
|
? editorProjectSessionCacheKey(projectId)
|
|
: EDITOR_PROJECT_RECENT_SESSION_CACHE_KEY;
|
|
try {
|
|
const rawValue = storage.getItem(key);
|
|
if (!rawValue) {
|
|
return null;
|
|
}
|
|
const cached = JSON.parse(rawValue) as Partial<CachedEditorProjectSnapshot>;
|
|
if (!cached.project || typeof cached.cachedAt !== 'number') {
|
|
storage.removeItem(key);
|
|
return null;
|
|
}
|
|
if (
|
|
projectId &&
|
|
cached.project.projectId &&
|
|
cached.project.projectId !== projectId
|
|
) {
|
|
storage.removeItem(key);
|
|
return null;
|
|
}
|
|
if (projectSnapshotContainsInlineMedia(cached.project)) {
|
|
storage.removeItem(key);
|
|
return null;
|
|
}
|
|
return cached.project;
|
|
} catch {
|
|
try {
|
|
storage.removeItem(key);
|
|
} catch {
|
|
// ignore storage cleanup errors
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function removeEditorProjectSessionCache(projectId: string) {
|
|
const storage = getEditorProjectSessionStorage();
|
|
if (!storage) {
|
|
return;
|
|
}
|
|
try {
|
|
storage.removeItem(editorProjectSessionCacheKey(projectId));
|
|
} catch {
|
|
// ignore storage cleanup errors
|
|
}
|
|
}
|
|
|
|
function writeEditorProjectSessionCache(project: EditorProjectSnapshot) {
|
|
if (projectSnapshotContainsInlineMedia(project)) {
|
|
return;
|
|
}
|
|
const storage = getEditorProjectSessionStorage();
|
|
if (!storage) {
|
|
return;
|
|
}
|
|
const cached: CachedEditorProjectSnapshot = {
|
|
project,
|
|
cachedAt: Date.now(),
|
|
};
|
|
try {
|
|
const serialized = JSON.stringify(cached);
|
|
storage.setItem(
|
|
editorProjectSessionCacheKey(project.projectId),
|
|
serialized,
|
|
);
|
|
storage.setItem(EDITOR_PROJECT_RECENT_SESSION_CACHE_KEY, serialized);
|
|
} catch {
|
|
// ignore quota or privacy mode failures
|
|
}
|
|
}
|
|
|
|
function createEditorProjectSessionCacheResource({
|
|
projectId,
|
|
layer,
|
|
}: {
|
|
projectId: string;
|
|
layer: CanvasLayer;
|
|
}): EditorProjectResourceSnapshot | null {
|
|
const imageSrc = resolveProjectResourceCreateImageSrc(layer);
|
|
if (!imageSrc) {
|
|
return null;
|
|
}
|
|
return {
|
|
resourceId: layer.resourceId,
|
|
projectId,
|
|
imageSrc,
|
|
objectKey: layer.objectKey,
|
|
assetObjectId: layer.assetObjectId,
|
|
width: layer.originalWidth,
|
|
height: layer.originalHeight,
|
|
sourceType: layer.sourceType,
|
|
prompt: layer.prompt,
|
|
actualPrompt: layer.actualPrompt,
|
|
model: layer.model,
|
|
provider: layer.provider,
|
|
taskId: layer.taskId,
|
|
durationSeconds: layer.durationSeconds,
|
|
sourceResourceId: layer.sourceResourceId,
|
|
assetKind: layer.assetKind,
|
|
generationInputs: layer.generationInputs,
|
|
};
|
|
}
|
|
|
|
function createEditorProjectSessionCacheSnapshot({
|
|
projectId,
|
|
title,
|
|
viewport,
|
|
layoutItems,
|
|
layers,
|
|
}: {
|
|
projectId: string;
|
|
title: string;
|
|
viewport: EditorProjectSnapshot['viewport'];
|
|
layoutItems: EditorProjectSnapshot['layers'];
|
|
layers: CanvasLayer[];
|
|
}): EditorProjectSnapshot | null {
|
|
const resources = layers.map((layer) =>
|
|
createEditorProjectSessionCacheResource({ projectId, layer }),
|
|
);
|
|
if (resources.some((resource) => !resource)) {
|
|
return null;
|
|
}
|
|
return {
|
|
projectId,
|
|
title: title.trim() || '未命名画布',
|
|
viewport,
|
|
layers: layoutItems,
|
|
resources: resources.filter(
|
|
(resource): resource is EditorProjectResourceSnapshot =>
|
|
Boolean(resource),
|
|
),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
export function useImageCanvasProjectPersistence({
|
|
refs,
|
|
setters,
|
|
layers,
|
|
canvasGenerationDialogs,
|
|
viewport,
|
|
canAccessProtectedData,
|
|
openEditorLoginModal,
|
|
}: {
|
|
refs: ImageCanvasProjectPersistenceRefs;
|
|
setters: ImageCanvasProjectPersistenceSetters;
|
|
layers: CanvasLayer[];
|
|
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
|
viewport: CanvasViewport;
|
|
canAccessProtectedData: boolean;
|
|
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
|
|
}) {
|
|
const projectIdRef = useRef<string | null>(null);
|
|
const pendingProjectResourceLayersRef = useRef<PendingProjectResourceLayer[]>(
|
|
[],
|
|
);
|
|
const pendingProjectLayoutSaveRef = useRef<PendingProjectLayoutSave | null>(
|
|
null,
|
|
);
|
|
const isProjectLayoutSaveRunningRef = useRef(false);
|
|
const skipNextProjectLayoutSaveRef = useRef(false);
|
|
const saveTimerRef = useRef<number | null>(null);
|
|
const projectTitleRef = useRef('未命名画布');
|
|
const coverSnapshotSignatureRef = useRef<string | null>(null);
|
|
const coverSnapshotUploadRequestRef = useRef(0);
|
|
const [projectId, setProjectId] = useState<string | null>(null);
|
|
const [isProjectReady, setIsProjectReady] = useState(false);
|
|
const {
|
|
setProjectTitle,
|
|
setProjectRenameValue,
|
|
setViewport,
|
|
setLayers,
|
|
selectSingleLayer,
|
|
setLayerCounter,
|
|
restoreCanvasGenerationDialogs,
|
|
} = setters;
|
|
|
|
const clearPendingProjectLayoutSave = useCallback(() => {
|
|
pendingProjectLayoutSaveRef.current = null;
|
|
if (saveTimerRef.current) {
|
|
window.clearTimeout(saveTimerRef.current);
|
|
saveTimerRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
const runPendingProjectLayoutSave = useCallback(
|
|
function runPendingProjectLayoutSave() {
|
|
if (isProjectLayoutSaveRunningRef.current) {
|
|
return;
|
|
}
|
|
const pendingSave = pendingProjectLayoutSaveRef.current;
|
|
if (!pendingSave) {
|
|
return;
|
|
}
|
|
|
|
pendingProjectLayoutSaveRef.current = null;
|
|
isProjectLayoutSaveRunningRef.current = true;
|
|
Promise.resolve(
|
|
saveEditorProjectLayout(pendingSave.projectId, pendingSave.input),
|
|
)
|
|
.catch((error: unknown) => {
|
|
if (isEditorAuthError(error)) {
|
|
openEditorLoginModal();
|
|
}
|
|
})
|
|
.finally(() => {
|
|
isProjectLayoutSaveRunningRef.current = false;
|
|
if (
|
|
pendingProjectLayoutSaveRef.current &&
|
|
saveTimerRef.current === null
|
|
) {
|
|
runPendingProjectLayoutSave();
|
|
}
|
|
});
|
|
},
|
|
[openEditorLoginModal],
|
|
);
|
|
|
|
const persistProjectCoverSnapshot = useCallback(
|
|
(
|
|
nextProjectId: string,
|
|
coverViewport: CanvasViewport,
|
|
coverLayers: readonly CanvasLayer[],
|
|
) => {
|
|
if (!canAccessProtectedData) {
|
|
return;
|
|
}
|
|
const signature = buildProjectCoverSnapshotSignature({
|
|
layers: coverLayers,
|
|
viewport: coverViewport,
|
|
});
|
|
if (!signature || coverSnapshotSignatureRef.current === signature) {
|
|
return;
|
|
}
|
|
coverSnapshotSignatureRef.current = signature;
|
|
coverSnapshotUploadRequestRef.current += 1;
|
|
const requestId = coverSnapshotUploadRequestRef.current;
|
|
|
|
void createProjectCoverSnapshotBlob({
|
|
layers: coverLayers,
|
|
viewport: coverViewport,
|
|
})
|
|
.then(async (blob) => {
|
|
if (!blob) {
|
|
if (coverSnapshotSignatureRef.current === signature) {
|
|
coverSnapshotSignatureRef.current = null;
|
|
}
|
|
return;
|
|
}
|
|
if (coverSnapshotUploadRequestRef.current !== requestId) {
|
|
return;
|
|
}
|
|
void putEditorProjectCoverCache({
|
|
projectId: nextProjectId,
|
|
blob,
|
|
});
|
|
const coverFile = new File([blob], `${nextProjectId}-cover.png`, {
|
|
type: 'image/png',
|
|
});
|
|
const uploadedCover = await uploadEditorMediaAssetFile(
|
|
coverFile,
|
|
'image',
|
|
{
|
|
assetKind: PROJECT_COVER_SNAPSHOT_OBJECT_ASSET_KIND,
|
|
pathSegments: [
|
|
'editor',
|
|
'project-covers',
|
|
nextProjectId,
|
|
`${Date.now()}`,
|
|
],
|
|
entityId: `editor-project-cover-${nextProjectId}`,
|
|
metadata: {
|
|
editor_project_id: nextProjectId,
|
|
},
|
|
},
|
|
);
|
|
if (coverSnapshotUploadRequestRef.current !== requestId) {
|
|
return;
|
|
}
|
|
await createEditorProjectResource(nextProjectId, {
|
|
imageSrc: uploadedCover.legacyPublicPath,
|
|
objectKey: uploadedCover.objectKey,
|
|
assetObjectId: uploadedCover.assetObjectId,
|
|
width: PROJECT_COVER_SNAPSHOT_SIZE.width,
|
|
height: PROJECT_COVER_SNAPSHOT_SIZE.height,
|
|
sourceType: 'uploaded',
|
|
assetKind: PROJECT_COVER_SNAPSHOT_ASSET_KIND,
|
|
});
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (coverSnapshotSignatureRef.current === signature) {
|
|
coverSnapshotSignatureRef.current = null;
|
|
}
|
|
if (isEditorAuthError(error)) {
|
|
openEditorLoginModal();
|
|
}
|
|
});
|
|
},
|
|
[canAccessProtectedData, openEditorLoginModal],
|
|
);
|
|
|
|
const queueProjectLayoutSave = useCallback(
|
|
(
|
|
nextProjectId: string,
|
|
input: Parameters<typeof saveEditorProjectLayout>[1],
|
|
options: { delayMs?: number } = {},
|
|
) => {
|
|
pendingProjectLayoutSaveRef.current = {
|
|
projectId: nextProjectId,
|
|
input,
|
|
};
|
|
const sessionSnapshot = createEditorProjectSessionCacheSnapshot({
|
|
projectId: nextProjectId,
|
|
title: projectTitleRef.current,
|
|
viewport: input.viewport,
|
|
layoutItems: input.layers,
|
|
layers: refs.layersRef.current,
|
|
});
|
|
if (sessionSnapshot) {
|
|
writeEditorProjectSessionCache(sessionSnapshot);
|
|
}
|
|
if (saveTimerRef.current) {
|
|
window.clearTimeout(saveTimerRef.current);
|
|
saveTimerRef.current = null;
|
|
}
|
|
|
|
const delayMs = options.delayMs ?? 0;
|
|
if (delayMs > 0) {
|
|
saveTimerRef.current = window.setTimeout(() => {
|
|
saveTimerRef.current = null;
|
|
persistProjectCoverSnapshot(
|
|
nextProjectId,
|
|
input.viewport,
|
|
refs.layersRef.current,
|
|
);
|
|
runPendingProjectLayoutSave();
|
|
}, delayMs);
|
|
return;
|
|
}
|
|
|
|
persistProjectCoverSnapshot(
|
|
nextProjectId,
|
|
input.viewport,
|
|
refs.layersRef.current,
|
|
);
|
|
runPendingProjectLayoutSave();
|
|
},
|
|
[persistProjectCoverSnapshot, refs.layersRef, runPendingProjectLayoutSave],
|
|
);
|
|
|
|
const createProjectResourceForLayer = useCallback(
|
|
(layer: CanvasLayer, options: ProjectResourceOptions = {}) => {
|
|
const readyProjectId = projectIdRef.current;
|
|
if (!readyProjectId) {
|
|
pendingProjectResourceLayersRef.current.push({ layer, options });
|
|
return;
|
|
}
|
|
const imageSrc = resolveProjectResourceCreateImageSrc(layer);
|
|
if (!imageSrc) {
|
|
return;
|
|
}
|
|
createEditorProjectResource(readyProjectId, {
|
|
imageSrc,
|
|
objectKey: layer.objectKey,
|
|
assetObjectId: layer.assetObjectId,
|
|
width: layer.originalWidth,
|
|
height: layer.originalHeight,
|
|
sourceType: layer.sourceType,
|
|
prompt: layer.prompt,
|
|
actualPrompt: layer.actualPrompt,
|
|
model: layer.model,
|
|
provider: layer.provider,
|
|
taskId: layer.taskId,
|
|
durationSeconds: layer.durationSeconds,
|
|
sourceResourceId: layer.sourceResourceId,
|
|
assetKind: layer.assetKind,
|
|
generationInputs: layer.generationInputs,
|
|
})
|
|
.then((resource) => {
|
|
options.onCreated?.(resource.resourceId);
|
|
const layerWithResourceId = {
|
|
...layer,
|
|
resourceId: resource.resourceId,
|
|
};
|
|
const currentLayers = refs.layersRef.current;
|
|
const nextLayers = currentLayers.some(
|
|
(currentLayer) => currentLayer.id === layer.id,
|
|
)
|
|
? currentLayers.map((currentLayer) =>
|
|
currentLayer.id === layer.id
|
|
? layerWithResourceId
|
|
: currentLayer,
|
|
)
|
|
: options.snapshotLayers?.some(
|
|
(snapshotLayer) => snapshotLayer.id === layer.id,
|
|
)
|
|
? options.snapshotLayers.map((snapshotLayer) =>
|
|
snapshotLayer.id === layer.id
|
|
? layerWithResourceId
|
|
: snapshotLayer,
|
|
)
|
|
: currentLayers;
|
|
refs.layersRef.current = nextLayers;
|
|
setLayers(nextLayers);
|
|
if (nextLayers.length) {
|
|
queueProjectLayoutSave(readyProjectId, {
|
|
viewport: viewportToCanvasDisplayViewport(
|
|
refs.viewportRef.current,
|
|
),
|
|
layers: serializeCanvasLayout({
|
|
layers: nextLayers,
|
|
canvasGenerationDialogs:
|
|
refs.canvasGenerationDialogsRef.current,
|
|
}),
|
|
});
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (isEditorAuthError(error)) {
|
|
openEditorLoginModal();
|
|
}
|
|
});
|
|
},
|
|
[openEditorLoginModal, queueProjectLayoutSave, refs, setLayers],
|
|
);
|
|
|
|
const applyProjectSnapshot = useCallback(
|
|
(project: EditorProjectSnapshot) => {
|
|
clearPendingProjectLayoutSave();
|
|
skipNextProjectLayoutSaveRef.current = true;
|
|
if (projectIdRef.current !== project.projectId) {
|
|
coverSnapshotSignatureRef.current = null;
|
|
coverSnapshotUploadRequestRef.current += 1;
|
|
}
|
|
projectIdRef.current = project.projectId;
|
|
setProjectId(project.projectId);
|
|
const nextProjectTitle = project.title?.trim() || '未命名画布';
|
|
projectTitleRef.current = nextProjectTitle;
|
|
setProjectTitle(nextProjectTitle);
|
|
setProjectRenameValue(nextProjectTitle);
|
|
const nextViewport = canvasDisplayViewportToViewport(project.viewport);
|
|
setViewport(nextViewport);
|
|
refs.viewportRef.current = nextViewport;
|
|
const resourcesById = new Map<string, CanvasLayerResourceMetadata>(
|
|
project.resources.map((resource) => [
|
|
resource.resourceId,
|
|
{
|
|
imageSrc: resource.imageSrc,
|
|
resourceId: resource.resourceId,
|
|
objectKey: resource.objectKey,
|
|
assetObjectId: resource.assetObjectId,
|
|
width: resource.width,
|
|
height: resource.height,
|
|
sourceType: resource.sourceType,
|
|
prompt: resource.prompt,
|
|
actualPrompt: resource.actualPrompt,
|
|
model: resource.model,
|
|
provider: resource.provider,
|
|
taskId: resource.taskId,
|
|
durationSeconds: resource.durationSeconds,
|
|
sourceResourceId: resource.sourceResourceId,
|
|
assetKind: resource.assetKind,
|
|
generationInputs: resource.generationInputs,
|
|
createdAt: resource.createdAt,
|
|
updatedAt: resource.updatedAt,
|
|
},
|
|
]),
|
|
);
|
|
const { layerItems, generationDialogs } = splitCanvasLayoutItems(
|
|
project.layers,
|
|
resourcesById,
|
|
);
|
|
const hydratedLayers = layerItems
|
|
.map((layer) => hydrateLayer(layer, resourcesById))
|
|
.filter((layer): layer is CanvasLayer => Boolean(layer));
|
|
setLayerCounter(hydratedLayers.length);
|
|
refs.layersRef.current = hydratedLayers;
|
|
setLayers(hydratedLayers);
|
|
selectSingleLayer(hydratedLayers[0]?.id ?? null);
|
|
refs.canvasGenerationDialogsRef.current = generationDialogs;
|
|
restoreCanvasGenerationDialogs(generationDialogs);
|
|
},
|
|
[
|
|
clearPendingProjectLayoutSave,
|
|
refs,
|
|
restoreCanvasGenerationDialogs,
|
|
selectSingleLayer,
|
|
setLayerCounter,
|
|
setLayers,
|
|
setProjectRenameValue,
|
|
setProjectTitle,
|
|
setViewport,
|
|
],
|
|
);
|
|
|
|
const appendCanvasLayersWithResources = useCallback(
|
|
(nextLayers: CanvasLayer[]) => {
|
|
if (!nextLayers.length) {
|
|
return;
|
|
}
|
|
const snapshotLayers = [...refs.layersRef.current, ...nextLayers];
|
|
refs.layersRef.current = snapshotLayers;
|
|
setLayers(snapshotLayers);
|
|
nextLayers.forEach((layer) =>
|
|
isLocalProjectResourceId(layer.resourceId)
|
|
? createProjectResourceForLayer(layer, { snapshotLayers })
|
|
: undefined,
|
|
);
|
|
},
|
|
[createProjectResourceForLayer, refs, setLayers],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!canAccessProtectedData) {
|
|
setIsProjectReady(false);
|
|
return undefined;
|
|
}
|
|
let cancelled = false;
|
|
const projectIdFromQuery =
|
|
typeof window === 'undefined'
|
|
? null
|
|
: new URLSearchParams(window.location.search)
|
|
.get('projectid')
|
|
?.trim() || null;
|
|
const cachedProject = readEditorProjectSessionCache(projectIdFromQuery);
|
|
if (cachedProject) {
|
|
applyProjectSnapshot(cachedProject);
|
|
}
|
|
const loadProject = projectIdFromQuery
|
|
? loadEditorProject(projectIdFromQuery)
|
|
: loadOrCreateRecentEditorProject();
|
|
|
|
loadProject
|
|
.then((project) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
applyProjectSnapshot(project);
|
|
writeEditorProjectSessionCache(project);
|
|
const pendingLayers = pendingProjectResourceLayersRef.current.splice(0);
|
|
pendingLayers.forEach(({ layer, options }) => {
|
|
createProjectResourceForLayer(layer, options);
|
|
});
|
|
setIsProjectReady(true);
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
setIsProjectReady(false);
|
|
if (isEditorAuthError(error)) {
|
|
openEditorLoginModal(() => {
|
|
window.location.reload();
|
|
});
|
|
return;
|
|
}
|
|
if (projectIdFromQuery && isEditorProjectAccessError(error)) {
|
|
removeEditorProjectSessionCache(projectIdFromQuery);
|
|
replaceAppHistoryPath('/project');
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [
|
|
canAccessProtectedData,
|
|
applyProjectSnapshot,
|
|
createProjectResourceForLayer,
|
|
openEditorLoginModal,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!projectId || !isProjectReady) {
|
|
return undefined;
|
|
}
|
|
if (skipNextProjectLayoutSaveRef.current) {
|
|
skipNextProjectLayoutSaveRef.current = false;
|
|
persistProjectCoverSnapshot(
|
|
projectId,
|
|
viewportToCanvasDisplayViewport(viewport),
|
|
layers,
|
|
);
|
|
return undefined;
|
|
}
|
|
queueProjectLayoutSave(
|
|
projectId,
|
|
{
|
|
viewport: viewportToCanvasDisplayViewport(viewport),
|
|
layers: serializeCanvasLayout({
|
|
layers,
|
|
canvasGenerationDialogs,
|
|
}),
|
|
},
|
|
{ delayMs: 450 },
|
|
);
|
|
|
|
return () => {
|
|
if (saveTimerRef.current) {
|
|
window.clearTimeout(saveTimerRef.current);
|
|
saveTimerRef.current = null;
|
|
}
|
|
};
|
|
}, [
|
|
isProjectReady,
|
|
canvasGenerationDialogs,
|
|
layers,
|
|
persistProjectCoverSnapshot,
|
|
projectId,
|
|
queueProjectLayoutSave,
|
|
refs,
|
|
viewport,
|
|
]);
|
|
|
|
return {
|
|
projectId,
|
|
isProjectReady,
|
|
projectIdRef,
|
|
createProjectResourceForLayer,
|
|
appendCanvasLayersWithResources,
|
|
applyProjectSnapshot,
|
|
};
|
|
}
|