import { memo, useState, useCallback, useRef } from "react"; import { VideoFrameThumbnail } from "../ui/VideoFrameThumbnail"; import { MEDIA_EXT, IMAGE_EXT, VIDEO_EXT, AUDIO_EXT } from "../../utils/mediaTypes"; import { TIMELINE_ASSET_MIME } from "../../utils/timelineAssetDrop"; import { copyTextToClipboard } from "../../utils/clipboard"; interface AssetsTabProps { projectId: string; assets: string[]; onImport?: (files: FileList) => void; onDelete?: (path: string) => void; onRename?: (oldPath: string, newPath: string) => void; } /** Inline thumbnail content — rendered inside the container div in AssetCard. */ function AssetThumbnail({ serveUrl, name, isImage, isVideo, isAudio, }: { serveUrl: string; name: string; isImage: boolean; isVideo: boolean; isAudio: boolean; }) { return ( <> {isImage && ( {name} { (e.target as HTMLImageElement).style.display = "none"; }} /> )} {isVideo && } {isAudio && (
)} {!isImage && !isVideo && !isAudio && (
)} ); } function AssetCard({ projectId, asset, onCopy, isCopied, onDelete, onRename, }: { projectId: string; asset: string; onCopy: (path: string) => void; isCopied: boolean; onDelete?: (path: string) => void; onRename?: (oldPath: string, newPath: string) => void; }) { const [hovered, setHovered] = useState(false); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const [renaming, setRenaming] = useState(false); const [renameName, setRenameName] = useState(""); const [confirmDelete, setConfirmDelete] = useState(false); const name = asset.split("/").pop() ?? asset; const serveUrl = `/api/projects/${projectId}/preview/${asset}`; const isVideo = VIDEO_EXT.test(asset); return ( <>
onCopy(asset)} onDragStart={(e) => { e.dataTransfer.effectAllowed = "copy"; e.dataTransfer.setData(TIMELINE_ASSET_MIME, JSON.stringify({ path: asset })); e.dataTransfer.setData("text/plain", asset); }} onContextMenu={(e) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY }); }} onPointerEnter={() => setHovered(true)} onPointerLeave={() => setHovered(false)} className={`w-full text-left px-2 py-1.5 flex items-center gap-2.5 transition-colors cursor-pointer ${ isCopied ? "bg-studio-accent/10 border-l-2 border-studio-accent" : "border-l-2 border-transparent hover:bg-neutral-800/50" }`} >
{isVideo && hovered && (
{renaming ? ( setRenameName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); const trimmed = renameName.trim(); if (trimmed && trimmed !== name) { const dir = asset.includes("/") ? asset.slice(0, asset.lastIndexOf("/") + 1) : ""; onRename?.(asset, dir + trimmed); } setRenaming(false); } else if (e.key === "Escape") { setRenaming(false); } }} onBlur={() => { const trimmed = renameName.trim(); if (trimmed && trimmed !== name) { const dir = asset.includes("/") ? asset.slice(0, asset.lastIndexOf("/") + 1) : ""; onRename?.(asset, dir + trimmed); } setRenaming(false); }} onClick={(e) => e.stopPropagation()} className="w-full bg-neutral-800 text-neutral-200 text-[11px] px-1.5 py-0.5 rounded border border-neutral-600 outline-none focus:border-studio-accent" spellCheck={false} /> ) : ( <> {name} {isCopied ? ( Copied! ) : ( {asset} )} )}
{/* Context menu */} {contextMenu && (
setContextMenu(null)} onContextMenu={(e) => { e.preventDefault(); setContextMenu(null); }} >
{onRename && ( )} {onDelete && ( )}
)} {/* Delete confirmation */} {confirmDelete && (
Delete {name}?
)} ); } export const AssetsTab = memo(function AssetsTab({ projectId, assets, onImport, onDelete, onRename, }: AssetsTabProps) { const fileInputRef = useRef(null); const [dragOver, setDragOver] = useState(false); const [copiedPath, setCopiedPath] = useState(null); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setDragOver(false); if (e.dataTransfer.files.length) onImport?.(e.dataTransfer.files); }, [onImport], ); const handleCopyPath = useCallback(async (path: string) => { const copied = await copyTextToClipboard(path); if (copied) { setCopiedPath(path); setTimeout(() => setCopiedPath(null), 1500); } }, []); const mediaAssets = assets.filter((a) => MEDIA_EXT.test(a)); return (
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} > {/* Import button */} {onImport && (
{ if (e.target.files?.length) { onImport(e.target.files); e.target.value = ""; } }} />
)} {/* Asset list */}
{mediaAssets.length === 0 ? (

Drop media files here

) : ( mediaAssets.map((asset) => ( )) )}
); });