diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-20 11:31:17 +0200 |
| commit | 3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch) | |
| tree | 57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/components/editor/panels/timeline/bookmarks.tsx | |
| parent | 729d10592f4e802f91c555c309ff3ba4b119f843 (diff) | |
| download | opencut-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.tsx | 427 |
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> - </> - ); -} |
