e4e3cab559
主站规范生成支持多张参考图 修复普通生成参考图删除与提交状态不同步 优化触屏设备删除按钮可达性并补充回归测试 --------- Co-authored-by: kdletters <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/164 Co-authored-by: 孟豪 <mh18530625731@163.com> Co-committed-by: 孟豪 <mh18530625731@163.com>
1641 lines
49 KiB
TypeScript
1641 lines
49 KiB
TypeScript
import {
|
|
type ChangeEvent,
|
|
type Dispatch,
|
|
type SetStateAction,
|
|
useCallback,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
|
|
import { ApiClientError } from '../../services/apiClient';
|
|
import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient';
|
|
import {
|
|
createEditorAsset,
|
|
createEditorAssetFolder,
|
|
createEditorProjectResource,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import { uploadEditorSeedanceReferenceFile } from '../../services/image-editor/editorReferenceUploadClient';
|
|
import {
|
|
EDITOR_UPLOAD_ASSET_FOLDER_LABEL,
|
|
resolvePreferredAssetUploadFolder,
|
|
resolveUploadAssetFolder,
|
|
} from './ImageCanvasAssetLibraryModel';
|
|
import type {
|
|
CanvasAssetKind,
|
|
CanvasHistoryAction,
|
|
CanvasLayer,
|
|
CanvasMediaType,
|
|
CanvasTool,
|
|
CanvasViewport,
|
|
CharacterReferenceImage,
|
|
EditorAsset,
|
|
EditorAssetFolder,
|
|
GenerateDialogState,
|
|
QuickEditPanelState,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
isImageFile,
|
|
isSeedanceReferenceAudioFile,
|
|
isSeedanceReferenceImageFile,
|
|
isSeedanceReferenceVideoFile,
|
|
probeImageFileDimensions,
|
|
probeMediaDurationSeconds,
|
|
probeMediaFileMetadata,
|
|
readFileAsDataUrl,
|
|
readImageFileAsDataUrl,
|
|
resolveEditorAssetUploadMediaType,
|
|
SEEDANCE_REFERENCE_FILE_LIMITS,
|
|
} from './ImageCanvasFileModel';
|
|
import {
|
|
applyGenerationReferenceUpload,
|
|
applyMeasuredUploadAssetSize,
|
|
applyPersistedUploadAsset,
|
|
applyQuickEditReferenceUpload,
|
|
applyUploadAssetCreateFailure,
|
|
applyUploadAssetCreatePending,
|
|
applyUploadAssetReadFailure,
|
|
applyUploadAssetReadSuccess,
|
|
bindUploadLayerToPersistedAsset,
|
|
buildUploadedMediaGenerationInputs,
|
|
createUploadCanvasLayer,
|
|
createUploadedGenerationReference,
|
|
createUploadingAssetPlaceholder,
|
|
expandUploadFolder,
|
|
resizeUploadCanvasLayerToImage,
|
|
resolveUploadFolderId,
|
|
} from './ImageCanvasUploadModel';
|
|
import { resolveUploadedAssetKind } from './ImageCanvasUploadUtils';
|
|
|
|
type UploadFilesOptions = {
|
|
folderId?: string;
|
|
canvasPoint?: { x: number; y: number };
|
|
addToCanvas?: boolean;
|
|
};
|
|
|
|
type UploadRequestOptions = {
|
|
addToCanvas?: boolean;
|
|
folderId?: string;
|
|
};
|
|
|
|
type UploadAssetFileOptions = UploadFilesOptions & {
|
|
uploadIndex: number;
|
|
};
|
|
|
|
class ImageReferenceCapacityChangedError extends Error {}
|
|
|
|
export type ImageReferenceUploadCapacity = {
|
|
label: string;
|
|
currentCount: number;
|
|
limit: number;
|
|
contextId?: string;
|
|
};
|
|
|
|
type ImageReferenceUploadReservation = {
|
|
target: UploadTarget;
|
|
count: number;
|
|
contextId?: string;
|
|
key: string;
|
|
release: () => void;
|
|
contextWarningShown: boolean;
|
|
};
|
|
|
|
type UseImageCanvasUploadWorkflowOptions = {
|
|
canAccessProtectedData: boolean;
|
|
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
|
|
projectId?: string | null;
|
|
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>>;
|
|
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
|
|
resolveImageReferenceUploadCapacity: (
|
|
target: UploadTarget,
|
|
) => ImageReferenceUploadCapacity | null;
|
|
appendUiAssetExtractionReferences: (
|
|
references: CharacterReferenceImage[],
|
|
) => void;
|
|
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
|
|
captureCanvasHistory: (action: CanvasHistoryAction) => void;
|
|
selectSingleLayer: (layerId: string | null) => void;
|
|
refreshAssetLibrary?: () => Promise<unknown> | void;
|
|
};
|
|
|
|
function isEditorAuthError(error: unknown) {
|
|
return (
|
|
error instanceof ApiClientError &&
|
|
(error.status === 401 || error.status === 403)
|
|
);
|
|
}
|
|
|
|
function resolveReferenceFallbackSize(mediaType: CanvasMediaType) {
|
|
if (mediaType === 'video') {
|
|
return { width: 560, height: 315 };
|
|
}
|
|
if (mediaType === 'audio') {
|
|
return { width: 420, height: 120 };
|
|
}
|
|
return { width: 420, height: 315 };
|
|
}
|
|
|
|
export function useImageCanvasUploadWorkflow({
|
|
canAccessProtectedData,
|
|
openEditorLoginModal,
|
|
projectId,
|
|
assetFolders,
|
|
activeUploadFolderId,
|
|
canvasSize,
|
|
viewport,
|
|
activeTool,
|
|
allocateUploadIndex,
|
|
setAssetFolders,
|
|
setAssets,
|
|
setLayers,
|
|
setGenerateDialog,
|
|
setQuickEditPanel,
|
|
resolveImageReferenceUploadCapacity,
|
|
appendUiAssetExtractionReferences,
|
|
appendCanvasLayersWithResources,
|
|
captureCanvasHistory,
|
|
selectSingleLayer,
|
|
refreshAssetLibrary,
|
|
}: UseImageCanvasUploadWorkflowOptions) {
|
|
const uploadInputRef = useRef<HTMLInputElement | null>(null);
|
|
const canAccessProtectedDataRef = useRef(canAccessProtectedData);
|
|
const uploadTargetRef = useRef<UploadTarget>('asset');
|
|
const uploadRequestOptionsRef = useRef<UploadRequestOptions>({});
|
|
const pendingImageReferenceUploadsRef = useRef<Record<string, number>>({});
|
|
const uploadAssetFolderCreatePromiseRef = useRef<Promise<string> | null>(
|
|
null,
|
|
);
|
|
const [uploadTarget, setUploadTargetState] = useState<UploadTarget>('asset');
|
|
const [
|
|
pendingImageReferenceUploadCount,
|
|
setPendingImageReferenceUploadCount,
|
|
] = useState(0);
|
|
|
|
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 showImageReferenceCapacityWarning = useCallback(
|
|
(capacity: ImageReferenceUploadCapacity, remaining: number) => {
|
|
window.alert(
|
|
`${capacity.label}最多允许 ${capacity.limit} 张,当前已有 ${capacity.currentCount} 张,本次最多还能选择 ${Math.max(0, remaining)} 张`,
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const reserveImageReferenceUpload = useCallback(
|
|
(target: UploadTarget, count: number) => {
|
|
const capacity = resolveImageReferenceUploadCapacity(target);
|
|
const key = `${target}\u0000${capacity?.contextId ?? ''}`;
|
|
const pending = pendingImageReferenceUploadsRef.current[key] ?? 0;
|
|
if (capacity) {
|
|
const remaining = capacity.limit - capacity.currentCount - pending;
|
|
if (count > remaining) {
|
|
showImageReferenceCapacityWarning(capacity, remaining);
|
|
return null;
|
|
}
|
|
}
|
|
pendingImageReferenceUploadsRef.current[key] = pending + count;
|
|
setPendingImageReferenceUploadCount((current) => current + count);
|
|
let released = false;
|
|
const reservation: ImageReferenceUploadReservation = {
|
|
target,
|
|
count,
|
|
contextId: capacity?.contextId,
|
|
key,
|
|
contextWarningShown: false,
|
|
release: () => {
|
|
if (released) {
|
|
return;
|
|
}
|
|
released = true;
|
|
const current = pendingImageReferenceUploadsRef.current[key] ?? 0;
|
|
pendingImageReferenceUploadsRef.current[key] = Math.max(
|
|
0,
|
|
current - count,
|
|
);
|
|
setPendingImageReferenceUploadCount((currentCount) =>
|
|
Math.max(0, currentCount - count),
|
|
);
|
|
},
|
|
};
|
|
return reservation;
|
|
},
|
|
[resolveImageReferenceUploadCapacity, showImageReferenceCapacityWarning],
|
|
);
|
|
|
|
const validateImageReferenceUploadReservation = useCallback(
|
|
(reservation: ImageReferenceUploadReservation) => {
|
|
const capacity = resolveImageReferenceUploadCapacity(reservation.target);
|
|
if (!capacity) {
|
|
if (reservation.contextId === undefined) {
|
|
return true;
|
|
}
|
|
if (!reservation.contextWarningShown) {
|
|
reservation.contextWarningShown = true;
|
|
window.alert(
|
|
'参考图上传目标已关闭或切换,本次结果不会添加到其他面板',
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
if (capacity.contextId !== reservation.contextId) {
|
|
if (!reservation.contextWarningShown) {
|
|
reservation.contextWarningShown = true;
|
|
window.alert(
|
|
'参考图上传目标已关闭或切换,本次结果不会添加到其他面板',
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
const pending =
|
|
pendingImageReferenceUploadsRef.current[reservation.key] ?? 0;
|
|
const remaining = capacity.limit - capacity.currentCount;
|
|
if (pending <= remaining) {
|
|
return true;
|
|
}
|
|
showImageReferenceCapacityWarning(capacity, remaining);
|
|
return false;
|
|
},
|
|
[resolveImageReferenceUploadCapacity, showImageReferenceCapacityWarning],
|
|
);
|
|
|
|
const createPersistedReference = useCallback(
|
|
async ({
|
|
idPrefix,
|
|
index,
|
|
file,
|
|
fallbackLabel,
|
|
imageSrc,
|
|
mediaType = 'image',
|
|
mimeType,
|
|
sizeBytes,
|
|
durationSeconds,
|
|
objectKey,
|
|
assetObjectId,
|
|
reservation,
|
|
authoritativeAssetKind,
|
|
}: {
|
|
idPrefix: string;
|
|
index?: number;
|
|
file: File;
|
|
fallbackLabel: string;
|
|
imageSrc: string;
|
|
mediaType?: CanvasMediaType;
|
|
mimeType?: string;
|
|
sizeBytes?: number;
|
|
durationSeconds?: number;
|
|
objectKey?: string | null;
|
|
assetObjectId?: string | null;
|
|
reservation?: ImageReferenceUploadReservation;
|
|
authoritativeAssetKind?: CanvasAssetKind;
|
|
}): Promise<CharacterReferenceImage> => {
|
|
const resolvedMediaType = mediaType ?? 'image';
|
|
let persistedImageSrc = imageSrc;
|
|
let persistedObjectKey = objectKey;
|
|
let persistedAssetObjectId = assetObjectId;
|
|
if (resolvedMediaType === 'image' && !persistedObjectKey) {
|
|
const uploadedImage = await uploadEditorMediaAssetFile(file, 'image');
|
|
persistedImageSrc = uploadedImage.legacyPublicPath;
|
|
persistedObjectKey = uploadedImage.objectKey;
|
|
persistedAssetObjectId = uploadedImage.assetObjectId;
|
|
}
|
|
const fallbackSize = resolveReferenceFallbackSize(resolvedMediaType);
|
|
const imageDimensions =
|
|
resolvedMediaType === 'image'
|
|
? await probeImageFileDimensions(file).catch(() => null)
|
|
: null;
|
|
const width = imageDimensions?.width ?? fallbackSize.width;
|
|
const height = imageDimensions?.height ?? fallbackSize.height;
|
|
if (
|
|
reservation &&
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
throw new ImageReferenceCapacityChangedError();
|
|
}
|
|
const assetKind =
|
|
authoritativeAssetKind ?? resolveUploadedAssetKind(resolvedMediaType);
|
|
const generationInputs = buildUploadedMediaGenerationInputs(
|
|
resolvedMediaType,
|
|
durationSeconds,
|
|
);
|
|
if (projectId) {
|
|
const resource = await createEditorProjectResource(projectId, {
|
|
imageSrc: persistedImageSrc,
|
|
...(persistedObjectKey ? { objectKey: persistedObjectKey } : {}),
|
|
...(persistedAssetObjectId
|
|
? { assetObjectId: persistedAssetObjectId }
|
|
: {}),
|
|
width,
|
|
height,
|
|
sourceType: 'uploaded',
|
|
assetKind,
|
|
generationInputs,
|
|
});
|
|
return createUploadedGenerationReference({
|
|
idPrefix,
|
|
index,
|
|
fileName: file.name,
|
|
fallbackLabel,
|
|
imageSrc: persistedImageSrc,
|
|
mediaType: resolvedMediaType,
|
|
mimeType,
|
|
sizeBytes,
|
|
durationSeconds,
|
|
objectKey: persistedObjectKey,
|
|
assetObjectId: persistedAssetObjectId,
|
|
resourceId: resource.resourceId,
|
|
});
|
|
}
|
|
|
|
const uploadFolderId = resolveUploadFolderId({
|
|
assetFolders,
|
|
activeUploadFolderId,
|
|
});
|
|
const asset = await createEditorAsset({
|
|
folderId: uploadFolderId,
|
|
label: file.name || fallbackLabel,
|
|
imageSrc: persistedImageSrc,
|
|
...(persistedObjectKey ? { objectKey: persistedObjectKey } : {}),
|
|
...(persistedAssetObjectId
|
|
? { assetObjectId: persistedAssetObjectId }
|
|
: {}),
|
|
width,
|
|
height,
|
|
sourceType: 'uploaded',
|
|
assetKind,
|
|
generationInputs,
|
|
});
|
|
return createUploadedGenerationReference({
|
|
idPrefix,
|
|
index,
|
|
fileName: file.name,
|
|
fallbackLabel,
|
|
imageSrc: persistedImageSrc,
|
|
mediaType: resolvedMediaType,
|
|
mimeType,
|
|
sizeBytes,
|
|
durationSeconds,
|
|
objectKey: persistedObjectKey,
|
|
assetObjectId: persistedAssetObjectId,
|
|
sourceAssetId: asset.assetId,
|
|
});
|
|
},
|
|
[
|
|
activeUploadFolderId,
|
|
assetFolders,
|
|
projectId,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const persistReferenceUploads = useCallback(
|
|
async (
|
|
inputs: Array<{
|
|
idPrefix: string;
|
|
index?: number;
|
|
file: File;
|
|
fallbackLabel: string;
|
|
imageSrc: string;
|
|
mediaType?: CanvasMediaType;
|
|
mimeType?: string;
|
|
sizeBytes?: number;
|
|
durationSeconds?: number;
|
|
objectKey?: string | null;
|
|
assetObjectId?: string | null;
|
|
authoritativeAssetKind?: CanvasAssetKind;
|
|
}>,
|
|
reservation?: ImageReferenceUploadReservation,
|
|
) => {
|
|
const results = await Promise.allSettled(
|
|
inputs.map((input) =>
|
|
createPersistedReference({ ...input, reservation }),
|
|
),
|
|
);
|
|
const references = results.flatMap((result) =>
|
|
result.status === 'fulfilled' ? [result.value] : [],
|
|
);
|
|
const errors = results.flatMap((result) =>
|
|
result.status === 'rejected' ? [result.reason] : [],
|
|
);
|
|
if (references.length > 0) {
|
|
void Promise.resolve(refreshAssetLibrary?.()).catch(() => undefined);
|
|
}
|
|
if (errors.length === 0) {
|
|
return references;
|
|
}
|
|
const authError = errors.find(isEditorAuthError);
|
|
if (authError) {
|
|
openEditorLoginModal();
|
|
window.alert(
|
|
references.length > 0
|
|
? `部分参考图保存失败,已保留成功的 ${references.length} 张;请登录后重试其余图片`
|
|
: '请先登录后再上传参考图',
|
|
);
|
|
} else if (
|
|
!errors.every(
|
|
(error) => error instanceof ImageReferenceCapacityChangedError,
|
|
)
|
|
) {
|
|
window.alert(
|
|
references.length > 0
|
|
? `部分参考图保存失败,已保留成功的 ${references.length} 张`
|
|
: '参考图保存失败,请稍后重试',
|
|
);
|
|
}
|
|
return references;
|
|
},
|
|
[createPersistedReference, openEditorLoginModal, refreshAssetLibrary],
|
|
);
|
|
|
|
const addCharacterSpecReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFile = Array.from(files).find(isImageFile);
|
|
if (!imageFile) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload('character-spec', 1);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const references = await persistReferenceUploads(
|
|
[
|
|
{
|
|
idPrefix: 'upload-character-spec',
|
|
file: imageFile,
|
|
fallbackLabel: '角色规范',
|
|
imageSrc,
|
|
},
|
|
],
|
|
reservation,
|
|
);
|
|
if (
|
|
references.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'character-spec',
|
|
references,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setGenerateDialog,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addSpecReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFile = Array.from(files).find(isImageFile);
|
|
if (!imageFile) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
|
|
const reservation = reserveImageReferenceUpload(
|
|
'spec-reference',
|
|
Array.from(files).filter(isImageFile).length,
|
|
);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const imageFiles = Array.from(files).filter(isImageFile);
|
|
const fileReferences = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
file,
|
|
index,
|
|
imageSrc: await readImageFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const references = await persistReferenceUploads(
|
|
fileReferences.map(({ file, imageSrc, index }) => ({
|
|
idPrefix: 'upload-spec-reference',
|
|
index,
|
|
file,
|
|
fallbackLabel: `参考图${index + 1}`,
|
|
imageSrc,
|
|
})),
|
|
reservation,
|
|
);
|
|
if (
|
|
references.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'spec-reference',
|
|
references,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setGenerateDialog,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addCharacterReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFiles = Array.from(files).filter(isImageFile);
|
|
if (!imageFiles.length) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload(
|
|
'character-reference',
|
|
imageFiles.length,
|
|
);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const references = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
file,
|
|
index,
|
|
imageSrc: await readImageFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const persistedReferences = await persistReferenceUploads(
|
|
references.map(({ file, imageSrc, index }) => ({
|
|
idPrefix: 'upload-character-reference',
|
|
index,
|
|
file,
|
|
fallbackLabel: `参考图${index + 1}`,
|
|
imageSrc,
|
|
})),
|
|
reservation,
|
|
);
|
|
if (
|
|
persistedReferences.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'character-reference',
|
|
references: persistedReferences,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setGenerateDialog,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addGenerationReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFiles = Array.from(files).filter(isImageFile);
|
|
if (!imageFiles.length) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload(
|
|
'generation-reference',
|
|
imageFiles.length,
|
|
);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const references = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
file,
|
|
index,
|
|
imageSrc: await readImageFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const persistedReferences = await persistReferenceUploads(
|
|
references.map(({ file, imageSrc, index }) => ({
|
|
idPrefix: 'upload-generation-reference',
|
|
index,
|
|
file,
|
|
fallbackLabel: `参考图${index + 1}`,
|
|
imageSrc,
|
|
})),
|
|
reservation,
|
|
);
|
|
if (
|
|
persistedReferences.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'generation-reference',
|
|
references: persistedReferences,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setGenerateDialog,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addQuickEditReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFiles = Array.from(files).filter(isImageFile);
|
|
if (!imageFiles.length) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload(
|
|
'quick-edit-reference',
|
|
imageFiles.length,
|
|
);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const references = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
file,
|
|
index,
|
|
imageSrc: await readImageFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const persistedReferences = await persistReferenceUploads(
|
|
references.map(({ file, imageSrc, index }) => ({
|
|
idPrefix: 'upload-quick-edit-reference',
|
|
index,
|
|
file,
|
|
fallbackLabel: `参考图${index + 1}`,
|
|
imageSrc,
|
|
})),
|
|
reservation,
|
|
);
|
|
if (
|
|
persistedReferences.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setQuickEditPanel((currentPanel) =>
|
|
applyQuickEditReferenceUpload({
|
|
panel: currentPanel,
|
|
references: persistedReferences,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setQuickEditPanel,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addUiAssetExtractionReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFiles = Array.from(files).filter(isImageFile);
|
|
if (!imageFiles.length) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload(
|
|
'ui-asset-extraction-reference',
|
|
imageFiles.length,
|
|
);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const references = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
file,
|
|
index,
|
|
imageSrc: await readImageFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const persistedReferences = await persistReferenceUploads(
|
|
references.map(({ file, imageSrc, index }) => ({
|
|
idPrefix: 'upload-ui-asset-extraction-reference',
|
|
index,
|
|
file,
|
|
fallbackLabel: `UI素材参考图${index + 1}`,
|
|
imageSrc,
|
|
})),
|
|
reservation,
|
|
);
|
|
if (
|
|
persistedReferences.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
appendUiAssetExtractionReferences(persistedReferences);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
appendUiAssetExtractionReferences,
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addPublicationReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFiles = Array.from(files).filter(isImageFile);
|
|
if (!imageFiles.length) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload(
|
|
'publication-reference',
|
|
imageFiles.length,
|
|
);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const references = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
file,
|
|
index,
|
|
imageSrc: await readImageFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const persistedReferences = await persistReferenceUploads(
|
|
references.map(({ file, imageSrc, index }) => ({
|
|
idPrefix: 'upload-publication-reference',
|
|
index,
|
|
file,
|
|
fallbackLabel: `宣发参考图${index + 1}`,
|
|
imageSrc,
|
|
})),
|
|
reservation,
|
|
);
|
|
if (
|
|
persistedReferences.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'publication-reference',
|
|
references: persistedReferences,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setGenerateDialog,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addVideoReferenceImageFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFiles = Array.from(files).filter(isSeedanceReferenceImageFile);
|
|
if (!imageFiles.length) {
|
|
window.alert('请选择 Seedance 支持的图片文件');
|
|
return;
|
|
}
|
|
if (
|
|
imageFiles.some(
|
|
(file) => file.size >= SEEDANCE_REFERENCE_FILE_LIMITS.imageBytes,
|
|
)
|
|
) {
|
|
window.alert('参考图片单张必须小于 30MB');
|
|
return;
|
|
}
|
|
const references = await Promise.all(
|
|
imageFiles.map(async (file, index) => ({
|
|
file,
|
|
index,
|
|
src: await readFileAsDataUrl(file),
|
|
})),
|
|
);
|
|
const persistedReferences = await persistReferenceUploads(
|
|
references.map(({ file, src, index }) => ({
|
|
idPrefix: 'upload-video-reference-image',
|
|
index,
|
|
file,
|
|
fallbackLabel: `参考图${index + 1}`,
|
|
imageSrc: src,
|
|
mediaType: 'image',
|
|
mimeType: file.type || undefined,
|
|
sizeBytes: file.size,
|
|
})),
|
|
);
|
|
if (persistedReferences.length === 0) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'video-reference-image',
|
|
references: persistedReferences,
|
|
}),
|
|
);
|
|
},
|
|
[persistReferenceUploads, setGenerateDialog],
|
|
);
|
|
|
|
const addVideoReferenceVideoFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const videoFiles = Array.from(files).filter(isSeedanceReferenceVideoFile);
|
|
if (!videoFiles.length) {
|
|
window.alert('请选择 mp4 或 mov 视频文件');
|
|
return;
|
|
}
|
|
if (
|
|
videoFiles.some(
|
|
(file) => file.size > SEEDANCE_REFERENCE_FILE_LIMITS.videoBytes,
|
|
)
|
|
) {
|
|
window.alert('参考视频单个不能超过 50MB');
|
|
return;
|
|
}
|
|
const references = await Promise.all(
|
|
videoFiles.map(async (file, index) => {
|
|
const durationSeconds = await probeMediaDurationSeconds(
|
|
file,
|
|
'video',
|
|
);
|
|
return {
|
|
file,
|
|
index,
|
|
durationSeconds,
|
|
};
|
|
}),
|
|
);
|
|
if (
|
|
references.some(
|
|
({ durationSeconds }) =>
|
|
Number.isFinite(durationSeconds) &&
|
|
(durationSeconds <
|
|
SEEDANCE_REFERENCE_FILE_LIMITS.mediaMinDurationSeconds ||
|
|
durationSeconds >
|
|
SEEDANCE_REFERENCE_FILE_LIMITS.mediaMaxDurationSeconds),
|
|
)
|
|
) {
|
|
window.alert('参考视频时长必须为 2-15 秒');
|
|
return;
|
|
}
|
|
const totalDuration = references.reduce(
|
|
(sum, reference) =>
|
|
Number.isFinite(reference.durationSeconds)
|
|
? sum + reference.durationSeconds
|
|
: sum,
|
|
0,
|
|
);
|
|
if (
|
|
totalDuration > SEEDANCE_REFERENCE_FILE_LIMITS.mediaTotalDurationSeconds
|
|
) {
|
|
window.alert('参考视频总时长不能超过 15 秒');
|
|
return;
|
|
}
|
|
let uploadedReferences: Array<{
|
|
file: File;
|
|
index: number;
|
|
durationSeconds: number;
|
|
src: string;
|
|
objectKey: string;
|
|
assetObjectId: string;
|
|
}>;
|
|
try {
|
|
uploadedReferences = await Promise.all(
|
|
references.map(async ({ file, index, durationSeconds }) => {
|
|
const uploaded = await uploadEditorSeedanceReferenceFile(
|
|
file,
|
|
'video',
|
|
);
|
|
return {
|
|
file,
|
|
index,
|
|
durationSeconds,
|
|
src: uploaded.src,
|
|
objectKey: uploaded.objectKey,
|
|
assetObjectId: uploaded.assetObjectId,
|
|
};
|
|
}),
|
|
);
|
|
} catch {
|
|
window.alert('参考视频上传失败,请稍后重试');
|
|
return;
|
|
}
|
|
const persistedReferences = await persistReferenceUploads(
|
|
uploadedReferences.map(
|
|
({
|
|
file,
|
|
src,
|
|
index,
|
|
durationSeconds,
|
|
objectKey,
|
|
assetObjectId,
|
|
}) => ({
|
|
idPrefix: 'upload-video-reference-video',
|
|
index,
|
|
file,
|
|
fallbackLabel: `参考视频${index + 1}`,
|
|
imageSrc: src,
|
|
mediaType: 'video',
|
|
mimeType: file.type || undefined,
|
|
sizeBytes: file.size,
|
|
durationSeconds,
|
|
objectKey,
|
|
assetObjectId,
|
|
}),
|
|
),
|
|
);
|
|
if (persistedReferences.length === 0) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'video-reference-video',
|
|
references: persistedReferences,
|
|
}),
|
|
);
|
|
},
|
|
[persistReferenceUploads, setGenerateDialog],
|
|
);
|
|
|
|
const addVideoReferenceAudioFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const audioFiles = Array.from(files).filter(isSeedanceReferenceAudioFile);
|
|
if (!audioFiles.length) {
|
|
window.alert('请选择 wav 或 mp3 音频文件');
|
|
return;
|
|
}
|
|
if (
|
|
audioFiles.some(
|
|
(file) => file.size > SEEDANCE_REFERENCE_FILE_LIMITS.audioBytes,
|
|
)
|
|
) {
|
|
window.alert('参考音频单个不能超过 15MB');
|
|
return;
|
|
}
|
|
const references = await Promise.all(
|
|
audioFiles.map(async (file, index) => {
|
|
const durationSeconds = await probeMediaDurationSeconds(
|
|
file,
|
|
'audio',
|
|
);
|
|
return {
|
|
file,
|
|
index,
|
|
durationSeconds,
|
|
};
|
|
}),
|
|
);
|
|
if (
|
|
references.some(
|
|
({ durationSeconds }) =>
|
|
Number.isFinite(durationSeconds) &&
|
|
(durationSeconds <
|
|
SEEDANCE_REFERENCE_FILE_LIMITS.mediaMinDurationSeconds ||
|
|
durationSeconds >
|
|
SEEDANCE_REFERENCE_FILE_LIMITS.mediaMaxDurationSeconds),
|
|
)
|
|
) {
|
|
window.alert('参考音频时长必须为 2-15 秒');
|
|
return;
|
|
}
|
|
const totalDuration = references.reduce(
|
|
(sum, reference) =>
|
|
Number.isFinite(reference.durationSeconds)
|
|
? sum + reference.durationSeconds
|
|
: sum,
|
|
0,
|
|
);
|
|
if (
|
|
totalDuration > SEEDANCE_REFERENCE_FILE_LIMITS.mediaTotalDurationSeconds
|
|
) {
|
|
window.alert('参考音频总时长不能超过 15 秒');
|
|
return;
|
|
}
|
|
let uploadedReferences: Array<{
|
|
file: File;
|
|
index: number;
|
|
durationSeconds: number;
|
|
src: string;
|
|
objectKey: string;
|
|
assetObjectId: string;
|
|
}>;
|
|
try {
|
|
uploadedReferences = await Promise.all(
|
|
references.map(async ({ file, index, durationSeconds }) => {
|
|
const uploaded = await uploadEditorSeedanceReferenceFile(
|
|
file,
|
|
'audio',
|
|
);
|
|
return {
|
|
file,
|
|
index,
|
|
durationSeconds,
|
|
src: uploaded.src,
|
|
objectKey: uploaded.objectKey,
|
|
assetObjectId: uploaded.assetObjectId,
|
|
};
|
|
}),
|
|
);
|
|
} catch {
|
|
window.alert('参考音频上传失败,请稍后重试');
|
|
return;
|
|
}
|
|
const persistedReferences = await persistReferenceUploads(
|
|
uploadedReferences.map(
|
|
({
|
|
file,
|
|
src,
|
|
index,
|
|
durationSeconds,
|
|
objectKey,
|
|
assetObjectId,
|
|
}) => ({
|
|
idPrefix: 'upload-video-reference-audio',
|
|
index,
|
|
file,
|
|
fallbackLabel: `参考音频${index + 1}`,
|
|
imageSrc: src,
|
|
mediaType: 'audio',
|
|
mimeType: file.type || undefined,
|
|
sizeBytes: file.size,
|
|
durationSeconds,
|
|
objectKey,
|
|
assetObjectId,
|
|
}),
|
|
),
|
|
);
|
|
if (persistedReferences.length === 0) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'video-reference-audio',
|
|
references: persistedReferences,
|
|
}),
|
|
);
|
|
},
|
|
[persistReferenceUploads, setGenerateDialog],
|
|
);
|
|
|
|
const addIconSpecReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFile = Array.from(files).find(isImageFile);
|
|
if (!imageFile) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload('icon-spec', 1);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const references = await persistReferenceUploads(
|
|
[
|
|
{
|
|
idPrefix: 'upload-icon-spec',
|
|
file: imageFile,
|
|
fallbackLabel: '图标规范',
|
|
imageSrc,
|
|
authoritativeAssetKind: 'icon-spec',
|
|
},
|
|
],
|
|
reservation,
|
|
);
|
|
if (
|
|
references.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'icon-spec',
|
|
references,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setGenerateDialog,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const addUiDesignSpecReferenceFiles = useCallback(
|
|
async (files: FileList | File[]) => {
|
|
const imageFile = Array.from(files).find(isImageFile);
|
|
if (!imageFile) {
|
|
window.alert('请选择图片文件');
|
|
return;
|
|
}
|
|
const reservation = reserveImageReferenceUpload('ui-design-icon-spec', 1);
|
|
if (!reservation) {
|
|
return;
|
|
}
|
|
try {
|
|
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
|
if (!validateImageReferenceUploadReservation(reservation)) {
|
|
return;
|
|
}
|
|
const references = await persistReferenceUploads(
|
|
[
|
|
{
|
|
idPrefix: 'upload-ui-design-icon-spec',
|
|
file: imageFile,
|
|
fallbackLabel: 'UI设计图标规范',
|
|
imageSrc,
|
|
},
|
|
],
|
|
reservation,
|
|
);
|
|
if (
|
|
references.length === 0 ||
|
|
!validateImageReferenceUploadReservation(reservation)
|
|
) {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
applyGenerationReferenceUpload({
|
|
dialog: currentDialog,
|
|
target: 'ui-design-icon-spec',
|
|
references,
|
|
}),
|
|
);
|
|
} finally {
|
|
reservation.release();
|
|
}
|
|
},
|
|
[
|
|
persistReferenceUploads,
|
|
reserveImageReferenceUpload,
|
|
setGenerateDialog,
|
|
validateImageReferenceUploadReservation,
|
|
],
|
|
);
|
|
|
|
const uploadAssetFile = useCallback(
|
|
async (file: File, options: UploadAssetFileOptions) => {
|
|
const mediaType = resolveEditorAssetUploadMediaType(file);
|
|
if (!mediaType) {
|
|
window.alert('请选择图片、MP3 或 MP4 文件');
|
|
return;
|
|
}
|
|
|
|
let uploadFolderId = resolveUploadFolderId({
|
|
assetFolders,
|
|
requestedFolderId: options.folderId,
|
|
activeUploadFolderId,
|
|
preferUploadFolder: !options.folderId,
|
|
});
|
|
if (!options.folderId && !resolveUploadAssetFolder(assetFolders)) {
|
|
if (!uploadAssetFolderCreatePromiseRef.current) {
|
|
uploadAssetFolderCreatePromiseRef.current = createEditorAssetFolder(
|
|
EDITOR_UPLOAD_ASSET_FOLDER_LABEL,
|
|
assetFolders.length + 100,
|
|
)
|
|
.then((folder) => {
|
|
setAssetFolders((currentFolders) =>
|
|
currentFolders.some(
|
|
(currentFolder) => currentFolder.id === folder.folderId,
|
|
)
|
|
? currentFolders
|
|
: [
|
|
...currentFolders,
|
|
{
|
|
id: folder.folderId,
|
|
label: folder.label,
|
|
collapsed: folder.collapsed,
|
|
systemDefault: folder.systemDefault,
|
|
persisted: true,
|
|
},
|
|
],
|
|
);
|
|
return folder.folderId;
|
|
})
|
|
.catch(() => {
|
|
const fallbackFolder =
|
|
resolvePreferredAssetUploadFolder(assetFolders);
|
|
return fallbackFolder?.id ?? 'project';
|
|
})
|
|
.finally(() => {
|
|
uploadAssetFolderCreatePromiseRef.current = null;
|
|
});
|
|
}
|
|
uploadFolderId = await uploadAssetFolderCreatePromiseRef.current;
|
|
}
|
|
const uploadIndex = options.uploadIndex;
|
|
let imageDimensions: { width: number; height: number } | null = null;
|
|
let mediaMetadata: Awaited<
|
|
ReturnType<typeof probeMediaFileMetadata>
|
|
> | null = null;
|
|
if (mediaType === 'image') {
|
|
imageDimensions = await probeImageFileDimensions(file).catch(
|
|
() => null,
|
|
);
|
|
} else if (mediaType === 'video') {
|
|
mediaMetadata = await probeMediaFileMetadata(file, 'video').catch(
|
|
() => null,
|
|
);
|
|
imageDimensions = mediaMetadata?.dimensions ?? null;
|
|
}
|
|
const uploadedAsset = createUploadingAssetPlaceholder({
|
|
uploadIndex,
|
|
fileName: file.name,
|
|
folderId: uploadFolderId,
|
|
mediaType,
|
|
imageDimensions,
|
|
});
|
|
setAssets((currentAssets) => [...currentAssets, uploadedAsset]);
|
|
setAssetFolders((currentFolders) =>
|
|
expandUploadFolder({
|
|
folders: currentFolders,
|
|
folderId: uploadFolderId,
|
|
}),
|
|
);
|
|
|
|
let previewSrc = '';
|
|
let persistedImageSrc = '';
|
|
let objectKey: string | null = null;
|
|
let assetObjectId: string | null = null;
|
|
let durationSeconds: number | undefined;
|
|
try {
|
|
if (mediaType !== 'image') {
|
|
durationSeconds =
|
|
mediaMetadata?.durationSeconds ??
|
|
(await probeMediaDurationSeconds(file, mediaType).catch(
|
|
() => undefined,
|
|
));
|
|
}
|
|
const uploadedMedia = await uploadEditorMediaAssetFile(file, mediaType);
|
|
previewSrc = uploadedMedia.src;
|
|
persistedImageSrc = uploadedMedia.legacyPublicPath;
|
|
objectKey = uploadedMedia.objectKey;
|
|
assetObjectId = uploadedMedia.assetObjectId;
|
|
setAssets((currentAssets) =>
|
|
applyUploadAssetReadSuccess({
|
|
assets: currentAssets,
|
|
uploadAssetId: uploadedAsset.id,
|
|
imageSrc: previewSrc,
|
|
}),
|
|
);
|
|
} catch {
|
|
setAssets((currentAssets) =>
|
|
applyUploadAssetReadFailure({
|
|
assets: currentAssets,
|
|
uploadAssetId: uploadedAsset.id,
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const nextLayer = createUploadCanvasLayer({
|
|
uploadIndex,
|
|
fileName: file.name,
|
|
imageSrc: previewSrc,
|
|
objectKey,
|
|
assetObjectId,
|
|
mediaType,
|
|
imageDimensions,
|
|
durationSeconds,
|
|
canvasPoint: options.canvasPoint,
|
|
canvasSize,
|
|
viewport,
|
|
});
|
|
|
|
if (options.addToCanvas) {
|
|
captureCanvasHistory({ type: 'upload-image', count: 1 });
|
|
appendCanvasLayersWithResources([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
}
|
|
|
|
setAssets((currentAssets) =>
|
|
applyUploadAssetCreatePending({
|
|
assets: currentAssets,
|
|
uploadAssetId: uploadedAsset.id,
|
|
}),
|
|
);
|
|
createEditorAsset({
|
|
folderId: uploadFolderId,
|
|
label: uploadedAsset.label,
|
|
imageSrc: persistedImageSrc,
|
|
...(objectKey ? { objectKey } : {}),
|
|
...(assetObjectId ? { assetObjectId } : {}),
|
|
width: uploadedAsset.width,
|
|
height: uploadedAsset.height,
|
|
sourceType: 'uploaded',
|
|
assetKind: nextLayer.assetKind,
|
|
generationInputs: nextLayer.generationInputs,
|
|
})
|
|
.then((asset) => {
|
|
const persistedAssetWithPreview = {
|
|
...asset,
|
|
imageSrc: previewSrc,
|
|
};
|
|
setAssets((currentAssets) =>
|
|
applyPersistedUploadAsset({
|
|
assets: currentAssets,
|
|
uploadAssetId: uploadedAsset.id,
|
|
persistedAsset: persistedAssetWithPreview,
|
|
}),
|
|
);
|
|
if (options.addToCanvas) {
|
|
setLayers((currentLayers) =>
|
|
bindUploadLayerToPersistedAsset({
|
|
layers: currentLayers,
|
|
layerId: nextLayer.id,
|
|
persistedAsset: persistedAssetWithPreview,
|
|
}),
|
|
);
|
|
}
|
|
})
|
|
.catch((error: unknown) => {
|
|
const isAuthError = isEditorAuthError(error);
|
|
if (isAuthError) {
|
|
openEditorLoginModal();
|
|
}
|
|
setAssets((currentAssets) =>
|
|
applyUploadAssetCreateFailure({
|
|
assets: currentAssets,
|
|
uploadAssetId: uploadedAsset.id,
|
|
message: isAuthError ? '请先登录' : '上传失败',
|
|
}),
|
|
);
|
|
});
|
|
|
|
if (mediaType === 'image' && previewSrc) {
|
|
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) =>
|
|
applyMeasuredUploadAssetSize({
|
|
assets: currentAssets,
|
|
uploadAssetId: uploadedAsset.id,
|
|
originalWidth,
|
|
originalHeight,
|
|
}),
|
|
);
|
|
};
|
|
uploadedImage.src = previewSrc;
|
|
}
|
|
},
|
|
[
|
|
activeUploadFolderId,
|
|
appendCanvasLayersWithResources,
|
|
captureCanvasHistory,
|
|
assetFolders,
|
|
canvasSize,
|
|
openEditorLoginModal,
|
|
selectSingleLayer,
|
|
setAssetFolders,
|
|
setAssets,
|
|
setLayers,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
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, options: UploadRequestOptions = {}) => {
|
|
setUploadTarget(target);
|
|
uploadRequestOptionsRef.current = options;
|
|
uploadInputRef.current?.click();
|
|
},
|
|
[setUploadTarget],
|
|
);
|
|
|
|
const requestUpload = useCallback(
|
|
(target: UploadTarget = 'asset', options: UploadRequestOptions = {}) => {
|
|
if (!canAccessProtectedDataRef.current) {
|
|
openEditorLoginModal();
|
|
return;
|
|
}
|
|
openUploadPicker(target, options);
|
|
},
|
|
[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 === 'generation-reference') {
|
|
void addGenerationReferenceFiles(files);
|
|
} else if (currentUploadTarget === 'ui-asset-extraction-reference') {
|
|
void addUiAssetExtractionReferenceFiles(files);
|
|
} else if (currentUploadTarget === 'quick-edit-reference') {
|
|
void addQuickEditReferenceFiles(files);
|
|
} else if (currentUploadTarget === 'publication-reference') {
|
|
void addPublicationReferenceFiles(files);
|
|
} else if (currentUploadTarget === 'video-reference-image') {
|
|
void addVideoReferenceImageFiles(files);
|
|
} else if (currentUploadTarget === 'video-reference-video') {
|
|
void addVideoReferenceVideoFiles(files);
|
|
} else if (currentUploadTarget === 'video-reference-audio') {
|
|
void addVideoReferenceAudioFiles(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 if (currentUploadTarget === 'ui-design-icon-spec') {
|
|
void addUiDesignSpecReferenceFiles(files);
|
|
} else {
|
|
const requestOptions = uploadRequestOptionsRef.current;
|
|
addUploadedFiles(files, {
|
|
folderId: requestOptions.folderId,
|
|
addToCanvas: requestOptions.addToCanvas ?? activeTool === 'upload',
|
|
});
|
|
}
|
|
}
|
|
uploadRequestOptionsRef.current = {};
|
|
setUploadTarget('asset');
|
|
event.currentTarget.value = '';
|
|
},
|
|
[
|
|
activeTool,
|
|
addCharacterReferenceFiles,
|
|
addCharacterSpecReferenceFiles,
|
|
addGenerationReferenceFiles,
|
|
addUiAssetExtractionReferenceFiles,
|
|
addQuickEditReferenceFiles,
|
|
addPublicationReferenceFiles,
|
|
addVideoReferenceImageFiles,
|
|
addVideoReferenceVideoFiles,
|
|
addVideoReferenceAudioFiles,
|
|
addSpecReferenceFiles,
|
|
addIconSpecReferenceFiles,
|
|
addUiDesignSpecReferenceFiles,
|
|
addUploadedFiles,
|
|
setUploadTarget,
|
|
],
|
|
);
|
|
|
|
return {
|
|
uploadInputRef,
|
|
uploadTarget,
|
|
pendingImageReferenceUploadCount,
|
|
setUploadTarget,
|
|
requestUpload,
|
|
handleUploadInputChange,
|
|
addUploadedFiles,
|
|
addSpecReferenceFiles,
|
|
addCharacterSpecReferenceFiles,
|
|
addCharacterReferenceFiles,
|
|
addGenerationReferenceFiles,
|
|
addUiAssetExtractionReferenceFiles,
|
|
addQuickEditReferenceFiles,
|
|
addPublicationReferenceFiles,
|
|
addVideoReferenceImageFiles,
|
|
addVideoReferenceVideoFiles,
|
|
addVideoReferenceAudioFiles,
|
|
addIconSpecReferenceFiles,
|
|
addUiDesignSpecReferenceFiles,
|
|
};
|
|
}
|