diff options
| author | Hyteq <DerpsDer.es@gmail.com> | 2025-06-23 09:02:32 +0300 |
|---|---|---|
| committer | Hyteq <DerpsDer.es@gmail.com> | 2025-06-23 09:02:32 +0300 |
| commit | c2b70c13e988fba76a7d9bc6ddebeeab82eb5ed9 (patch) | |
| tree | a8f61e5ebcc3fa1ed4b641a9fa216acca2c2356c /apps | |
| parent | 2bb347f1ae570e3b2b4e5da3de7787e754d49f67 (diff) | |
| download | opencut-c2b70c13e988fba76a7d9bc6ddebeeab82eb5ed9.tar.gz | |
chore: cleaned up logic, comments, logic and overall structure of media panel to be more robust
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/src/components/editor/media-panel.tsx | 287 |
1 files changed, 118 insertions, 169 deletions
diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx index 17fe0417..f38e0f4f 100644 --- a/apps/web/src/components/editor/media-panel.tsx +++ b/apps/web/src/components/editor/media-panel.tsx @@ -16,17 +16,17 @@ export function MediaPanel() { const [isProcessing, setIsProcessing] = useState(false); const processFiles = async (files: FileList | File[]) => { - setIsProcessing(true); + if (!files?.length) return; + setIsProcessing(true); try { - const processedItems = await processMediaFiles(files); - - for (const processedItem of processedItems) { - addMediaItem(processedItem); - toast.success(`Added ${processedItem.name} to project`); - } + const items = await processMediaFiles(files); + items.forEach(item => { + addMediaItem(item); + toast.success(`Added ${item.name}`); + }); } catch (error) { - console.error("Error processing files:", error); + console.error("File processing failed:", error); toast.error("Failed to process files"); } finally { setIsProcessing(false); @@ -34,82 +34,64 @@ export function MediaPanel() { }; const { isDragOver, dragProps } = useDragDrop({ - onDrop: (files) => { - processFiles(files); - }, + onDrop: processFiles, }); - const handleFileSelect = () => { - fileInputRef.current?.click(); - }; + const handleFileSelect = () => fileInputRef.current?.click(); - const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { - if (e.target.files && e.target.files.length > 0) { - processFiles(e.target.files); - // Reset the input so the same file can be selected again - e.target.value = ""; - } + const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { + if (e.target.files) processFiles(e.target.files); + e.target.value = ""; }; - const handleRemoveItem = (e: React.MouseEvent, itemId: string) => { + const handleRemove = (e: React.MouseEvent, id: string) => { e.stopPropagation(); - removeMediaItem(itemId); - toast.success("Media removed from project"); + removeMediaItem(id); + toast.success("Media removed"); }; - const handleDragStart = (e: React.DragEvent, item: any) => { - // Mark this as an internal app drag - e.dataTransfer.setData( - "application/x-media-item", - JSON.stringify({ - id: item.id, - type: item.type, - name: item.name, - }) - ); - e.dataTransfer.effectAllowed = "copy"; + const formatDuration = (duration: number) => { + const min = Math.floor(duration / 60); + const sec = Math.floor(duration % 60); + return `${min}:${sec.toString().padStart(2, "0")}`; }; - const formatDuration = (duration: number) => { - const minutes = Math.floor(duration / 60); - const seconds = Math.floor(duration % 60); - return `${minutes}:${seconds.toString().padStart(2, "0")}`; + const startDrag = (e: React.DragEvent, item: any) => { + e.dataTransfer.setData("application/x-media-item", JSON.stringify({ + id: item.id, + type: item.type, + name: item.name, + })); + e.dataTransfer.effectAllowed = "copy"; }; - const getMediaIcon = (type: string) => { - switch (type) { - case "video": - return <Video className="h-4 w-4" />; - case "audio": - return <Music className="h-4 w-4" />; - default: - return <Image className="h-4 w-4" />; + const renderPreview = (item: any) => { + const baseDragProps = { + draggable: true, + onDragStart: (e: React.DragEvent) => startDrag(e, item), + }; + + if (item.type === "image") { + return ( + <img + src={item.url} + alt={item.name} + className="w-full h-full object-cover rounded cursor-grab active:cursor-grabbing" + loading="lazy" + {...baseDragProps} + /> + ); } - }; - const renderMediaPreview = (item: any) => { - switch (item.type) { - case "image": + if (item.type === "video") { + if (item.thumbnailUrl) { return ( - <img - src={item.url} - alt={item.name} - className="w-full h-full object-cover rounded cursor-grab active:cursor-grabbing" - loading="lazy" - draggable={true} - onDragStart={(e) => handleDragStart(e, item)} - /> - ); - case "video": - return item.thumbnailUrl ? ( - <div className="relative w-full h-full"> + <div className="relative w-full h-full cursor-grab active:cursor-grabbing" {...baseDragProps}> <img src={item.thumbnailUrl} alt={item.name} - className="w-full h-full object-cover rounded cursor-grab active:cursor-grabbing" + className="w-full h-full object-cover rounded" loading="lazy" - draggable={true} - onDragStart={(e) => handleDragStart(e, item)} /> <div className="absolute inset-0 flex items-center justify-center bg-black/20 rounded"> <Video className="h-6 w-6 text-white drop-shadow-md" /> @@ -120,49 +102,37 @@ export function MediaPanel() { </div> )} </div> - ) : ( - <div - className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded cursor-grab active:cursor-grabbing" - draggable={true} - onDragStart={(e) => handleDragStart(e, item)} - > - <Video className="h-6 w-6 mb-1" /> - <span className="text-xs">Video</span> - {item.duration && ( - <span className="text-xs opacity-70"> - {formatDuration(item.duration)} - </span> - )} - </div> - ); - case "audio": - return ( - <div - className="w-full h-full bg-gradient-to-br from-green-500/20 to-emerald-500/20 flex flex-col items-center justify-center text-muted-foreground rounded border border-green-500/20 cursor-grab active:cursor-grabbing" - draggable={true} - onDragStart={(e) => handleDragStart(e, item)} - > - <Music className="h-6 w-6 mb-1" /> - <span className="text-xs">Audio</span> - {item.duration && ( - <span className="text-xs opacity-70"> - {formatDuration(item.duration)} - </span> - )} - </div> - ); - default: - return ( - <div - className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded cursor-grab active:cursor-grabbing" - draggable={true} - onDragStart={(e) => handleDragStart(e, item)} - > - {getMediaIcon(item.type)} - <span className="text-xs mt-1">Unknown</span> - </div> ); + } + return ( + <div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded cursor-grab active:cursor-grabbing" {...baseDragProps}> + <Video className="h-6 w-6 mb-1" /> + <span className="text-xs">Video</span> + {item.duration && ( + <span className="text-xs opacity-70">{formatDuration(item.duration)}</span> + )} + </div> + ); + } + + if (item.type === "audio") { + return ( + <div className="w-full h-full bg-gradient-to-br from-green-500/20 to-emerald-500/20 flex flex-col items-center justify-center text-muted-foreground rounded border border-green-500/20 cursor-grab active:cursor-grabbing" {...baseDragProps}> + <Music className="h-6 w-6 mb-1" /> + <span className="text-xs">Audio</span> + {item.duration && ( + <span className="text-xs opacity-70">{formatDuration(item.duration)}</span> + )} + </div> + ); } + + return ( + <div className="w-full h-full bg-muted/30 flex flex-col items-center justify-center text-muted-foreground rounded cursor-grab active:cursor-grabbing" {...baseDragProps}> + <Image className="h-6 w-6" /> + <span className="text-xs mt-1">Unknown</span> + </div> + ); }; return ( @@ -173,51 +143,71 @@ export function MediaPanel() { accept="image/*,video/*,audio/*" multiple className="hidden" - onChange={handleFileInputChange} + onChange={handleFileChange} /> <div - className={`h-full overflow-y-auto transition-colors duration-200 relative ${ - isDragOver ? "bg-accent/30 border-accent" : "" - }`} + className={`h-full flex flex-col transition-colors relative ${isDragOver ? "bg-accent/30" : "" + }`} {...dragProps} > <DragOverlay isVisible={isDragOver} /> - <div className="space-y-4 p-2 h-full"> - {/* Media Grid */} + <div className="p-2 border-b"> + <Button + variant="outline" + size="sm" + className="w-full" + onClick={handleFileSelect} + disabled={isProcessing} + > + {isProcessing ? ( + <> + <Upload className="h-4 w-4 mr-2 animate-spin" /> + Processing... + </> + ) : ( + <> + <Plus className="h-4 w-4 mr-2" /> + Add Media + </> + )} + </Button> + </div> + + <div className="flex-1 overflow-y-auto p-2"> {mediaItems.length === 0 ? ( - <EmptyMedia - onFileSelect={handleFileSelect} - isProcessing={isProcessing} - /> + <div className="flex flex-col items-center justify-center py-8 text-center h-full"> + <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> + <Image className="h-8 w-8 text-muted-foreground" /> + </div> + <p className="text-sm text-muted-foreground">No media in project</p> + <p className="text-xs text-muted-foreground/70 mt-1"> + Drag files here or use the button above + </p> + </div> ) : ( <div className="grid grid-cols-2 gap-2"> {mediaItems.map((item) => ( <div key={item.id} className="relative group"> <Button variant="outline" - className="flex flex-col gap-2 p-2 h-auto overflow-hidden w-full relative" + className="flex flex-col gap-2 p-2 h-auto w-full relative" > - <AspectRatio ratio={item.aspectRatio} className="w-full"> - {renderMediaPreview(item)} + <AspectRatio ratio={item.aspectRatio}> + {renderPreview(item)} </AspectRatio> - <span className="text-xs truncate max-w-full px-1"> + <span className="text-xs truncate px-1"> {item.name} </span> </Button> - {/* Remove button - positioned outside the button container */} - <div - className="absolute -top-2 -right-2 opacity-0 group-hover:opacity-100 transition-opacity z-20" - onDragStart={(e) => e.preventDefault()} - onDrag={(e) => e.preventDefault()} - > + <div className="absolute -top-2 -right-2 opacity-0 group-hover:opacity-100 transition-opacity"> <Button variant="destructive" size="icon" - className="h-6 w-6 pointer-events-auto" - onClick={(e) => handleRemoveItem(e, item.id)} + className="h-6 w-6" + onClick={(e) => handleRemove(e, item.id)} > <Trash2 className="h-3 w-3" /> </Button> @@ -231,44 +221,3 @@ export function MediaPanel() { </> ); } - -function EmptyMedia({ - onFileSelect, - isProcessing, -}: { - onFileSelect: () => void; - isProcessing: boolean; -}) { - return ( - <div className="flex flex-col items-center justify-center py-8 text-center h-full"> - <div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4"> - {isProcessing ? ( - <div className="animate-spin"> - <Upload className="h-8 w-8 text-muted-foreground" /> - </div> - ) : ( - <Image className="h-8 w-8 text-muted-foreground" /> - )} - </div> - <p className="text-sm text-muted-foreground"> - {isProcessing ? "Processing files..." : "No media in project"} - </p> - <p className="text-xs text-muted-foreground/70 mt-1"> - {isProcessing - ? "Please wait while files are being processed" - : "Drag files or click to add media"} - </p> - {!isProcessing && ( - <Button - variant="outline" - size="sm" - className="mt-4" - onClick={onFileSelect} - > - <Plus className="h-4 w-4 mr-2" /> - Add Media - </Button> - )} - </div> - ); -} |
