946308b75e
将生成器对话框作为画布布局项序列化和恢复 生成成功后保留生成器快照并锚定到成品图层 图片类生成结果同步写入账号素材库 补充生成器持久化测试和浏览器回归相关文档
300 lines
8.7 KiB
TypeScript
300 lines
8.7 KiB
TypeScript
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,
|
|
serializeCanvasLayout,
|
|
splitCanvasLayoutItems,
|
|
} from './ImageCanvasEditorModel';
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasLayer,
|
|
CanvasViewport,
|
|
} from './ImageCanvasEditorTypes';
|
|
|
|
type ProjectResourceOptions = {
|
|
onCreated?: (resourceId: string) => void;
|
|
snapshotLayers?: CanvasLayer[];
|
|
};
|
|
|
|
type PendingProjectResourceLayer = {
|
|
layer: CanvasLayer;
|
|
options: ProjectResourceOptions;
|
|
};
|
|
|
|
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 || error.status === 403)
|
|
);
|
|
}
|
|
|
|
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 saveTimerRef = useRef<number | null>(null);
|
|
const [projectId, setProjectId] = useState<string | null>(null);
|
|
const [isProjectReady, setIsProjectReady] = useState(false);
|
|
const {
|
|
setProjectTitle,
|
|
setProjectRenameValue,
|
|
setViewport,
|
|
setLayers,
|
|
selectSingleLayer,
|
|
setLayerCounter,
|
|
restoreCanvasGenerationDialogs,
|
|
} = setters;
|
|
|
|
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;
|
|
setLayers(nextLayers);
|
|
if (nextLayers.length) {
|
|
void saveEditorProjectLayout(readyProjectId, {
|
|
viewport: refs.viewportRef.current,
|
|
layers: serializeCanvasLayout({
|
|
layers: nextLayers,
|
|
canvasGenerationDialogs:
|
|
refs.canvasGenerationDialogsRef.current,
|
|
}),
|
|
}).catch((error: unknown) => {
|
|
if (isEditorAuthError(error)) {
|
|
openEditorLoginModal();
|
|
}
|
|
});
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
if (isEditorAuthError(error)) {
|
|
openEditorLoginModal();
|
|
}
|
|
});
|
|
},
|
|
[openEditorLoginModal, refs, setLayers],
|
|
);
|
|
|
|
const appendCanvasLayersWithResources = useCallback(
|
|
(nextLayers: CanvasLayer[]) => {
|
|
if (!nextLayers.length) {
|
|
return;
|
|
}
|
|
const snapshotLayers = [...refs.layersRef.current, ...nextLayers];
|
|
refs.layersRef.current = snapshotLayers;
|
|
setLayers(snapshotLayers);
|
|
nextLayers.forEach((layer) =>
|
|
createProjectResourceForLayer(layer, { snapshotLayers }),
|
|
);
|
|
},
|
|
[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 loadProject = projectIdFromQuery
|
|
? loadEditorProject(projectIdFromQuery)
|
|
: loadOrCreateRecentEditorProject();
|
|
|
|
loadProject
|
|
.then((project) => {
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
projectIdRef.current = project.projectId;
|
|
setProjectId(project.projectId);
|
|
const nextProjectTitle = project.title?.trim() || '未命名画布';
|
|
setProjectTitle(nextProjectTitle);
|
|
setProjectRenameValue(nextProjectTitle);
|
|
const pendingLayers = pendingProjectResourceLayersRef.current.splice(0);
|
|
pendingLayers.forEach(({ layer, options }) => {
|
|
createProjectResourceForLayer(layer, options);
|
|
});
|
|
setViewport(project.viewport);
|
|
const resourcesById = new Map(
|
|
project.resources.map((resource) => [
|
|
resource.resourceId,
|
|
{ imageSrc: resource.imageSrc },
|
|
]),
|
|
);
|
|
const { layerItems, generationDialogs } = splitCanvasLayoutItems(
|
|
project.layers,
|
|
);
|
|
const hydratedLayers = layerItems
|
|
.map((layer) => hydrateLayer(layer, resourcesById))
|
|
.filter((layer): layer is CanvasLayer => Boolean(layer));
|
|
restoreCanvasGenerationDialogs(generationDialogs);
|
|
if (hydratedLayers.length > 0) {
|
|
setLayerCounter(hydratedLayers.length);
|
|
setLayers(hydratedLayers);
|
|
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,
|
|
restoreCanvasGenerationDialogs,
|
|
selectSingleLayer,
|
|
setLayerCounter,
|
|
setLayers,
|
|
setProjectRenameValue,
|
|
setProjectTitle,
|
|
setViewport,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!projectId || !isProjectReady) {
|
|
return undefined;
|
|
}
|
|
if (saveTimerRef.current) {
|
|
window.clearTimeout(saveTimerRef.current);
|
|
}
|
|
|
|
saveTimerRef.current = window.setTimeout(() => {
|
|
saveEditorProjectLayout(projectId, {
|
|
viewport,
|
|
layers: serializeCanvasLayout({
|
|
layers,
|
|
canvasGenerationDialogs,
|
|
}),
|
|
}).catch((error: unknown) => {
|
|
if (isEditorAuthError(error)) {
|
|
openEditorLoginModal();
|
|
}
|
|
});
|
|
}, 450);
|
|
|
|
return () => {
|
|
if (saveTimerRef.current) {
|
|
window.clearTimeout(saveTimerRef.current);
|
|
}
|
|
};
|
|
}, [
|
|
isProjectReady,
|
|
canvasGenerationDialogs,
|
|
layers,
|
|
openEditorLoginModal,
|
|
projectId,
|
|
refs,
|
|
viewport,
|
|
]);
|
|
|
|
return {
|
|
projectId,
|
|
isProjectReady,
|
|
projectIdRef,
|
|
createProjectResourceForLayer,
|
|
appendCanvasLayersWithResources,
|
|
};
|
|
}
|