From c32daa4f2ec967902f2ce12d291f8034ca8bce38 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Fri, 27 Jun 2025 07:56:18 +0530 Subject: [PATCH] refactor: enhance timeline component by improving drag-and-drop functionality and clip selection handling --- apps/web/src/components/editor/timeline.tsx | 708 ++++++++------------ 1 file changed, 275 insertions(+), 433 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index b20006d..f4c2473 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1,27 +1,37 @@ "use client"; -import { processMediaFiles } from "@/lib/media-processing"; -import { useMediaStore } from "@/stores/media-store"; -import { usePlaybackStore } from "@/stores/playback-store"; -import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store"; +import { ScrollArea } from "../ui/scroll-area"; +import { Button } from "../ui/button"; import { + Scissors, ArrowLeftToLine, ArrowRightToLine, - Copy, - MoreVertical, - Pause, - Play, - Scissors, - Snowflake, - SplitSquareHorizontal, Trash2, + Snowflake, + Copy, + SplitSquareHorizontal, Volume2, VolumeX, + Pause, + Play, } from "lucide-react"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, + TooltipProvider, +} from "../ui/tooltip"; +import { + useTimelineStore, + type TimelineTrack, + type TimelineClip as TypeTimelineClip, +} from "@/stores/timeline-store"; +import { useMediaStore } from "@/stores/media-store"; +import { usePlaybackStore } from "@/stores/playback-store"; +import { useDragClip } from "@/hooks/use-drag-clip"; +import { processMediaFiles } from "@/lib/media-processing"; import { toast } from "sonner"; -import { Button } from "../ui/button"; -import { ScrollArea } from "../ui/scroll-area"; +import { useState, useRef, useEffect, useCallback } from "react"; import { Select, SelectContent, @@ -29,15 +39,8 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "../ui/tooltip"; - -import AudioWaveform from "./audio-waveform"; +import { TimelineClip } from "./timeline-clip"; +import { ContextMenuState } from "@/types/timeline"; export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. @@ -55,12 +58,12 @@ export function Timeline() { clearSelectedClips, setSelectedClips, updateClipTrim, - undo, - redo, splitClip, splitAndKeepLeft, splitAndKeepRight, separateAudio, + undo, + redo, } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); const { @@ -75,19 +78,14 @@ export function Timeline() { } = usePlaybackStore(); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); + const [progress, setProgress] = useState(0); const [zoomLevel, setZoomLevel] = useState(1); const dragCounterRef = useRef(0); const timelineRef = useRef(null); const [isInTimeline, setIsInTimeline] = useState(false); // Unified context menu state - const [contextMenu, setContextMenu] = useState<{ - type: "track" | "clip"; - trackId: string; - clipId?: string; - x: number; - y: number; - } | null>(null); + const [contextMenu, setContextMenu] = useState(null); // Marquee selection state const [marquee, setMarquee] = useState<{ @@ -340,8 +338,12 @@ export function Timeline() { } else if (e.dataTransfer.files?.length > 0) { // Handle file drops by creating new tracks setIsProcessing(true); + setProgress(0); try { - const processedItems = await processMediaFiles(e.dataTransfer.files); + const processedItems = await processMediaFiles( + e.dataTransfer.files, + (p) => setProgress(p) + ); for (const processedItem of processedItems) { addMediaItem(processedItem); const currentMediaItems = useMediaStore.getState().mediaItems; @@ -369,6 +371,7 @@ export function Timeline() { toast.error("Failed to process dropped files"); } finally { setIsProcessing(false); + setProgress(0); } } }; @@ -456,7 +459,6 @@ export function Timeline() { toast.error("No clips selected"); return; } - let splitCount = 0; selectedClips.forEach(({ trackId, clipId }) => { const track = tracks.find((t) => t.id === trackId); @@ -472,7 +474,6 @@ export function Timeline() { } } }); - if (splitCount > 0) { toast.success(`Split ${splitCount} clip(s) at playhead`); } else { @@ -527,29 +528,27 @@ export function Timeline() { }); toast.success("Freeze frame added for selected clip(s)"); }; - const handleSplitAndKeepLeft = () => { if (selectedClips.length === 0) { toast.error("No clips selected"); return; } - + let splitCount = 0; selectedClips.forEach(({ trackId, clipId }) => { const track = tracks.find((t) => t.id === trackId); const clip = track?.clips.find((c) => c.id === clipId); if (clip && track) { const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - + const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + if (currentTime > effectiveStart && currentTime < effectiveEnd) { splitAndKeepLeft(trackId, clipId, currentTime); splitCount++; } } }); - + if (splitCount > 0) { toast.success(`Split and kept left portion of ${splitCount} clip(s)`); } else { @@ -562,23 +561,22 @@ export function Timeline() { toast.error("No clips selected"); return; } - + let splitCount = 0; selectedClips.forEach(({ trackId, clipId }) => { const track = tracks.find((t) => t.id === trackId); const clip = track?.clips.find((c) => c.id === clipId); if (clip && track) { const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - + const effectiveEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + if (currentTime > effectiveStart && currentTime < effectiveEnd) { splitAndKeepRight(trackId, clipId, currentTime); splitCount++; } } }); - + if (splitCount > 0) { toast.success(`Split and kept right portion of ${splitCount} clip(s)`); } else { @@ -591,31 +589,25 @@ export function Timeline() { toast.error("No clips selected"); return; } - + let separatedCount = 0; selectedClips.forEach(({ trackId, clipId }) => { const track = tracks.find((t) => t.id === trackId); const clip = track?.clips.find((c) => c.id === clipId); const mediaItem = mediaItems.find((item) => item.id === clip?.mediaId); - - if ( - clip && - track && - mediaItem?.type === "video" && - track.type === "video" - ) { + + if (clip && track && mediaItem?.type === "video" && track.type === "video") { const audioClipId = separateAudio(trackId, clipId); if (audioClipId) separatedCount++; } }); - + if (separatedCount > 0) { toast.success(`Separated audio from ${separatedCount} video clip(s)`); } else { toast.error("Select video clips to separate audio"); } }; - const handleDeleteSelected = () => { if (selectedClips.length === 0) { toast.error("No clips selected"); @@ -722,7 +714,6 @@ export function Timeline() {
- {/* Clip editing operations */} @@ -747,11 +734,7 @@ export function Timeline() { - @@ -1021,6 +1004,16 @@ export function Timeline() { y: e.clientY, }); }} + onClick={(e) => { + // If clicking empty area (not on a clip), deselect all clips + if ( + !(e.target as HTMLElement).closest(".timeline-clip") + ) { + const { clearSelectedClips } = + useTimelineStore.getState(); + clearSelectedClips(); + } + }} > )} {isDragOver && ( -
-
Drop media here to add a new track
+
+
+ {isProcessing + ? `Processing ${progress}%` + : "Drop media here to add to timeline"} +
)}
@@ -1081,22 +1072,17 @@ export function Timeline() { setContextMenu(null); }} > - {(() => { - const track = tracks.find( - (t) => t.id === contextMenu.trackId - ); - return track?.muted ? ( - <> - - Unmute Track - - ) : ( - <> - - Mute Track - - ); - })()} + {contextMenu.trackId ? ( +
+ + Unmute Track +
+ ) : ( +
+ + Mute Track +
+ )}
- {clipMenuOpen === clip.id && ( -
- - -
- )} -
-
- {/* Right trim handle */} -
handleResizeStart(e, clip.id, "right")} - /> -
+ clip={clip} + track={track} + zoomLevel={zoomLevel} + isSelected={isSelected} + onContextMenu={handleClipContextMenu} + onClipMouseDown={handleClipMouseDown} + onClipClick={handleClipClick} + /> ); })} - - {/* Drop position indicator */} - {isDraggedOver && dropPosition !== null && ( -
-
-
-
- {wouldOverlap ? "⚠️" : ""} - {dropPosition.toFixed(1)}s -
-
- )} )}