import { FileImage, FileUp, Upload } from "lucide-react"; import { type DragEvent, useCallback, useEffect, useState } from "react"; import { useTranslation } from "@/contexts/i18n-context"; import { useUrlImport } from "@/hooks/use-url-import"; import { cn } from "@/lib/utils"; import { UrlImportModal } from "./url-import-modal"; const IMAGE_EXTENSIONS = new Set([ "jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "avif", "tiff", "tif", "ico", "heic", "heif", "hif", "jxl", "apng", "dng", "cr2", "cr3", "nef", "nrw", "arw", "orf", "rw2", "raf", "pef", "3fr", "iiq", "srw", "x3f", "rwl", "gpr", "fff", "mrw", "mef", "kdc", "dcr", "erf", "ptx", "tga", "psd", "exr", "hdr", "svgz", "jp2", "j2k", "j2c", "jpc", "jpf", "jpx", "qoi", "eps", "epsf", "dds", "cur", "dpx", "cin", "fits", "fit", "fts", "pbm", "pgm", "ppm", "pnm", "pam", "pfm", ]); export function isImageFile(file: File): boolean { if (file.type.startsWith("image/")) return true; const ext = file.name.split(".").pop()?.toLowerCase() ?? ""; return IMAGE_EXTENSIONS.has(ext); } interface DropzoneProps { onFiles?: (files: File[]) => void; onUrlImport?: (file: File) => void; accept?: string; multiple?: boolean; /** Files that have already been dropped (for showing count & list). */ currentFiles?: File[]; /** Use a smaller layout that fits constrained containers like the pipeline preview. */ compact?: boolean; /** Override the default isImageFile check for drag/drop and paste. */ fileFilter?: (file: File) => boolean; /** Override the format hint text (e.g. "SVG files only"). */ acceptDescription?: string; } // Browsers may not map certain formats (HEIC, JXL, RAW, etc.) to image/* in file pickers. // Append explicit extensions so they are selectable. function expandAccept(accept?: string): string | undefined { if (!accept?.includes("image/*")) return accept; return `${accept},.avif,.heic,.heif,.hif,.jxl,.ico,.dng,.cr2,.cr3,.nef,.nrw,.arw,.orf,.rw2,.raf,.pef,.3fr,.iiq,.srw,.x3f,.rwl,.gpr,.fff,.mrw,.mef,.kdc,.dcr,.erf,.ptx,.tga,.psd,.exr,.hdr,.svgz,.jp2,.j2k,.j2c,.jpc,.jpf,.jpx,.qoi,.eps,.epsf,.dds,.cur,.apng,.dpx,.cin,.fits,.fit,.fts,.ppm,.pgm,.pbm,.pnm,.pam,.pfm`; } export function Dropzone({ onFiles, onUrlImport, accept, multiple = true, currentFiles = [], compact = false, fileFilter, acceptDescription, }: DropzoneProps) { const { t } = useTranslation(); const checkFile = fileFilter ?? isImageFile; const resolvedAccept = expandAccept(accept); const [isDragging, setIsDragging] = useState(false); const [urlInput, setUrlInput] = useState(""); const [urlLoading, setUrlLoading] = useState(false); const [urlError, setUrlError] = useState(null); const [showBulkModal, setShowBulkModal] = useState(false); const { importSingleUrl } = useUrlImport(); const handleUrlSubmit = useCallback(async () => { const url = urlInput.trim(); if (!url) return; setUrlLoading(true); setUrlError(null); const file = await importSingleUrl(url); if (file) { if (!checkFile(file)) { setUrlError(acceptDescription ?? t.dropzone.unsupportedFileType); } else { setUrlInput(""); onUrlImport?.(file); } } else { setUrlError(t.dropzone.urlFetchFailed); } setUrlLoading(false); }, [urlInput, importSingleUrl, onUrlImport, checkFile, acceptDescription]); const handleDrag = useCallback((e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") setIsDragging(true); else if (e.type === "dragleave") setIsDragging(false); }, []); const handleDrop = useCallback( (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const files = Array.from(e.dataTransfer.files).filter(checkFile); if (files.length > 0) onFiles?.(files); }, [onFiles, checkFile], ); const handleClick = () => { const input = document.createElement("input"); input.type = "file"; input.multiple = multiple; if (resolvedAccept) input.accept = resolvedAccept; input.onchange = (e) => { const files = Array.from((e.target as HTMLInputElement).files || []).filter(checkFile); if (files.length > 0) onFiles?.(files); }; input.click(); }; useEffect(() => { const handlePaste = (e: ClipboardEvent) => { const clip = e.clipboardData; if (!clip) return; const files: File[] = []; if (clip.files.length > 0) { for (const file of clip.files) { if (checkFile(file)) files.push(file); } } else if (clip.items) { for (const item of clip.items) { if (item.kind === "file") { const file = item.getAsFile(); if (file && checkFile(file)) files.push(file); } } } if (files.length > 0) { e.preventDefault(); onFiles?.(files); } }; document.addEventListener("paste", handlePaste); return () => document.removeEventListener("paste", handlePaste); }, [onFiles, checkFile]); const hasMultipleFiles = currentFiles.length > 1; return (

{t.dropzone.dropPrompt}

{t.dropzone.browseOrPaste}

{acceptDescription ?? t.dropzone.defaultFormats}

{!compact && onUrlImport && ( <>
{t.dropzone.orSeparator}
{ setUrlInput(e.target.value); setUrlError(null); }} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); handleUrlSubmit(); } }} onClick={(e) => e.stopPropagation()} placeholder={t.dropzone.urlPlaceholder} className="flex-1 rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none" disabled={urlLoading} />
{urlError &&

{urlError}

} )} {hasMultipleFiles && (
{currentFiles.length} files selected
{currentFiles.map((f) => (
{f.name} {(f.size / 1024).toFixed(0)} KB
))}
)}
{showBulkModal && ( setShowBulkModal(false)} onImport={(files) => { for (const file of files) { if (checkFile(file)) onUrlImport?.(file); } setShowBulkModal(false); }} /> )}
); }