Files
Genarrative/src/components/image-editor/useImageCanvasUploadWorkflow.ts
T
kdletters 98545d74d1 修复画布底部上传入画布尺寸
底部工具栏上传图片后直接创建画布图层

上传图片在创建素材和图层前读取原始分辨率

补充上传尺寸解析与素材库集成回归测试

同步图片画布上传语义文档和共享决策记录
2026-06-19 17:36:20 +08:00

934 lines
28 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 } from '../../services/image-editor/editorProjectClient';
import { uploadEditorSeedanceReferenceFile } from '../../services/image-editor/editorReferenceUploadClient';
import type {
CanvasLayer,
CanvasTool,
CanvasViewport,
EditorAsset,
EditorAssetFolder,
GenerateDialogState,
QuickEditPanelState,
UploadTarget,
} from './ImageCanvasEditorTypes';
import {
SEEDANCE_REFERENCE_FILE_LIMITS,
isImageFile,
resolveEditorAssetUploadMediaType,
isSeedanceReferenceAudioFile,
isSeedanceReferenceImageFile,
isSeedanceReferenceVideoFile,
probeImageFileDimensions,
probeMediaDurationSeconds,
readFileAsDataUrl,
readImageFileAsDataUrl,
} from './ImageCanvasFileModel';
import {
applyGenerationReferenceUpload,
applyQuickEditReferenceUpload,
applyMeasuredUploadAssetSize,
applyPersistedUploadAsset,
applyUploadAssetCreateFailure,
applyUploadAssetCreatePending,
applyUploadAssetReadFailure,
applyUploadAssetReadSuccess,
bindUploadLayerToPersistedAsset,
createUploadCanvasLayer,
createUploadedGenerationReference,
createUploadingAssetPlaceholder,
expandUploadFolder,
resizeUploadCanvasLayerToImage,
resolveUploadFolderId,
} from './ImageCanvasUploadModel';
type UploadFilesOptions = {
folderId?: string;
canvasPoint?: { x: number; y: number };
addToCanvas?: boolean;
};
type UploadRequestOptions = {
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>>;
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
selectSingleLayer: (layerId: string | null) => void;
};
function isEditorAuthError(error: unknown) {
return (
error instanceof ApiClientError &&
(error.status === 401 || error.status === 403)
);
}
export function useImageCanvasUploadWorkflow({
canAccessProtectedData,
openEditorLoginModal,
assetFolders,
activeUploadFolderId,
canvasSize,
viewport,
activeTool,
allocateUploadIndex,
setAssetFolders,
setAssets,
setLayers,
setGenerateDialog,
setQuickEditPanel,
appendCanvasLayersWithResources,
selectSingleLayer,
}: UseImageCanvasUploadWorkflowOptions) {
const uploadInputRef = useRef<HTMLInputElement | null>(null);
const canAccessProtectedDataRef = useRef(canAccessProtectedData);
const uploadTargetRef = useRef<UploadTarget>('asset');
const uploadRequestOptionsRef = useRef<UploadRequestOptions>({});
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) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'character-spec',
references: [
createUploadedGenerationReference({
idPrefix: 'upload-character-spec',
fileName: imageFile.name,
fallbackLabel: '角色规范',
imageSrc,
}),
],
}),
);
},
[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) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'spec-reference',
references: [
createUploadedGenerationReference({
idPrefix: 'upload-spec-reference',
fileName: imageFile.name,
fallbackLabel: '参考图',
imageSrc,
}),
],
}),
);
},
[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) => ({
file,
index,
imageSrc: await readImageFileAsDataUrl(file),
})),
);
setGenerateDialog((currentDialog) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'character-reference',
references: references.map(({ file, imageSrc, index }) =>
createUploadedGenerationReference({
idPrefix: 'upload-character-reference',
index,
fileName: file.name,
fallbackLabel: `参考图${index + 1}`,
imageSrc,
}),
),
}),
);
},
[setGenerateDialog],
);
const addGenerationReferenceFiles = 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) => ({
file,
index,
imageSrc: await readImageFileAsDataUrl(file),
})),
);
setGenerateDialog((currentDialog) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'generation-reference',
references: references.map(({ file, imageSrc, index }) =>
createUploadedGenerationReference({
idPrefix: 'upload-generation-reference',
index,
fileName: file.name,
fallbackLabel: `参考图${index + 1}`,
imageSrc,
}),
),
}),
);
},
[setGenerateDialog],
);
const addQuickEditReferenceFiles = 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) => ({
file,
index,
imageSrc: await readImageFileAsDataUrl(file),
})),
);
setQuickEditPanel((currentPanel) =>
applyQuickEditReferenceUpload({
panel: currentPanel,
references: references.map(({ file, imageSrc, index }) =>
createUploadedGenerationReference({
idPrefix: 'upload-quick-edit-reference',
index,
fileName: file.name,
fallbackLabel: `参考图${index + 1}`,
imageSrc,
}),
),
}),
);
},
[setQuickEditPanel],
);
const addPublicationReferenceFiles = 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) => ({
file,
index,
imageSrc: await readImageFileAsDataUrl(file),
})),
);
setGenerateDialog((currentDialog) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'publication-reference',
references: references.map(({ file, imageSrc, index }) =>
createUploadedGenerationReference({
idPrefix: 'upload-publication-reference',
index,
fileName: file.name,
fallbackLabel: `宣发参考图${index + 1}`,
imageSrc,
}),
),
}),
);
},
[setGenerateDialog],
);
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),
})),
);
setGenerateDialog((currentDialog) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'video-reference-image',
references: references.map(({ file, src, index }) =>
createUploadedGenerationReference({
idPrefix: 'upload-video-reference-image',
index,
fileName: file.name,
fallbackLabel: `参考图${index + 1}`,
imageSrc: src,
mediaType: 'image',
mimeType: file.type || undefined,
sizeBytes: file.size,
}),
),
}),
);
},
[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;
}
setGenerateDialog((currentDialog) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'video-reference-video',
references: uploadedReferences.map(({ file, src, index, durationSeconds, objectKey, assetObjectId }) =>
createUploadedGenerationReference({
idPrefix: 'upload-video-reference-video',
index,
fileName: file.name,
fallbackLabel: `参考视频${index + 1}`,
imageSrc: src,
mediaType: 'video',
mimeType: file.type || undefined,
sizeBytes: file.size,
durationSeconds,
objectKey,
assetObjectId,
}),
),
}),
);
},
[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;
}
setGenerateDialog((currentDialog) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'video-reference-audio',
references: uploadedReferences.map(({ file, src, index, durationSeconds, objectKey, assetObjectId }) =>
createUploadedGenerationReference({
idPrefix: 'upload-video-reference-audio',
index,
fileName: file.name,
fallbackLabel: `参考音频${index + 1}`,
imageSrc: src,
mediaType: 'audio',
mimeType: file.type || undefined,
sizeBytes: file.size,
durationSeconds,
objectKey,
assetObjectId,
}),
),
}),
);
},
[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) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'icon-spec',
references: [
createUploadedGenerationReference({
idPrefix: 'upload-icon-spec',
fileName: imageFile.name,
fallbackLabel: '图标规范',
imageSrc,
}),
],
}),
);
},
[setGenerateDialog],
);
const addUiDesignSpecReferenceFiles = useCallback(
async (files: FileList | File[]) => {
const imageFile = Array.from(files).find(isImageFile);
if (!imageFile) {
window.alert('请选择图片文件');
return;
}
const imageSrc = await readImageFileAsDataUrl(imageFile);
setGenerateDialog((currentDialog) =>
applyGenerationReferenceUpload({
dialog: currentDialog,
target: 'ui-design-icon-spec',
references: [
createUploadedGenerationReference({
idPrefix: 'upload-ui-design-icon-spec',
fileName: imageFile.name,
fallbackLabel: 'UI设计图标规范',
imageSrc,
}),
],
}),
);
},
[setGenerateDialog],
);
const uploadAssetFile = useCallback(
async (file: File, options: UploadAssetFileOptions) => {
const mediaType = resolveEditorAssetUploadMediaType(file);
if (!mediaType) {
window.alert('请选择图片、MP3 或 MP4 文件');
return;
}
const uploadFolderId = resolveUploadFolderId({
assetFolders,
requestedFolderId: options.folderId,
activeUploadFolderId,
});
const uploadIndex = options.uploadIndex;
let imageDimensions: { width: number; height: number } | null = null;
if (mediaType === 'image') {
imageDimensions = await probeImageFileDimensions(file).catch(() => null);
}
const uploadedAsset = createUploadingAssetPlaceholder({
uploadIndex,
fileName: file.name,
folderId: uploadFolderId,
mediaType,
imageDimensions,
});
setAssets((currentAssets) => [...currentAssets, uploadedAsset]);
setAssetFolders((currentFolders) =>
expandUploadFolder({
folders: currentFolders,
folderId: uploadFolderId,
}),
);
let imageSrc = '';
let objectKey: string | null = null;
let assetObjectId: string | null = null;
let durationSeconds: number | undefined;
try {
if (mediaType === 'image') {
imageSrc = await readImageFileAsDataUrl(file);
} else {
durationSeconds = await probeMediaDurationSeconds(
file,
mediaType,
).catch(() => undefined);
const uploadedMedia = await uploadEditorMediaAssetFile(file, mediaType);
imageSrc = uploadedMedia.legacyPublicPath;
objectKey = uploadedMedia.objectKey;
assetObjectId = uploadedMedia.assetObjectId;
}
setAssets((currentAssets) =>
applyUploadAssetReadSuccess({
assets: currentAssets,
uploadAssetId: uploadedAsset.id,
imageSrc,
}),
);
} catch {
setAssets((currentAssets) =>
applyUploadAssetReadFailure({
assets: currentAssets,
uploadAssetId: uploadedAsset.id,
}),
);
return;
}
const nextLayer = createUploadCanvasLayer({
uploadIndex,
fileName: file.name,
imageSrc,
mediaType,
imageDimensions,
durationSeconds,
canvasPoint: options.canvasPoint,
canvasSize,
viewport,
});
if (options.addToCanvas) {
appendCanvasLayersWithResources([nextLayer]);
selectSingleLayer(nextLayer.id);
}
setAssets((currentAssets) =>
applyUploadAssetCreatePending({
assets: currentAssets,
uploadAssetId: uploadedAsset.id,
}),
);
createEditorAsset({
folderId: uploadFolderId,
label: uploadedAsset.label,
imageSrc,
...(objectKey ? { objectKey } : {}),
...(assetObjectId ? { assetObjectId } : {}),
width: uploadedAsset.width,
height: uploadedAsset.height,
sourceType: 'uploaded',
...(durationSeconds ? { durationSeconds } : {}),
})
.then((asset) => {
setAssets((currentAssets) =>
applyPersistedUploadAsset({
assets: currentAssets,
uploadAssetId: uploadedAsset.id,
persistedAsset: asset,
}),
);
if (options.addToCanvas) {
setLayers((currentLayers) =>
bindUploadLayerToPersistedAsset({
layers: currentLayers,
layerId: nextLayer.id,
persistedAsset: asset,
}),
);
}
})
.catch((error: unknown) => {
const isAuthError = isEditorAuthError(error);
if (isAuthError) {
openEditorLoginModal();
}
setAssets((currentAssets) =>
applyUploadAssetCreateFailure({
assets: currentAssets,
uploadAssetId: uploadedAsset.id,
message: isAuthError ? '请先登录' : '上传失败',
}),
);
});
if (mediaType === 'image' && 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) =>
applyMeasuredUploadAssetSize({
assets: currentAssets,
uploadAssetId: uploadedAsset.id,
originalWidth,
originalHeight,
}),
);
};
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, options: UploadRequestOptions = {}) => {
setUploadTarget(target);
uploadRequestOptionsRef.current = options;
uploadInputRef.current?.click();
},
[setUploadTarget],
);
const requestUpload = useCallback(
(target: UploadTarget = 'asset', options: UploadRequestOptions = {}) => {
if (target === 'asset' && !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 === '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, {
addToCanvas:
requestOptions.addToCanvas ?? activeTool === 'upload',
});
}
}
uploadRequestOptionsRef.current = {};
setUploadTarget('asset');
event.currentTarget.value = '';
},
[
activeTool,
addCharacterReferenceFiles,
addCharacterSpecReferenceFiles,
addGenerationReferenceFiles,
addQuickEditReferenceFiles,
addPublicationReferenceFiles,
addVideoReferenceImageFiles,
addVideoReferenceVideoFiles,
addVideoReferenceAudioFiles,
addSpecReferenceFiles,
addIconSpecReferenceFiles,
addUiDesignSpecReferenceFiles,
addUploadedFiles,
setUploadTarget,
],
);
return {
uploadInputRef,
uploadTarget,
setUploadTarget,
requestUpload,
handleUploadInputChange,
addUploadedFiles,
addSpecReferenceFiles,
addCharacterSpecReferenceFiles,
addCharacterReferenceFiles,
addGenerationReferenceFiles,
addQuickEditReferenceFiles,
addPublicationReferenceFiles,
addVideoReferenceImageFiles,
addVideoReferenceVideoFiles,
addVideoReferenceAudioFiles,
addIconSpecReferenceFiles,
addUiDesignSpecReferenceFiles,
};
}