511 lines
15 KiB
TypeScript
511 lines
15 KiB
TypeScript
import {
|
|
type ChangeEvent,
|
|
type Dispatch,
|
|
type SetStateAction,
|
|
useCallback,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import { ApiClientError } from '../../services/apiClient';
|
|
import { createEditorAsset } from '../../services/image-editor/editorProjectClient';
|
|
import type {
|
|
CanvasLayer,
|
|
CanvasTool,
|
|
CanvasViewport,
|
|
EditorAsset,
|
|
EditorAssetFolder,
|
|
GenerateDialogState,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { isImageFile, readImageFileAsDataUrl } from './ImageCanvasFileModel';
|
|
import {
|
|
createUploadCanvasLayer,
|
|
createUploadingAssetPlaceholder,
|
|
resizeUploadCanvasLayerToImage,
|
|
resolveUploadFolderId,
|
|
} from './ImageCanvasUploadModel';
|
|
|
|
type UploadFilesOptions = {
|
|
folderId?: string;
|
|
canvasPoint?: { x: number; y: number };
|
|
addToCanvas?: boolean;
|
|
};
|
|
|
|
type UploadAssetFileOptions = UploadFilesOptions & {
|
|
uploadIndex: number;
|
|
};
|
|
|
|
type UseImageCanvasUploadWorkflowOptions = {
|
|
canAccessProtectedData: boolean;
|
|
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
|
|
assetFolders: EditorAssetFolder[];
|
|
activeUploadFolderId: string;
|
|
canvasSize: { width: number; height: number };
|
|
viewport: CanvasViewport;
|
|
activeTool: CanvasTool;
|
|
allocateUploadIndex: () => number;
|
|
setAssetFolders: Dispatch<SetStateAction<EditorAssetFolder[]>>;
|
|
setAssets: Dispatch<SetStateAction<EditorAsset[]>>;
|
|
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
|
|
selectSingleLayer: (layerId: string | null) => void;
|
|
};
|
|
|
|
function isEditorAuthError(error: unknown) {
|
|
return (
|
|
error instanceof ApiClientError &&
|
|
(error.status === 401 || error.status === 403)
|
|
);
|
|
}
|
|
|
|
function setFailedGenerationIdle(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
export function useImageCanvasUploadWorkflow({
|
|
canAccessProtectedData,
|
|
openEditorLoginModal,
|
|
assetFolders,
|
|
activeUploadFolderId,
|
|
canvasSize,
|
|
viewport,
|
|
activeTool,
|
|
allocateUploadIndex,
|
|
setAssetFolders,
|
|
setAssets,
|
|
setLayers,
|
|
setGenerateDialog,
|
|
appendCanvasLayersWithResources,
|
|
selectSingleLayer,
|
|
}: UseImageCanvasUploadWorkflowOptions) {
|
|
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
|
const canAccessProtectedDataRef = useRef(canAccessProtectedData);
|
|
const uploadTargetRef = useRef<UploadTarget>('asset');
|
|
const [uploadTarget, setUploadTargetState] = useState<UploadTarget>('asset');
|
|
|
|
canAccessProtectedDataRef.current = canAccessProtectedData;
|
|
|
|
const setUploadTarget: Dispatch<SetStateAction<UploadTarget>> = useCallback(
|
|
(nextTarget) => {
|
|
const resolvedTarget =
|
|
typeof nextTarget === 'function'
|
|
? nextTarget(uploadTargetRef.current)
|
|
: nextTarget;
|
|
uploadTargetRef.current = resolvedTarget;
|
|
setUploadTargetState(resolvedTarget);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const addCharacterSpecReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFile = Array.from(files).find(isImageFile);
|
|
if (!imageFile) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
|
|
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'character'
|
|
? {
|
|
...setFailedGenerationIdle(currentDialog),
|
|
characterSpecReference: {
|
|
id: `upload-character-spec-${Date.now()}`,
|
|
label: imageFile.name || '角色形象规范',
|
|
src: imageSrc,
|
|
},
|
|
}
|
|
: currentDialog,
|
|
);
|
|
},
|
|
[setGenerateDialog],
|
|
);
|
|
|
|
const addSpecReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFile = Array.from(files).find(isImageFile);
|
|
if (!imageFile) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
|
|
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'spec'
|
|
? {
|
|
...setFailedGenerationIdle(currentDialog),
|
|
specReference: {
|
|
id: `upload-spec-reference-${Date.now()}`,
|
|
label: imageFile.name || '参考图',
|
|
src: imageSrc,
|
|
},
|
|
}
|
|
: currentDialog,
|
|
);
|
|
},
|
|
[setGenerateDialog],
|
|
);
|
|
|
|
const addCharacterReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFiles = Array.from(files).filter(isImageFile);
|
|
if (!imageFiles.length) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
|
|
const references = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
id: `upload-character-reference-${Date.now()}-${index}`,
|
|
label: file.name || `参考图${index + 1}`,
|
|
src: await readImageFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'character'
|
|
? {
|
|
...setFailedGenerationIdle(currentDialog),
|
|
characterReferences: [
|
|
...(currentDialog.characterReferences ?? []),
|
|
...references,
|
|
],
|
|
}
|
|
: currentDialog,
|
|
);
|
|
},
|
|
[setGenerateDialog],
|
|
);
|
|
|
|
const addIconSpecReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFile = Array.from(files).find(isImageFile);
|
|
if (!imageFile) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
|
|
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'icon'
|
|
? {
|
|
...setFailedGenerationIdle(currentDialog),
|
|
iconSpecReference: {
|
|
id: `upload-icon-spec-${Date.now()}`,
|
|
label: imageFile.name || '图标素材规范',
|
|
src: imageSrc,
|
|
},
|
|
}
|
|
: currentDialog,
|
|
);
|
|
},
|
|
[setGenerateDialog],
|
|
);
|
|
|
|
const uploadAssetFile = useCallback(
|
|
async (file: File, options: UploadAssetFileOptions) => {
|
|
if (!isImageFile(file)) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
|
|
const uploadFolderId = resolveUploadFolderId({
|
|
assetFolders,
|
|
requestedFolderId: options.folderId,
|
|
activeUploadFolderId,
|
|
});
|
|
const uploadIndex = options.uploadIndex;
|
|
const uploadedAsset = createUploadingAssetPlaceholder({
|
|
uploadIndex,
|
|
fileName: file.name,
|
|
folderId: uploadFolderId,
|
|
});
|
|
setAssets((currentAssets) => [...currentAssets, uploadedAsset]);
|
|
setAssetFolders((currentFolders) =>
|
|
currentFolders.map((folder) =>
|
|
folder.id === uploadFolderId
|
|
? {
|
|
...folder,
|
|
collapsed: false,
|
|
}
|
|
: folder,
|
|
),
|
|
);
|
|
|
|
let imageSrc = '';
|
|
try {
|
|
imageSrc = await readImageFileAsDataUrl(file);
|
|
setAssets((currentAssets) =>
|
|
currentAssets.map((asset) =>
|
|
asset.id === uploadedAsset.id
|
|
? {
|
|
...asset,
|
|
src: imageSrc,
|
|
uploadProgress: 42,
|
|
uploadMessage: '读取图片',
|
|
}
|
|
: asset,
|
|
),
|
|
);
|
|
} catch {
|
|
setAssets((currentAssets) =>
|
|
currentAssets.map((asset) =>
|
|
asset.id === uploadedAsset.id
|
|
? {
|
|
...asset,
|
|
uploadStatus: 'failed',
|
|
uploadProgress: 100,
|
|
uploadMessage: '读取失败',
|
|
}
|
|
: asset,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const nextLayer = createUploadCanvasLayer({
|
|
uploadIndex,
|
|
fileName: file.name,
|
|
imageSrc,
|
|
canvasPoint: options.canvasPoint,
|
|
canvasSize,
|
|
viewport,
|
|
});
|
|
|
|
if (options.addToCanvas) {
|
|
appendCanvasLayersWithResources([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
}
|
|
|
|
setAssets((currentAssets) =>
|
|
currentAssets.map((asset) =>
|
|
asset.id === uploadedAsset.id
|
|
? {
|
|
...asset,
|
|
uploadProgress: 68,
|
|
uploadMessage: '上传中',
|
|
}
|
|
: asset,
|
|
),
|
|
);
|
|
createEditorAsset({
|
|
folderId: uploadFolderId,
|
|
label: uploadedAsset.label,
|
|
imageSrc,
|
|
width: uploadedAsset.width,
|
|
height: uploadedAsset.height,
|
|
sourceType: 'uploaded',
|
|
})
|
|
.then((asset) => {
|
|
setAssets((currentAssets) =>
|
|
currentAssets.map((currentAsset) =>
|
|
currentAsset.id === uploadedAsset.id
|
|
? {
|
|
...currentAsset,
|
|
id: asset.assetId,
|
|
folderId: asset.folderId,
|
|
label: asset.label,
|
|
src: asset.imageSrc,
|
|
width: asset.width,
|
|
height: asset.height,
|
|
objectKey: asset.objectKey ?? undefined,
|
|
assetObjectId: asset.assetObjectId ?? undefined,
|
|
persisted: true,
|
|
uploadStatus: undefined,
|
|
uploadProgress: undefined,
|
|
uploadMessage: undefined,
|
|
}
|
|
: currentAsset,
|
|
),
|
|
);
|
|
if (options.addToCanvas) {
|
|
setLayers((currentLayers) =>
|
|
currentLayers.map((currentLayer) =>
|
|
currentLayer.id === nextLayer.id
|
|
? {
|
|
...currentLayer,
|
|
sourceAssetId: asset.assetId,
|
|
objectKey: asset.objectKey ?? currentLayer.objectKey,
|
|
assetObjectId:
|
|
asset.assetObjectId ?? currentLayer.assetObjectId,
|
|
}
|
|
: currentLayer,
|
|
),
|
|
);
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
const isAuthError = isEditorAuthError(error);
|
|
if (isAuthError) {
|
|
openEditorLoginModal();
|
|
}
|
|
setAssets((currentAssets) =>
|
|
currentAssets.map((asset) =>
|
|
asset.id === uploadedAsset.id
|
|
? {
|
|
...asset,
|
|
uploadStatus: 'failed',
|
|
uploadProgress: 100,
|
|
uploadMessage: isAuthError ? '请先登录' : '上传失败',
|
|
}
|
|
: asset,
|
|
),
|
|
);
|
|
});
|
|
|
|
if (imageSrc) {
|
|
const uploadedImage = new Image();
|
|
uploadedImage.onload = () => {
|
|
const originalWidth =
|
|
uploadedImage.naturalWidth || uploadedAsset.width;
|
|
const originalHeight =
|
|
uploadedImage.naturalHeight || uploadedAsset.height;
|
|
if (options.addToCanvas) {
|
|
setLayers((currentLayers) =>
|
|
currentLayers.map((layer) =>
|
|
layer.id === nextLayer.id
|
|
? resizeUploadCanvasLayerToImage({
|
|
layer,
|
|
originalWidth,
|
|
originalHeight,
|
|
canvasPoint: options.canvasPoint,
|
|
canvasSize,
|
|
viewport,
|
|
})
|
|
: layer,
|
|
),
|
|
);
|
|
}
|
|
setAssets((currentAssets) =>
|
|
currentAssets.map((asset) =>
|
|
asset.id === uploadedAsset.id
|
|
? {
|
|
...asset,
|
|
width: originalWidth,
|
|
height: originalHeight,
|
|
}
|
|
: asset,
|
|
),
|
|
);
|
|
};
|
|
uploadedImage.src = imageSrc;
|
|
}
|
|
},
|
|
[
|
|
activeUploadFolderId,
|
|
appendCanvasLayersWithResources,
|
|
assetFolders,
|
|
canvasSize.height,
|
|
canvasSize.width,
|
|
openEditorLoginModal,
|
|
selectSingleLayer,
|
|
setAssetFolders,
|
|
setAssets,
|
|
setLayers,
|
|
viewport.scale,
|
|
viewport.x,
|
|
viewport.y,
|
|
],
|
|
);
|
|
|
|
const addUploadedFiles = useCallback(
|
|
(files: FileList | File[], options: UploadFilesOptions = {}) => {
|
|
const imageFiles = Array.from(files);
|
|
if (!canAccessProtectedDataRef.current) {
|
|
openEditorLoginModal(() => {
|
|
addUploadedFiles(imageFiles, options);
|
|
});
|
|
return;
|
|
}
|
|
imageFiles.forEach((file, index) => {
|
|
const uploadIndex = allocateUploadIndex();
|
|
void uploadAssetFile(file, {
|
|
...options,
|
|
addToCanvas: options.addToCanvas ?? false,
|
|
uploadIndex,
|
|
canvasPoint: options.canvasPoint
|
|
? {
|
|
x: options.canvasPoint.x + index * 28,
|
|
y: options.canvasPoint.y + index * 28,
|
|
}
|
|
: undefined,
|
|
});
|
|
});
|
|
},
|
|
[
|
|
allocateUploadIndex,
|
|
openEditorLoginModal,
|
|
uploadAssetFile,
|
|
],
|
|
);
|
|
|
|
const openUploadPicker = useCallback(
|
|
(target: UploadTarget) => {
|
|
setUploadTarget(target);
|
|
uploadInputRef.current?.click();
|
|
},
|
|
[setUploadTarget],
|
|
);
|
|
|
|
const requestUpload = useCallback(
|
|
(target: UploadTarget = 'asset') => {
|
|
if (target === 'asset' && !canAccessProtectedDataRef.current) {
|
|
openEditorLoginModal();
|
|
return;
|
|
}
|
|
openUploadPicker(target);
|
|
},
|
|
[openEditorLoginModal, openUploadPicker],
|
|
);
|
|
|
|
const handleUploadInputChange = useCallback(
|
|
(event: ChangeEvent<HTMLInputElement>) => {
|
|
const files = event.currentTarget.files;
|
|
const currentUploadTarget = uploadTargetRef.current;
|
|
if (files?.length) {
|
|
if (currentUploadTarget === 'spec-reference') {
|
|
void addSpecReferenceFiles(files);
|
|
} else if (currentUploadTarget === 'character-spec') {
|
|
void addCharacterSpecReferenceFiles(files);
|
|
} else if (currentUploadTarget === 'character-reference') {
|
|
void addCharacterReferenceFiles(files);
|
|
} else if (currentUploadTarget === 'icon-spec') {
|
|
void addIconSpecReferenceFiles(files);
|
|
} else {
|
|
addUploadedFiles(files, { addToCanvas: activeTool === 'upload' });
|
|
}
|
|
}
|
|
setUploadTarget('asset');
|
|
event.currentTarget.value = '';
|
|
},
|
|
[
|
|
activeTool,
|
|
addCharacterReferenceFiles,
|
|
addCharacterSpecReferenceFiles,
|
|
addSpecReferenceFiles,
|
|
addIconSpecReferenceFiles,
|
|
addUploadedFiles,
|
|
setUploadTarget,
|
|
],
|
|
);
|
|
|
|
return {
|
|
uploadInputRef,
|
|
uploadTarget,
|
|
setUploadTarget,
|
|
requestUpload,
|
|
handleUploadInputChange,
|
|
addUploadedFiles,
|
|
addSpecReferenceFiles,
|
|
addCharacterSpecReferenceFiles,
|
|
addCharacterReferenceFiles,
|
|
addIconSpecReferenceFiles,
|
|
};
|
|
}
|