diff --git a/apps/web/src/components/editor/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline-toolbar.tsx new file mode 100644 index 0000000..5146479 --- /dev/null +++ b/apps/web/src/components/editor/timeline-toolbar.tsx @@ -0,0 +1,219 @@ +"use client"; + +import type { TrackType } from "@/types/timeline"; +import { + ArrowLeftToLine, + ArrowRightToLine, + Copy, + Pause, + Play, + Scissors, + Snowflake, + SplitSquareHorizontal, + Trash2, +} from "lucide-react"; +import { Button } from "../ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "../ui/tooltip"; + +interface TimelineToolbarProps { + isPlaying: boolean; + currentTime: number; + duration: number; + speed: number; + tracks: any[]; + toggle: () => void; + setSpeed: (speed: number) => void; + addTrack: (type: TrackType) => string; + addClipToTrack: (trackId: string, clip: any) => void; + handleSplitSelected: () => void; + handleDuplicateSelected: () => void; + handleFreezeSelected: () => void; + handleDeleteSelected: () => void; +} + +export function TimelineToolbar({ + isPlaying, + currentTime, + duration, + speed, + tracks, + toggle, + setSpeed, + addTrack, + addClipToTrack, + handleSplitSelected, + handleDuplicateSelected, + handleFreezeSelected, + handleDeleteSelected, +}: TimelineToolbarProps) { + return ( +
+ + {/* 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 + + +
+ ); +} diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index ae6b98d..4de290a 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -5,14 +5,9 @@ import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store"; import { useTimelineStore, type TimelineTrack } from "@/stores/timeline-store"; import { - ArrowLeftToLine, - ArrowRightToLine, Copy, MoreVertical, - Pause, - Play, Scissors, - Snowflake, SplitSquareHorizontal, Trash2, Volume2, @@ -22,23 +17,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { Button } from "../ui/button"; import { ScrollArea } from "../ui/scroll-area"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; - -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "../ui/tooltip"; import AudioWaveform from "./audio-waveform"; - +import { TimelineToolbar } from "./timeline-toolbar"; export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. @@ -71,7 +52,6 @@ export function Timeline() { speed, } = usePlaybackStore(); const [isDragOver, setIsDragOver] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); const [zoomLevel, setZoomLevel] = useState(1); const dragCounterRef = useRef(0); const timelineRef = useRef(null); @@ -217,7 +197,7 @@ export function Timeline() { const bx2 = clamp(x2, 0, rect.width); const by1 = clamp(y1, 0, rect.height); const by2 = clamp(y2, 0, rect.height); - let newSelection: { trackId: string; clipId: string; }[] = []; + let newSelection: { trackId: string; clipId: string }[] = []; tracks.forEach((track, trackIdx) => { track.clips.forEach((clip) => { const effectiveDuration = clip.duration - clip.trimStart - clip.trimEnd; @@ -335,8 +315,6 @@ 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); try { const processedItems = await processMediaFiles(e.dataTransfer.files); for (const processedItem of processedItems) { @@ -364,8 +342,6 @@ 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); } } }; @@ -570,162 +546,21 @@ 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 */}
@@ -781,17 +616,19 @@ export function Timeline() { return (
{(() => { const formatTime = (seconds: number) => { @@ -852,12 +689,13 @@ export function Timeline() { >
{track.name} @@ -1197,7 +1035,7 @@ function TimelineTrackContent({ }); }; - const updateTrimFromMouseMove = (e: { clientX: number; }) => { + const updateTrimFromMouseMove = (e: { clientX: number }) => { if (!resizing) return; const clip = track.clips.find((c) => c.id === resizing.clipId); @@ -1632,18 +1470,18 @@ function TimelineTrackContent({ } if (mediaItem.type === "audio") { - return ( -
-
- + return ( +
+
+ +
-
- ); - } + ); + } // Fallback for videos without thumbnails return ( @@ -1681,12 +1519,13 @@ function TimelineTrackContent({ return (
{ e.preventDefault(); // Only show track menu if we didn't click on a clip @@ -1710,12 +1549,13 @@ function TimelineTrackContent({
{track.clips.length === 0 ? (
{isDropping ? wouldOverlap @@ -1860,4 +1700,3 @@ function TimelineTrackContent({
); } - diff --git a/apps/web/src/stores/timeline-store.ts b/apps/web/src/stores/timeline-store.ts index 3b068d7..4018c30 100644 --- a/apps/web/src/stores/timeline-store.ts +++ b/apps/web/src/stores/timeline-store.ts @@ -1,4 +1,5 @@ import { create } from "zustand"; +import type { TrackType } from "@/types/timeline"; export interface TimelineClip { id: string; @@ -13,7 +14,7 @@ export interface TimelineClip { export interface TimelineTrack { id: string; name: string; - type: "video" | "audio" | "effects"; + type: TrackType; clips: TimelineClip[]; muted?: boolean; } @@ -52,7 +53,7 @@ interface TimelineStore { endDrag: () => void; // Actions - addTrack: (type: "video" | "audio" | "effects") => string; + addTrack: (type: TrackType) => string; removeTrack: (trackId: string) => void; addClipToTrack: (trackId: string, clip: Omit) => void; removeClipFromTrack: (trackId: string, clipId: string) => void; diff --git a/apps/web/src/types/timeline.ts b/apps/web/src/types/timeline.ts index 21be520..7fafc18 100644 --- a/apps/web/src/types/timeline.ts +++ b/apps/web/src/types/timeline.ts @@ -1,5 +1,7 @@ import { TimelineTrack, TimelineClip } from "@/stores/timeline-store"; +export type TrackType = "video" | "audio" | "effects"; + export interface TimelineClipProps { clip: TimelineClip; track: TimelineTrack;