diff --git a/frontend/src/canvas/image-drop.ts b/frontend/src/canvas/image-drop.ts index 2e865c9..20e125d 100644 --- a/frontend/src/canvas/image-drop.ts +++ b/frontend/src/canvas/image-drop.ts @@ -119,14 +119,22 @@ export function setupDragDrop( const world = viewport.toWorld(e.clientX - rect.left, e.clientY - rect.top); const placeholder = createPlaceholder(viewport, world.x, world.y); + // Derive a filename from the URL for the upload manager + const urlFilename = url.split('/').pop()?.split('?')[0] || 'url-import'; + const isVideoUrl = /\.(mp4|webm|mov)(\?|$)/i.test(url); + const jobId = uploads?.addUrlJob(urlFilename, isVideoUrl ? 'video' : 'image'); + try { const res = await uploadImageFromUrl(boardId, url); removePlaceholder(viewport, placeholder); const { id } = handleUploadResult(res, viewport, sceneManager, world.x, world.y, onChange); selection?.selectOnly(id); + const imgData = res.data.image || res.data; + if (jobId) uploads?.uploadComplete(jobId, imgData.id); } catch (err: any) { console.error('URL image upload failed:', err); removePlaceholder(viewport, placeholder); + if (jobId) uploads?.setFailed(jobId, err.response?.data?.error || err.message || 'URL import failed'); } } return; diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index a8db4e8..841fd8e 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -64,6 +64,11 @@ export default function Editor({ isPublicView }: EditorProps) { const canvasContainerRef = useRef(null); const [uploadManager] = useState(() => new UploadManager()); + // Reset upload manager when switching boards + useEffect(() => { + uploadManager.clear(); + }, [boardId, uploadManager]); + // UI state const [activeTool, setActiveTool] = useState(ToolType.SELECT); const [color, setColor] = useState('#ffffff'); diff --git a/frontend/src/stores/uploadManager.ts b/frontend/src/stores/uploadManager.ts index 5633873..28f9f1f 100644 --- a/frontend/src/stores/uploadManager.ts +++ b/frontend/src/stores/uploadManager.ts @@ -32,7 +32,7 @@ export class UploadManager { for (const fn of this._listeners) fn(); } - /** Create a new upload job. Returns the job ID. */ + /** Create a new upload job from a local file. Returns the job ID. */ addJob(file: File, boardId: string): string { const id = crypto.randomUUID(); const isVideo = file.type.startsWith('video/'); @@ -51,6 +51,22 @@ export class UploadManager { return id; } + /** Create a job for a URL-based import (no File object, unknown size). */ + addUrlJob(fileName: string, mediaType: 'image' | 'video'): string { + const id = crypto.randomUUID(); + this.jobs.set(id, { + id, + fileName, + fileSize: 0, + mediaType, + status: 'uploading', + progress: 0, + createdAt: Date.now(), + }); + this._notify(); + return id; + } + /** Update upload progress (0-1). */ setProgress(jobId: string, progress: number) { const job = this.jobs.get(jobId); @@ -116,6 +132,13 @@ export class UploadManager { this._notify(); } + /** Remove all jobs (used on board change). */ + clear() { + for (const id of this._dismissTimers.keys()) this._clearDismissTimer(id); + this.jobs.clear(); + this._notify(); + } + /** Remove all completed/failed jobs. */ clearFinished() { for (const [id, job] of this.jobs) { @@ -127,11 +150,11 @@ export class UploadManager { this._notify(); } - /** Get active (non-done) job count. */ + /** Get active (uploading or processing) job count. */ get activeCount(): number { let count = 0; for (const job of this.jobs.values()) { - if (job.status !== 'done') count++; + if (job.status === 'uploading' || job.status === 'processing') count++; } return count; }