mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-06 01:14:36 +08:00
feat(drag-and-drop): implement file upload via drag-and-drop for reference assets in image and video workspaces
This commit is contained in:
@@ -2087,14 +2087,13 @@ function InfiniteCanvasPage() {
|
||||
return;
|
||||
}
|
||||
const markSourceStatus = sourceNode?.type !== CanvasNodeType.Image && !editingTextNode;
|
||||
const statusPrompt = sourceNode?.type === CanvasNodeType.Config ? effectivePrompt : prompt;
|
||||
if (!effectivePrompt && (mode === "text" || mode === "audio")) {
|
||||
finishGenerationRequest(nodeId, runController);
|
||||
setRunningNodeId(null);
|
||||
return;
|
||||
}
|
||||
let pendingChildIds: string[] = [];
|
||||
if (markSourceStatus) setNodes((prev) => prev.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, prompt: statusPrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } } : node)));
|
||||
if (markSourceStatus) setNodes((prev) => prev.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...(node.type === CanvasNodeType.Config ? {} : { prompt }), status: NODE_STATUS_LOADING, errorDetails: undefined } } : node)));
|
||||
|
||||
try {
|
||||
if (mode === "image") {
|
||||
@@ -2158,7 +2157,7 @@ function InfiniteCanvasPage() {
|
||||
? isConfigNode
|
||||
? {
|
||||
...node,
|
||||
metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined },
|
||||
metadata: { ...node.metadata, status: NODE_STATUS_LOADING, errorDetails: undefined },
|
||||
}
|
||||
: isEmptyImageNode
|
||||
? {
|
||||
@@ -2386,7 +2385,7 @@ function InfiniteCanvasPage() {
|
||||
height: textConfig.height,
|
||||
metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, fontSize: 14 },
|
||||
}));
|
||||
setNodes((prev) => [...prev.map((node) => (node.id === nodeId && isConfigNode ? { ...node, metadata: { ...node.metadata, prompt: effectivePrompt, status: NODE_STATUS_LOADING, errorDetails: undefined } } : node)), ...childNodes]);
|
||||
setNodes((prev) => [...prev.map((node) => (node.id === nodeId && isConfigNode ? { ...node, metadata: { ...node.metadata, status: NODE_STATUS_LOADING, errorDetails: undefined } } : node)), ...childNodes]);
|
||||
setConnections((prev) => [...prev, ...childIds.map((childId) => ({ id: nanoid(), fromNodeId: nodeId, toNodeId: childId }))]);
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ const logStore = localforage.createInstance({ name: "infinite-canvas", storeName
|
||||
export default function ImagePage() {
|
||||
const { message } = App.useApp();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const dragDepthRef = useRef(0);
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
const updateConfig = useConfigStore((state) => state.updateConfig);
|
||||
@@ -89,6 +90,7 @@ export default function ImagePage() {
|
||||
const [selectedLogIds, setSelectedLogIds] = useState<string[]>([]);
|
||||
const [previewLog, setPreviewLog] = useState<GenerationLog | null>(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [isReferenceDragActive, setIsReferenceDragActive] = useState(false);
|
||||
const [autoRunToken, setAutoRunToken] = useState(0);
|
||||
const imageCommand = useWorkbenchAgentStore((state) => state.imageCommand);
|
||||
const clearImageCommand = useWorkbenchAgentStore((state) => state.clearImageCommand);
|
||||
@@ -427,7 +429,27 @@ export default function ImagePage() {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed border-stone-300 p-2 pb-3 overscroll-x-contain dark:border-stone-700"
|
||||
className={`hover-scrollbar hover-scrollbar-hint relative flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed p-2 pb-3 overscroll-x-contain transition-colors ${isReferenceDragActive ? "border-stone-900 bg-stone-100/80 dark:border-stone-100 dark:bg-stone-900/80" : "border-stone-300 dark:border-stone-700"}`}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
dragDepthRef.current += 1;
|
||||
if (event.dataTransfer.types.includes("Files")) setIsReferenceDragActive(true);
|
||||
}}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}}
|
||||
onDragLeave={(event) => {
|
||||
event.preventDefault();
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||
if (!dragDepthRef.current) setIsReferenceDragActive(false);
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
dragDepthRef.current = 0;
|
||||
setIsReferenceDragActive(false);
|
||||
void addReferences(event.dataTransfer.files);
|
||||
}}
|
||||
onWheel={(event) => {
|
||||
if (event.currentTarget.scrollWidth <= event.currentTarget.clientWidth) return;
|
||||
event.preventDefault();
|
||||
@@ -449,7 +471,7 @@ export default function ImagePage() {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{!references.length ? <div className="flex min-w-full items-center justify-center text-sm text-stone-500">暂无参考图</div> : null}
|
||||
{!references.length ? <div className="flex min-w-full items-center justify-center text-sm text-stone-500">{isReferenceDragActive ? "松开即可添加参考图" : "暂无参考图,可将图片拖到这里"}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ArrowLeft, ArrowRight, BookOpen, CheckSquare, ClipboardPaste, Download, FolderPlus, History, LoaderCircle, Music2, Plus, SlidersHorizontal, Sparkles, Trash2, Upload, VideoIcon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState, type DragEvent } from "react";
|
||||
import { App, Button, Checkbox, Drawer, Empty, Input, Modal, Tag, Typography } from "antd";
|
||||
import localforage from "localforage";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -71,6 +71,7 @@ const logStore = localforage.createInstance({ name: "infinite-canvas", storeName
|
||||
export default function VideoPage() {
|
||||
const { message } = App.useApp();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const dragDepthRef = useRef(0);
|
||||
const activeLogIdsRef = useRef<Set<string>>(new Set());
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
@@ -94,6 +95,7 @@ export default function VideoPage() {
|
||||
const [selectedLogIds, setSelectedLogIds] = useState<string[]>([]);
|
||||
const [previewLog, setPreviewLog] = useState<GenerationLog | null>(null);
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [referenceDragTarget, setReferenceDragTarget] = useState<"image" | "video" | "audio" | null>(null);
|
||||
const [autoRunToken, setAutoRunToken] = useState(0);
|
||||
const videoCommand = useWorkbenchAgentStore((state) => state.videoCommand);
|
||||
const clearVideoCommand = useWorkbenchAgentStore((state) => state.clearVideoCommand);
|
||||
@@ -151,6 +153,25 @@ export default function VideoPage() {
|
||||
setAudioReferences((value) => [...value, ...nextAudioReferences].slice(0, SEEDANCE_REFERENCE_LIMITS.audios));
|
||||
};
|
||||
|
||||
const handleReferenceDragEnter = (event: DragEvent<HTMLDivElement>, target: "image" | "video" | "audio") => {
|
||||
event.preventDefault();
|
||||
dragDepthRef.current += 1;
|
||||
if (event.dataTransfer.types.includes("Files")) setReferenceDragTarget(target);
|
||||
};
|
||||
|
||||
const handleReferenceDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
||||
if (!dragDepthRef.current) setReferenceDragTarget(null);
|
||||
};
|
||||
|
||||
const handleReferenceDrop = (event: DragEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
dragDepthRef.current = 0;
|
||||
setReferenceDragTarget(null);
|
||||
void addReferences(event.dataTransfer.files);
|
||||
};
|
||||
|
||||
const addReferencesFromClipboard = async () => {
|
||||
try {
|
||||
const items = await navigator.clipboard.read();
|
||||
@@ -431,7 +452,16 @@ export default function VideoPage() {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed border-stone-300 p-2 pb-3 overscroll-x-contain dark:border-stone-700">
|
||||
<div
|
||||
className={`hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed p-2 pb-3 overscroll-x-contain transition-colors ${referenceDragTarget === "image" ? "border-stone-900 bg-stone-100/80 dark:border-stone-100 dark:bg-stone-900/80" : "border-stone-300 dark:border-stone-700"}`}
|
||||
onDragEnter={(event) => handleReferenceDragEnter(event, "image")}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}}
|
||||
onDragLeave={handleReferenceDragLeave}
|
||||
onDrop={handleReferenceDrop}
|
||||
>
|
||||
{references.map((item, index) => (
|
||||
<div key={item.id} className="group relative size-20 shrink-0 overflow-hidden rounded-md border border-stone-200 dark:border-stone-800">
|
||||
<img src={item.dataUrl} alt={item.name} className="size-full object-cover" />
|
||||
@@ -442,7 +472,7 @@ export default function VideoPage() {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{!references.length ? <div className="flex min-w-full items-center justify-center text-sm text-stone-500">暂无参考图,最多 9 张</div> : null}
|
||||
{!references.length ? <div className="flex min-w-full items-center justify-center text-sm text-stone-500">{referenceDragTarget === "image" ? "松开即可上传参考资产" : "暂无参考图,可拖入文件,最多 9 张"}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -453,7 +483,16 @@ export default function VideoPage() {
|
||||
上传
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed border-stone-300 p-2 pb-3 overscroll-x-contain dark:border-stone-700">
|
||||
<div
|
||||
className={`hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed p-2 pb-3 overscroll-x-contain transition-colors ${referenceDragTarget === "video" ? "border-stone-900 bg-stone-100/80 dark:border-stone-100 dark:bg-stone-900/80" : "border-stone-300 dark:border-stone-700"}`}
|
||||
onDragEnter={(event) => handleReferenceDragEnter(event, "video")}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}}
|
||||
onDragLeave={handleReferenceDragLeave}
|
||||
onDrop={handleReferenceDrop}
|
||||
>
|
||||
{videoReferences.map((item, index) => (
|
||||
<div key={item.id} className="group relative h-20 w-32 shrink-0 overflow-hidden rounded-md border border-stone-200 bg-black dark:border-stone-800">
|
||||
<video src={item.url} className="size-full object-cover" muted preload="metadata" />
|
||||
@@ -464,7 +503,7 @@ export default function VideoPage() {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{!videoReferences.length ? <div className="flex min-w-full items-center justify-center text-sm text-stone-500">暂无参考视频,最多 3 个</div> : null}
|
||||
{!videoReferences.length ? <div className="flex min-w-full items-center justify-center text-sm text-stone-500">{referenceDragTarget === "video" ? "松开即可上传参考资产" : "暂无参考视频,可拖入文件,最多 3 个"}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -475,7 +514,16 @@ export default function VideoPage() {
|
||||
上传
|
||||
</Button>
|
||||
</div>
|
||||
<div className="hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed border-stone-300 p-2 pb-3 overscroll-x-contain dark:border-stone-700">
|
||||
<div
|
||||
className={`hover-scrollbar hover-scrollbar-hint flex min-h-24 w-full min-w-0 max-w-full gap-2 overflow-x-scroll overflow-y-hidden rounded-lg border border-dashed p-2 pb-3 overscroll-x-contain transition-colors ${referenceDragTarget === "audio" ? "border-stone-900 bg-stone-100/80 dark:border-stone-100 dark:bg-stone-900/80" : "border-stone-300 dark:border-stone-700"}`}
|
||||
onDragEnter={(event) => handleReferenceDragEnter(event, "audio")}
|
||||
onDragOver={(event) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
}}
|
||||
onDragLeave={handleReferenceDragLeave}
|
||||
onDrop={handleReferenceDrop}
|
||||
>
|
||||
{audioReferences.map((item, index) => (
|
||||
<div key={item.id} className="group relative flex h-20 w-48 shrink-0 flex-col justify-center gap-2 rounded-md border border-stone-200 bg-stone-50 px-2 dark:border-stone-800 dark:bg-stone-900">
|
||||
<div className="flex min-w-0 items-center gap-2 text-xs text-stone-500 dark:text-stone-400">
|
||||
@@ -490,7 +538,7 @@ export default function VideoPage() {
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{!audioReferences.length ? <div className="flex min-w-full items-center justify-center text-center text-sm text-stone-500">暂无参考音频,最多 3 个,mp3/wav,单个 15MB 内</div> : null}
|
||||
{!audioReferences.length ? <div className="flex min-w-full items-center justify-center text-center text-sm text-stone-500">{referenceDragTarget === "audio" ? "松开即可上传参考资产" : "暂无参考音频,可拖入文件,最多 3 个,mp3/wav,单个 15MB 内"}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user