From 7bf0984698e43b9aa2dd879f65951318ed410390 Mon Sep 17 00:00:00 2001 From: Maze <167211895+mazeincoding@users.noreply.github.com> Date: Sat, 31 Jan 2026 00:20:04 +0100 Subject: codebase overhaul (#697) --- .../editor/timeline/timeline-toolbar.tsx | 696 ++++++++++----------- 1 file changed, 327 insertions(+), 369 deletions(-) (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 index 80c6ce08..822bb438 100644 --- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx +++ b/apps/web/src/components/editor/timeline/timeline-toolbar.tsx @@ -1,397 +1,355 @@ -import { usePlaybackStore } from "@/stores/playback-store"; -import { useProjectStore } from "@/stores/project-store"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { useSceneStore } from "@/stores/scene-store"; -import { toast } from "sonner"; +import { useEditor } from "@/hooks/use-editor"; import { - TooltipProvider, - Tooltip, - TooltipTrigger, - TooltipContent, + TooltipProvider, + Tooltip, + TooltipTrigger, + TooltipContent, } from "@/components/ui/tooltip"; import { Button } from "@/components/ui/button"; +import { SkipBack, SplitSquareHorizontal } from "lucide-react"; import { - Pause, - Play, - SkipBack, - Bookmark, - Magnet, - Link, - ZoomOut, - ZoomIn, - Copy, - Trash2, - Snowflake, - ArrowLeftToLine, - ArrowRightToLine, - SplitSquareHorizontal, - Scissors, - LayersIcon, -} from "lucide-react"; -import { - SplitButton, - SplitButtonLeft, - SplitButtonRight, - SplitButtonSeparator, + SplitButton, + SplitButtonLeft, + SplitButtonRight, + SplitButtonSeparator, } from "@/components/ui/split-button"; import { Slider } from "@/components/ui/slider"; -import { DEFAULT_FPS } from "@/stores/project-store"; import { formatTimeCode } from "@/lib/time"; import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants"; -import { EditableTimecode } from "@/components/ui/editable-timecode"; +import { EditableTimecode } from "@/components/editable-timecode"; import { ScenesView } from "../scenes-view"; +import { type TAction, invokeAction } from "@/lib/actions"; +import { cn } from "@/utils/ui"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Bookmark02Icon, + Delete02Icon, + SnowIcon, + ScissorIcon, + MagnetIcon, + Link04Icon, + SearchAddIcon, + SearchMinusIcon, + PauseIcon, + PlayIcon, + Copy01Icon, + AlignLeftIcon, + AlignRightIcon, + Layers01Icon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; export function TimelineToolbar({ - zoomLevel, - setZoomLevel, + zoomLevel, + minZoom, + setZoomLevel, }: { - zoomLevel: number; - setZoomLevel: (zoom: number) => void; + zoomLevel: number; + minZoom: number; + setZoomLevel: ({ zoom }: { zoom: number }) => void; }) { - const { - tracks, - addTrack, - addElementToTrack, - selectedElements, - clearSelectedElements, - deleteSelected, - splitSelected, - splitAndKeepLeft, - splitAndKeepRight, - separateAudio, - snappingEnabled, - toggleSnapping, - rippleEditingEnabled, - toggleRippleEditing, - } = useTimelineStore(); - const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore(); - const { toggleBookmark, isBookmarked, activeProject } = useProjectStore(); - const { scenes, currentScene } = useSceneStore(); + const handleZoom = ({ direction }: { direction: "in" | "out" }) => { + const newZoomLevel = + direction === "in" + ? Math.min( + TIMELINE_CONSTANTS.ZOOM_MAX, + zoomLevel + TIMELINE_CONSTANTS.ZOOM_STEP, + ) + : Math.max(minZoom, zoomLevel - TIMELINE_CONSTANTS.ZOOM_STEP); + setZoomLevel({ zoom: newZoomLevel }); + }; + + return ( + +
+ + + + + setZoomLevel({ zoom })} + onZoom={handleZoom} + /> +
+
+ ); +} + +function ToolbarLeftSection() { + const editor = useEditor(); + const currentTime = editor.playback.getCurrentTime(); + const isPlaying = editor.playback.getIsPlaying(); + const currentBookmarked = editor.scenes.isBookmarked({ time: currentTime }); + + const handleAction = ({ + action, + event, + }: { + action: TAction; + event: React.MouseEvent; + }) => { + event.stopPropagation(); + invokeAction(action); + }; + + return ( +
+ + + ) : ( + + ) + } + tooltip={isPlaying ? "Pause" : "Play"} + onClick={({ event }) => + handleAction({ action: "toggle-play", event }) + } + /> + + } + tooltip="Go to start" + onClick={({ event }) => handleAction({ action: "goto-start", event })} + /> + +
+ + - const handleSplitSelected = () => { - splitSelected(currentTime); - }; +
- const handleDuplicateSelected = () => { - if (selectedElements.length === 0) return; - const canDuplicate = selectedElements.length === 1; - if (!canDuplicate) return; + } + tooltip="Split element" + onClick={({ event }) => + handleAction({ action: "split", event }) + } + /> - selectedElements.forEach(({ trackId, elementId }) => { - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((el) => el.id === elementId); - if (element) { - const newStartTime = - element.startTime + - (element.duration - element.trimStart - element.trimEnd) + - 0.1; - const { id, ...elementWithoutId } = element; - addElementToTrack(trackId, { - ...elementWithoutId, - startTime: newStartTime, - }); - } - }); - clearSelectedElements(); - }; + } + tooltip="Split left" + onClick={({ event }) => + handleAction({ action: "split-left", event }) + } + /> - const handleFreezeSelected = () => { - toast.info("Freeze frame functionality coming soon!"); - }; + } + tooltip="Split right" + onClick={({ event }) => + handleAction({ action: "split-right", event }) + } + /> - const handleSplitAndKeepLeft = () => { - if (selectedElements.length !== 1) { - toast.error("Select exactly one element"); - return; - } - const { trackId, elementId } = selectedElements[0]; - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); - if (!element) return; - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected element"); - return; - } - splitAndKeepLeft(trackId, elementId, currentTime); - }; + } + tooltip="Coming soon" /* separate audio */ + disabled={true} + onClick={({ event: _event }) => {}} + /> - const handleSplitAndKeepRight = () => { - if (selectedElements.length !== 1) { - toast.error("Select exactly one element"); - return; - } - const { trackId, elementId } = selectedElements[0]; - const track = tracks.find((t) => t.id === trackId); - const element = track?.elements.find((c) => c.id === elementId); - if (!element) return; - const effectiveStart = element.startTime; - const effectiveEnd = - element.startTime + - (element.duration - element.trimStart - element.trimEnd); - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected element"); - return; - } - splitAndKeepRight(trackId, elementId, currentTime); - }; + } + tooltip="Duplicate element" + onClick={({ event }) => + handleAction({ action: "duplicate-selected", event }) + } + /> - const handleSeparateAudio = () => { - if (selectedElements.length !== 1) { - toast.error("Select exactly one media element to separate audio"); - return; - } - const { trackId, elementId } = selectedElements[0]; - const track = tracks.find((t) => t.id === trackId); - if (!track || track.type !== "media") { - toast.error("Select a media element to separate audio"); - return; - } - separateAudio(trackId, elementId); - }; + } + tooltip="Coming soon" /* freeze frame */ + disabled={true} + onClick={({ event: _event }) => {}} + /> - const handleDeleteSelected = () => { - deleteSelected(); - }; + } + tooltip="Delete element" + onClick={({ event }) => + handleAction({ action: "delete-selected", event }) + } + /> - const handleZoomIn = () => { - setZoomLevel(Math.min(4, zoomLevel + 0.25)); - }; +
- const handleZoomOut = () => { - setZoomLevel(Math.max(0.25, zoomLevel - 0.25)); - }; + + + } + tooltip={currentBookmarked ? "Remove bookmark" : "Add bookmark"} + onClick={({ event }) => + handleAction({ action: "toggle-bookmark", event }) + } + /> + + +
+ ); +} + +function TimeDisplay() { + const editor = useEditor(); + const currentTime = editor.playback.getCurrentTime(); + const totalDuration = editor.timeline.getTotalDuration(); + const fps = editor.project.getActive().settings.fps; + + return ( +
+ editor.playback.seek({ time })} + className="text-center" + /> +
/
+
+ {formatTimeCode({ + timeInSeconds: totalDuration, + format: "HH:MM:SS:FF", + fps, + })} +
+
+ ); +} + +function SceneSelector() { + const editor = useEditor(); + const currentScene = editor.scenes.getActiveScene(); + + return ( +
+ + {currentScene?.name || "No Scene"} + + + {}} type="button"> + + + + +
+ ); +} - const handleZoomSliderChange = (values: number[]) => { - setZoomLevel(values[0]); - }; +function ToolbarRightSection({ + zoomLevel, + minZoom, + onZoomChange, + onZoom, +}: { + zoomLevel: number; + minZoom: number; + onZoomChange: (zoom: number) => void; + onZoom: (options: { direction: "in" | "out" }) => void; +}) { + const { + snappingEnabled, + rippleEditingEnabled, + toggleSnapping, + toggleRippleEditing, + } = useTimelineStore(); - const handleToggleBookmark = async () => { - await toggleBookmark(currentTime); - }; + return ( +
+ + + } + tooltip="Auto snapping" + onClick={() => toggleSnapping()} + /> - const currentBookmarked = isBookmarked(currentTime); - return ( -
-
- - - - - - - {isPlaying ? "Pause (Space)" : "Play (Space)"} - - - - - - - Return to Start (Home / Enter) - -
- {/* Time Display */} -
- -
- / -
-
- {formatTimeCode(duration, "HH:MM:SS:FF")} -
-
- {tracks.length === 0 && ( - <> -
- - - - - Add a test clip to try playback - - - )} -
- - - - - Split element (Ctrl+S) - - - - - - Split and keep left (Ctrl+Q) - - - - - - Split and keep right (Ctrl+W) - - - - - - Separate audio (Ctrl+D) - - - - - - Duplicate element (Ctrl+D) - - - - - - Freeze frame (F) - - - - - - Delete element (Delete) - -
- - - - - - {currentBookmarked ? "Remove bookmark" : "Add bookmark"} - - - -
-
- - {currentScene?.name || "No Scene"} - - - {}}> - - - - -
-
- - - - - - Auto snapping - - - - - - - {rippleEditingEnabled - ? "Disable Ripple Editing" - : "Enable Ripple Editing"} - - - + + } + tooltip="Ripple editing" + onClick={() => toggleRippleEditing()} + /> + -
-
- - - -
-
-
- ); +
+ +
+ + onZoomChange(values[0])} + min={minZoom} + max={TIMELINE_CONSTANTS.ZOOM_MAX} + step={TIMELINE_CONSTANTS.ZOOM_STEP} + /> + +
+
+ ); +} + +function ToolbarButton({ + icon, + tooltip, + onClick, + disabled, +}: { + icon: React.ReactNode; + tooltip: string; + onClick: ({ event }: { event: React.MouseEvent }) => void; + disabled?: boolean; +}) { + return ( + + + + + {tooltip} + + ); } -- cgit v1.3.1