From 2aa37920d5f183f9c0de2c42076fa57c5b48320e Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Tue, 24 Jun 2025 17:51:34 +0200 Subject: refactor: overhaul timeline component, separate into different files --- .../editor/timeline/timeline-toolbar.tsx | 296 +++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 apps/web/src/components/editor/timeline/timeline-toolbar.tsx (limited to 'apps/web/src/components/editor/timeline/timeline-toolbar.tsx') diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx new file mode 100644 index 00000000..3778ccf8 --- /dev/null +++ b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx @@ -0,0 +1,296 @@ +"use client"; + +import { Button } from "../../ui/button"; +import { + Scissors, + ArrowLeftToLine, + ArrowRightToLine, + Trash2, + Snowflake, + Copy, + SplitSquareHorizontal, + Pause, + Play, +} from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, + TooltipProvider, +} from "../../ui/tooltip"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../../ui/select"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { usePlaybackStore } from "@/stores/playback-store"; +import { toast } from "sonner"; + +interface TimelineToolbarProps {} + +export function TimelineToolbar({}: TimelineToolbarProps) { + const { + tracks, + addTrack, + addClipToTrack, + removeClipFromTrack, + selectedClips, + clearSelectedClips, + updateClipTrim, + } = useTimelineStore(); + const { currentTime, duration, isPlaying, toggle, setSpeed, speed } = + usePlaybackStore(); + + const handleSplitSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + 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 splitTime = currentTime; + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + if (splitTime > effectiveStart && splitTime < effectiveEnd) { + updateClipTrim( + track.id, + clip.id, + clip.trimStart, + clip.trimEnd + (effectiveEnd - splitTime) + ); + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (split)", + duration: clip.duration, + startTime: splitTime, + trimStart: clip.trimStart + (splitTime - effectiveStart), + trimEnd: clip.trimEnd, + }); + } + } + }); + toast.success("Split selected clip(s)"); + }; + + const handleDuplicateSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + selectedClips.forEach(({ trackId, clipId }) => { + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + if (clip && track) { + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (copy)", + duration: clip.duration, + startTime: + clip.startTime + + (clip.duration - clip.trimStart - clip.trimEnd) + + 0.1, + trimStart: clip.trimStart, + trimEnd: clip.trimEnd, + }); + } + }); + toast.success("Duplicated selected clip(s)"); + }; + + const handleFreezeSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + selectedClips.forEach(({ trackId, clipId }) => { + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + if (clip && track) { + // Add a new freeze frame clip at the playhead + addClipToTrack(track.id, { + mediaId: clip.mediaId, + name: clip.name + " (freeze)", + duration: 1, // 1 second freeze frame + startTime: currentTime, + trimStart: 0, + trimEnd: clip.duration - 1, + }); + } + }); + toast.success("Freeze frame added for selected clip(s)"); + }; + + const handleDeleteSelected = () => { + if (selectedClips.length === 0) { + toast.error("No clips selected"); + return; + } + selectedClips.forEach(({ trackId, clipId }) => { + removeClipFromTrack(trackId, clipId); + }); + clearSelectedClips(); + toast.success("Deleted selected clip(s)"); + }; + + return ( +
+ + {/* Play/Pause Button */} + + + + + + {isPlaying ? "Pause (Space)" : "Play (Space)"} + + + +
+ + {/* Time Display */} +
+ {Math.floor(currentTime * 10) / 10}s /{" "} + {Math.floor(duration * 10) / 10}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 + + +
+ ); +} -- cgit v1.3.1