import { type RefObject, useCallback, useEffect, useRef, useState } from 'react'; import { ApiClientError } from '../../services/apiClient'; import { createEditorProjectResource, loadEditorProject, loadOrCreateRecentEditorProject, saveEditorProjectLayout, } from '../../services/image-editor/editorProjectClient'; import { hydrateLayer, serializeLayer } from './ImageCanvasEditorModel'; import type { CanvasLayer, CanvasViewport } from './ImageCanvasEditorTypes'; type ProjectResourceOptions = { onCreated?: (resourceId: string) => void; snapshotLayers?: CanvasLayer[]; }; type PendingProjectResourceLayer = { layer: CanvasLayer; options: ProjectResourceOptions; }; type ImageCanvasProjectPersistenceRefs = { layersRef: RefObject; viewportRef: RefObject; }; 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; }; function isEditorAuthError(error: unknown) { return ( error instanceof ApiClientError && (error.status === 401 || error.status === 403) ); } export function useImageCanvasProjectPersistence({ refs, setters, layers, viewport, canAccessProtectedData, openEditorLoginModal, }: { refs: ImageCanvasProjectPersistenceRefs; setters: ImageCanvasProjectPersistenceSetters; layers: CanvasLayer[]; viewport: CanvasViewport; canAccessProtectedData: boolean; openEditorLoginModal: (postLoginAction?: (() => void) | null) => void; }) { const projectIdRef = useRef(null); const pendingProjectResourceLayersRef = useRef( [], ); const saveTimerRef = useRef(null); const [projectId, setProjectId] = useState(null); const [isProjectReady, setIsProjectReady] = useState(false); const createProjectResourceForLayer = useCallback( (layer: CanvasLayer, options: ProjectResourceOptions = {}) => { const readyProjectId = projectIdRef.current; if (!readyProjectId) { pendingProjectResourceLayersRef.current.push({ layer, options }); return; } createEditorProjectResource(readyProjectId, { imageSrc: layer.src, 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, sourceResourceId: layer.sourceResourceId, }) .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; setters.setLayers(nextLayers); if (nextLayers.length) { void saveEditorProjectLayout(readyProjectId, { viewport: refs.viewportRef.current, layers: nextLayers.map(serializeLayer), }).catch((error: unknown) => { if (isEditorAuthError(error)) { openEditorLoginModal(); } }); } }) .catch((error: unknown) => { if (isEditorAuthError(error)) { openEditorLoginModal(); } }); }, [openEditorLoginModal, refs, setters], ); const appendCanvasLayersWithResources = useCallback( (nextLayers: CanvasLayer[]) => { if (!nextLayers.length) { return; } const snapshotLayers = [...refs.layersRef.current, ...nextLayers]; refs.layersRef.current = snapshotLayers; setters.setLayers(snapshotLayers); nextLayers.forEach((layer) => createProjectResourceForLayer(layer, { snapshotLayers }), ); }, [createProjectResourceForLayer, refs, setters], ); 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 loadProject = projectIdFromQuery ? loadEditorProject(projectIdFromQuery) : loadOrCreateRecentEditorProject(); loadProject .then((project) => { if (cancelled) { return; } projectIdRef.current = project.projectId; setProjectId(project.projectId); const nextProjectTitle = project.title?.trim() || '未命名画布'; setters.setProjectTitle(nextProjectTitle); setters.setProjectRenameValue(nextProjectTitle); const pendingLayers = pendingProjectResourceLayersRef.current.splice(0); pendingLayers.forEach(({ layer, options }) => { createProjectResourceForLayer(layer, options); }); setters.setViewport(project.viewport); const resourcesById = new Map( project.resources.map((resource) => [ resource.resourceId, { imageSrc: resource.imageSrc }, ]), ); const hydratedLayers = project.layers .map((layer) => hydrateLayer(layer, resourcesById)) .filter((layer): layer is CanvasLayer => Boolean(layer)); if (hydratedLayers.length > 0) { setters.setLayerCounter(hydratedLayers.length); setters.setLayers(hydratedLayers); setters.selectSingleLayer(hydratedLayers[0]?.id ?? null); } setIsProjectReady(true); }) .catch((error: unknown) => { if (cancelled) { return; } setIsProjectReady(false); if (isEditorAuthError(error)) { openEditorLoginModal(() => { window.location.reload(); }); } }); return () => { cancelled = true; }; }, [ canAccessProtectedData, createProjectResourceForLayer, openEditorLoginModal, setters, ]); useEffect(() => { if (!projectId || !isProjectReady) { return undefined; } if (saveTimerRef.current) { window.clearTimeout(saveTimerRef.current); } saveTimerRef.current = window.setTimeout(() => { saveEditorProjectLayout(projectId, { viewport, layers: layers.map(serializeLayer), }).catch((error: unknown) => { if (isEditorAuthError(error)) { openEditorLoginModal(); } }); }, 450); return () => { if (saveTimerRef.current) { window.clearTimeout(saveTimerRef.current); } }; }, [isProjectReady, layers, openEditorLoginModal, projectId, viewport]); return { projectId, isProjectReady, projectIdRef, createProjectResourceForLayer, appendCanvasLayersWithResources, }; }