From 87e90a5e24b6fbfe37be7d057617411071b56abf Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 27 Jun 2025 03:50:39 +0200 Subject: [PATCH] fix dragging clips in timeline --- apps/web/src/components/editor/timeline.tsx | 801 +++++++++++--------- 1 file changed, 429 insertions(+), 372 deletions(-) diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 7e63e68..93f722b 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -1,25 +1,46 @@ "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 { - Copy, - MoreVertical, Scissors, - SplitSquareHorizontal, + ArrowLeftToLine, + ArrowRightToLine, 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 AudioWaveform from "./audio-waveform"; -import { TimelineToolbar } from "./timeline-toolbar"; +import { useState, useRef, useEffect, useCallback } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { TimelineClip } from "./timeline-clip"; +import { ContextMenuState } from "@/types/timeline"; export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. @@ -52,19 +73,15 @@ export function Timeline() { speed, } = 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<{ @@ -315,8 +332,14 @@ export function Timeline() { toast.error("Failed to add media to 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; @@ -342,6 +365,9 @@ export function Timeline() { // Show error if file processing fails console.error("Error processing external files:", error); toast.error("Failed to process dropped files"); + } finally { + setIsProcessing(false); + setProgress(0); } } }; @@ -546,21 +572,163 @@ export function Timeline() { onMouseLeave={() => setIsInTimeline(false)} onWheel={handleWheel} > - + {/* Toolbar */} +
+ + {/* Play/Pause Button */} + + + + + + {isPlaying ? "Pause (Space)" : "Play (Space)"} + + + +
+ + {/* Time Display */} +
+ {currentTime.toFixed(1)}s / {duration.toFixed(1)}s +
+ + {/* Test Clip Button - for debugging */} + {tracks.length === 0 && ( + <> +
+ + + + + Add a test clip to try playback + + + )} + +
+ + + + + + Split clip (S) + + + + + + + Split and keep left (A) + + + + + + + Split and keep right (D) + + + + + + + Separate audio (E) + + + + + + + Duplicate clip (Ctrl+D) + + + + + + + Freeze frame (F) + + + + + + + Delete clip (Delete) + + +
+ + {/* Speed Control */} + + + + + Playback Speed + + +
{/* Timeline Container */}
@@ -760,6 +928,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"} +
)}
@@ -820,22 +996,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 +
+ )}