summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/components/editor/panels/timeline/bookmarks.tsx
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/components/editor/panels/timeline/bookmarks.tsx')
-rw-r--r--apps/web/src/components/editor/panels/timeline/bookmarks.tsx427
1 files changed, 0 insertions, 427 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
deleted file mode 100644
index c432b7d4..00000000
--- a/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
+++ /dev/null
@@ -1,427 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import type { EditorCore } from "@/core";
-import { useEditor } from "@/hooks/use-editor";
-import type { BookmarkDragState } from "@/hooks/timeline/use-bookmark-drag";
-import {
- DEFAULT_TIMELINE_BOOKMARK_COLOR,
-} from "./theme";
-import { TIMELINE_BOOKMARK_ROW_HEIGHT_PX } from "./layout";
-import { DEFAULT_FPS } from "@/lib/fps/defaults";
-import { snappedSeekTime } from "opencut-wasm";
-import {
- ArrowTurnBackwardIcon,
- Delete02Icon,
-} from "@hugeicons/core-free-icons";
-import { HugeiconsIcon } from "@hugeicons/react";
-import type { Bookmark } from "@/lib/timeline";
-import {
- Popover,
- PopoverAnchor,
- PopoverContent,
-} from "@/components/ui/popover";
-import { Input } from "@/components/ui/input";
-import { ColorPicker } from "@/components/ui/color-picker";
-import { Button } from "@/components/ui/button";
-import { Label } from "@/components/ui/label";
-import { uppercase } from "@/utils/string";
-import { clamp, formatNumberForDisplay } from "@/utils/math";
-import {
- timelineTimeToPixels,
- timelineTimeToSnappedPixels,
-} from "@/lib/timeline";
-
-const MIN_BOOKMARK_WIDTH_PX = 2;
-const BOOKMARK_MARKER_WIDTH_PX = 12;
-const BOOKMARK_MARKER_HEIGHT_PX = 15;
-const BOOKMARK_HALF_WIDTH_PX = BOOKMARK_MARKER_WIDTH_PX / 2;
-const BOOKMARK_MARKER_CLIP_PATH =
- "polygon(50% 100%, 12% 72%, 12% 10%, 88% 10%, 88% 72%)";
-
-function seekToBookmarkTime({
- editor,
- time,
-}: {
- editor: EditorCore;
- time: number;
-}) {
- const activeProject = editor.project.getActive();
- const duration = editor.timeline.getTotalDuration();
- const rate = activeProject?.settings.fps ?? DEFAULT_FPS;
- const snappedTime = snappedSeekTime({ time, duration, rate }) ?? time;
- editor.playback.seek({ time: snappedTime });
-}
-
-interface TimelineBookmarksRowProps {
- zoomLevel: number;
- dynamicTimelineWidth: number;
- dragState: BookmarkDragState;
- onBookmarkMouseDown: (params: {
- event: React.MouseEvent;
- bookmark: Bookmark;
- }) => void;
- handleWheel: (event: React.WheelEvent) => void;
- handleTimelineContentClick: (event: React.MouseEvent) => void;
- handleRulerTrackingMouseDown: (event: React.MouseEvent) => void;
- handleRulerMouseDown: (event: React.MouseEvent) => void;
-}
-
-export function TimelineBookmarksRow({
- zoomLevel,
- dynamicTimelineWidth,
- dragState,
- onBookmarkMouseDown,
- handleWheel,
- handleTimelineContentClick,
- handleRulerTrackingMouseDown,
- handleRulerMouseDown,
-}: TimelineBookmarksRowProps) {
- const bookmarks = useEditor((e) => e.scenes.getActiveScene().bookmarks);
-
- return (
- <div
- className="relative flex-1 overflow-hidden"
- style={{ height: TIMELINE_BOOKMARK_ROW_HEIGHT_PX }}
- >
- <button
- className="relative w-full cursor-default select-none border-0 bg-transparent p-0"
- style={{
- height: TIMELINE_BOOKMARK_ROW_HEIGHT_PX,
- width: `${dynamicTimelineWidth}px`,
- }}
- aria-label="Timeline ruler"
- type="button"
- onWheel={handleWheel}
- onClick={(event) => {
- if (!event.currentTarget.contains(event.target as Node)) return;
- handleTimelineContentClick(event);
- }}
- onMouseDown={(event) => {
- if (!event.currentTarget.contains(event.target as Node)) return;
- handleRulerMouseDown(event);
- handleRulerTrackingMouseDown(event);
- }}
- >
- {bookmarks.map((bookmark) => (
- <TimelineBookmark
- key={`bookmark-${bookmark.time}`}
- bookmark={bookmark}
- zoomLevel={zoomLevel}
- dragState={dragState}
- onBookmarkMouseDown={onBookmarkMouseDown}
- />
- ))}
- </button>
- </div>
- );
-}
-
-function TimelineBookmark({
- bookmark,
- zoomLevel,
- dragState,
- onBookmarkMouseDown,
-}: {
- bookmark: Bookmark;
- zoomLevel: number;
- dragState: BookmarkDragState;
- onBookmarkMouseDown: (params: {
- event: React.MouseEvent;
- bookmark: Bookmark;
- }) => void;
-}) {
- const editor = useEditor();
- const duration = editor.timeline.getTotalDuration();
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
-
- const isDragging =
- dragState.isDragging &&
- dragState.bookmarkTime !== null &&
- dragState.bookmarkTime === bookmark.time;
-
- const displayTime = isDragging ? dragState.currentTime : bookmark.time;
- const time = bookmark.time;
- const bookmarkDuration = bookmark.duration ?? 0;
- const durationWidth =
- bookmarkDuration > 0
- ? timelineTimeToPixels({ time: bookmarkDuration, zoomLevel })
- : 0;
- const hasDurationRange = durationWidth > MIN_BOOKMARK_WIDTH_PX;
- const bookmarkWidth = BOOKMARK_MARKER_WIDTH_PX + Math.max(durationWidth, 0);
- const left = timelineTimeToSnappedPixels({ time: displayTime, zoomLevel });
- const bookmarkLeft = left - BOOKMARK_HALF_WIDTH_PX;
- const rightHalfLeft = BOOKMARK_HALF_WIDTH_PX + Math.max(durationWidth, 0);
- const iconColor = bookmark.color ?? DEFAULT_TIMELINE_BOOKMARK_COLOR;
-
- const handleSeek = () => seekToBookmarkTime({ editor, time });
-
- const handleClick = (event: React.MouseEvent) => {
- event.preventDefault();
- event.stopPropagation();
- if (event.detail === 2) {
- setIsPopoverOpen(true);
- } else {
- handleSeek();
- }
- };
-
- const handleKeyDown = (event: React.KeyboardEvent) => {
- if (event.key !== "Enter" && event.key !== " ") return;
- event.preventDefault();
- handleSeek();
- };
-
- const handleMouseDown = (event: React.MouseEvent) => {
- onBookmarkMouseDown({ event, bookmark });
- event.preventDefault();
- event.stopPropagation();
- };
-
- return (
- <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
- <PopoverAnchor asChild>
- <button
- className="absolute top-0 h-full min-w-0.5 border-0 bg-transparent p-0"
- style={{
- left: `${bookmarkLeft}px`,
- width: `${bookmarkWidth}px`,
- }}
- aria-label={`Bookmark at ${formatNumberForDisplay({ value: time, fractionDigits: 1 })}s`}
- type="button"
- onMouseDown={handleMouseDown}
- onClick={handleClick}
- onKeyDown={handleKeyDown}
- >
- {hasDurationRange ? (
- <div
- className="absolute opacity-30"
- style={{
- top: 1.5,
- height: BOOKMARK_MARKER_HEIGHT_PX - 2.5,
- left: BOOKMARK_HALF_WIDTH_PX,
- width: durationWidth,
- backgroundColor: iconColor,
- }}
- />
- ) : null}
- <div
- className="absolute top-0 overflow-hidden"
- style={{
- left: 0,
- width: BOOKMARK_HALF_WIDTH_PX,
- height: BOOKMARK_MARKER_HEIGHT_PX,
- }}
- >
- <div
- className="absolute inset-0"
- style={{
- width: BOOKMARK_MARKER_WIDTH_PX,
- height: BOOKMARK_MARKER_HEIGHT_PX,
- clipPath: BOOKMARK_MARKER_CLIP_PATH,
- backgroundColor: "hsl(var(--background))",
- }}
- />
- <div
- className="absolute"
- style={{
- top: 1,
- left: 1,
- width: BOOKMARK_MARKER_WIDTH_PX - 2,
- height: BOOKMARK_MARKER_HEIGHT_PX - 2,
- clipPath: BOOKMARK_MARKER_CLIP_PATH,
- backgroundColor: iconColor,
- }}
- />
- </div>
- <div
- className="absolute top-0 overflow-hidden"
- style={{
- left: rightHalfLeft,
- width: BOOKMARK_HALF_WIDTH_PX,
- height: BOOKMARK_MARKER_HEIGHT_PX,
- }}
- >
- <div
- className="absolute top-0"
- style={{
- left: -BOOKMARK_HALF_WIDTH_PX,
- width: BOOKMARK_MARKER_WIDTH_PX,
- height: BOOKMARK_MARKER_HEIGHT_PX,
- clipPath: BOOKMARK_MARKER_CLIP_PATH,
- backgroundColor: "hsl(var(--background))",
- }}
- />
- <div
- className="absolute"
- style={{
- top: 1,
- left: 1 - BOOKMARK_HALF_WIDTH_PX,
- width: BOOKMARK_MARKER_WIDTH_PX - 2,
- height: BOOKMARK_MARKER_HEIGHT_PX - 2,
- clipPath: BOOKMARK_MARKER_CLIP_PATH,
- backgroundColor: iconColor,
- }}
- />
- </div>
- </button>
- </PopoverAnchor>
- <PopoverContent
- className="w-64 flex flex-col gap-3 p-3"
- align="start"
- side="bottom"
- sideOffset={8}
- onOpenAutoFocus={(event) => event.preventDefault()}
- >
- <BookmarkPopoverContent
- bookmark={bookmark}
- time={time}
- timelineDuration={duration}
- onPopoverClose={() => setIsPopoverOpen(false)}
- />
- </PopoverContent>
- </Popover>
- );
-}
-
-function BookmarkPopoverContent({
- bookmark,
- time,
- timelineDuration,
- onPopoverClose,
-}: {
- bookmark: Bookmark;
- time: number;
- timelineDuration: number;
- onPopoverClose: () => void;
-}) {
- const editor = useEditor();
- const [draftColorHex, setDraftColorHex] = useState(
- (bookmark.color ?? DEFAULT_TIMELINE_BOOKMARK_COLOR)
- .replace("#", "")
- .toUpperCase(),
- );
-
- useEffect(() => {
- setDraftColorHex(
- (bookmark.color ?? DEFAULT_TIMELINE_BOOKMARK_COLOR)
- .replace("#", "")
- .toUpperCase(),
- );
- }, [bookmark.color]);
-
- const handleRemove = () => {
- editor.scenes.removeBookmark({ time });
- onPopoverClose();
- };
-
- const handleUpdate = ({
- note,
- color,
- duration,
- }: Partial<{ note: string; color: string; duration: number }>) => {
- const updates: Partial<{ note: string; color: string; duration: number }> =
- {};
- if (note !== undefined && note !== bookmark.note) updates.note = note;
- if (
- color !== undefined &&
- color.toUpperCase() !== (bookmark.color ?? "").toUpperCase()
- ) {
- updates.color = color;
- }
- if (duration !== undefined && duration !== bookmark.duration) {
- updates.duration = duration;
- }
- if (Object.keys(updates).length === 0) return;
- editor.scenes.updateBookmark({ time, updates });
- };
-
- return (
- <>
- <div className="flex flex-col gap-2">
- <Label className="text-xs">Note</Label>
- <Input
- placeholder="Add a note..."
- value={bookmark.note ?? ""}
- onChange={(event) => handleUpdate({ note: event.target.value })}
- className="h-8 text-sm"
- />
- </div>
- <div className="flex flex-col gap-2">
- <Label className="text-xs">Color</Label>
- <div className="relative">
- <ColorPicker
- value={uppercase({ string: draftColorHex })}
- onChange={(color) => setDraftColorHex(uppercase({ string: color }))}
- onChangeEnd={(color) =>
- handleUpdate({ color: `#${uppercase({ string: color })}` })
- }
- className="bg-background border"
- />
- {bookmark.color &&
- bookmark.color.replace(/^#/, "").toUpperCase() !==
- DEFAULT_TIMELINE_BOOKMARK_COLOR.replace(/^#/, "").toUpperCase() && (
- <Button
- type="button"
- variant="text"
- size="text"
- aria-label="Reset to default color"
- className="absolute top-1/2 right-1 -translate-y-1/2 mr-1"
- onClick={() =>
- editor.scenes.updateBookmark({
- time,
- updates: { color: undefined },
- })
- }
- >
- <HugeiconsIcon
- icon={ArrowTurnBackwardIcon}
- className="!size-3.5"
- />
- </Button>
- )}
- </div>
- </div>
- <div className="flex flex-col gap-2">
- <Label className="text-xs">Duration</Label>
- <div className="flex items-center gap-1.5">
- <Input
- type="number"
- min={0}
- step={0.1}
- value={bookmark.duration ?? 0}
- onChange={(event) => {
- const parsed = parseFloat(event.target.value);
- const value = Number.isNaN(parsed)
- ? 0
- : clamp({
- value: parsed,
- min: 0,
- max: Math.max(0, timelineDuration - time),
- });
- handleUpdate({ duration: value });
- }}
- className="h-8 text-sm"
- containerClassName="w-full"
- />
- </div>
- </div>
- <Button
- type="button"
- variant="outline"
- size="sm"
- className="text-destructive hover:bg-destructive/10"
- onClick={handleRemove}
- onKeyDown={(event) => {
- if (event.key === "Enter" || event.key === " ") {
- handleRemove();
- }
- }}
- aria-label="delete bookmark"
- >
- <HugeiconsIcon icon={Delete02Icon} className="!size-3.5" />
- Delete
- </Button>
- </>
- );
-}