From 9abb502919135782cc51e82fa8a7154a7fc95a80 Mon Sep 17 00:00:00 2001 From: Sompalkar Date: Mon, 23 Jun 2025 14:43:12 +0530 Subject: fix: delete only selected clip from timeline --- apps/web/src/components/editor/media-panel.tsx | 78 ++- apps/web/src/components/editor/timeline.tsx | 805 ++++++++++++++++++++----- 2 files changed, 713 insertions(+), 170 deletions(-) (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/media-panel.tsx b/apps/web/src/components/editor/media-panel.tsx index ab2e66e2..0d6a1f1c 100644 --- a/apps/web/src/components/editor/media-panel.tsx +++ b/apps/web/src/components/editor/media-panel.tsx @@ -10,21 +10,28 @@ import { useDragDrop } from "@/hooks/use-drag-drop"; import { useRef, useState } from "react"; import { toast } from "sonner"; +// MediaPanel lets users add, view, and drag media (images, videos, audio) into the project. +// You can upload files or drag them from your computer. Dragging from here to the timeline adds them to your video project. + export function MediaPanel() { const { mediaItems, addMediaItem, removeMediaItem } = useMediaStore(); const fileInputRef = useRef(null); const [isProcessing, setIsProcessing] = useState(false); const processFiles = async (files: FileList | File[]) => { + // If no files, do nothing if (!files?.length) return; setIsProcessing(true); try { + // Process files (extract metadata, generate thumbnails, etc.) const items = await processMediaFiles(files); - items.forEach(item => { + // Add each processed media item to the store + items.forEach((item) => { addMediaItem(item); }); } catch (error) { + // Show error if processing fails console.error("File processing failed:", error); toast.error("Failed to process files"); } finally { @@ -33,37 +40,47 @@ export function MediaPanel() { }; const { isDragOver, dragProps } = useDragDrop({ + // When files are dropped, process them onDrop: processFiles, }); - const handleFileSelect = () => fileInputRef.current?.click(); + const handleFileSelect = () => fileInputRef.current?.click(); // Open file picker const handleFileChange = (e: React.ChangeEvent) => { + // When files are selected via file picker, process them if (e.target.files) processFiles(e.target.files); - e.target.value = ""; + e.target.value = ""; // Reset input }; const handleRemove = (e: React.MouseEvent, id: string) => { + // Remove a media item from the store e.stopPropagation(); removeMediaItem(id); }; const formatDuration = (duration: number) => { + // Format seconds as mm:ss const min = Math.floor(duration / 60); const sec = Math.floor(duration % 60); return `${min}:${sec.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, - })); + // When dragging a media item, set drag data for timeline to read + e.dataTransfer.setData( + "application/x-media-item", + JSON.stringify({ + id: item.id, + type: item.type, + name: item.name, + }) + ); e.dataTransfer.effectAllowed = "copy"; }; const renderPreview = (item: any) => { + // Render a preview for each media type (image, video, audio, unknown) + // Each preview is draggable to the timeline const baseDragProps = { draggable: true, onDragStart: (e: React.DragEvent) => startDrag(e, item), @@ -84,7 +101,10 @@ export function MediaPanel() { if (item.type === "video") { if (item.thumbnailUrl) { return ( -
+
{item.name} +
); @@ -115,18 +140,26 @@ export function MediaPanel() { if (item.type === "audio") { return ( -
+
Audio {item.duration && ( - {formatDuration(item.duration)} + + {formatDuration(item.duration)} + )}
); } return ( -
+
Unknown
@@ -135,6 +168,7 @@ export function MediaPanel() { return ( <> + {/* Hidden file input for uploading media */}
+ {/* Show overlay when dragging files over the panel */}
+ {/* Button to add/upload media */} + {/* Show remove button on hover */}
+ {clipMenuOpen === clip.id && ( +
+ + +
+ )} +
- + {/* Right trim handle */}
handleResizeStart(e, clip.id, 'right')} + onMouseDown={(e) => handleResizeStart(e, clip.id, "right")} />
); @@ -907,23 +1250,185 @@ function TimelineTrackContent({ track, zoomLevel }: { track: TimelineTrack, zoom {/* Drop position indicator */} {isDraggedOver && dropPosition !== null && (
-
-
-
- {wouldOverlap ? "⚠️" : ""}{dropPosition.toFixed(1)}s +
+
+
+ {wouldOverlap ? "⚠️" : ""} + {dropPosition.toFixed(1)}s
)} )} + + {/* Track Context Menu (for empty areas) */} + {trackContextMenu && ( +
e.preventDefault()} + > + {/* Mute/Unmute option */} + +
+ )} + + {/* Clip Context Menu (for specific clips) */} + {clipContextMenu && + track.clips.some((c) => c.id === clipContextMenu.clipId) && ( +
e.preventDefault()} + > + {/* Split option */} + + {/* Delete clip option */} + +
+ )}
); } + +// Custom context menu for track actions +function TrackContextMenu({ + x, + y, + track, + onClose, + onSplit, + onMute, + onDelete, +}: { + x: number; + y: number; + track: any; + onClose: () => void; + onSplit: () => void; + onMute: () => void; + onDelete: () => void; +}) { + // Small, modern, visually appealing popup + return ( +
e.preventDefault()} + > + {/* Split option */} + + {/* Mute/Unmute option */} + + {/* Delete option */} + +
+ ); +} + + + + + + + + + + + + + + + -- cgit v1.3.1