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