From d6724e2552d14140017b194e7020252bda73751a Mon Sep 17 00:00:00 2001 From: leimingsheng Date: Mon, 21 Jul 2025 19:15:52 +0800 Subject: feat: init timeline zoom --- apps/web/src/hooks/use-keyboard-shortcuts-help.ts | 4 ++ apps/web/src/hooks/use-timeline-zoom-actions.ts | 37 +++++++++++++ apps/web/src/hooks/use-timeline-zoom.ts | 64 ++++++++++++++++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/hooks/use-timeline-zoom-actions.ts (limited to 'apps/web/src/hooks') diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts index c7de8466..d0f177f6 100644 --- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts +++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts @@ -60,6 +60,10 @@ const actionDescriptions: Record< category: "Selection", }, "toggle-snapping": { description: "Toggle snapping", category: "Editing" }, + "zoom-in": { description: "Zoom in timeline", category: "View" }, + "zoom-out": { description: "Zoom out timeline", category: "View" }, + "zoom-reset": { description: "Reset timeline zoom", category: "View" }, + "zoom-fit": { description: "Fit timeline to window", category: "View" }, undo: { description: "Undo", category: "History" }, redo: { description: "Redo", category: "History" }, }; diff --git a/apps/web/src/hooks/use-timeline-zoom-actions.ts b/apps/web/src/hooks/use-timeline-zoom-actions.ts new file mode 100644 index 00000000..85003684 --- /dev/null +++ b/apps/web/src/hooks/use-timeline-zoom-actions.ts @@ -0,0 +1,37 @@ +"use client"; + +import { useActionHandler } from "@/constants/actions"; + +interface UseTimelineZoomActionsProps { + zoomLevel: number; + setZoomLevel: (zoomLevel: number | ((prev: number) => number)) => void; + onFitToWindow?: () => void; +} + +export function useTimelineZoomActions({ + zoomLevel, + setZoomLevel, + onFitToWindow, +}: UseTimelineZoomActionsProps) { + // Zoom in action + useActionHandler("zoom-in", () => { + setZoomLevel((prev) => Math.min(5, prev + 0.2)); + }); + + // Zoom out action + useActionHandler("zoom-out", () => { + setZoomLevel((prev) => Math.max(0.1, prev - 0.2)); + }); + + // Reset zoom action + useActionHandler("zoom-reset", () => { + setZoomLevel(1); + }); + + // Fit to window action + useActionHandler("zoom-fit", () => { + if (onFitToWindow) { + onFitToWindow(); + } + }); +} diff --git a/apps/web/src/hooks/use-timeline-zoom.ts b/apps/web/src/hooks/use-timeline-zoom.ts index ce32c9f0..8d41d917 100644 --- a/apps/web/src/hooks/use-timeline-zoom.ts +++ b/apps/web/src/hooks/use-timeline-zoom.ts @@ -3,6 +3,7 @@ import { useState, useCallback, useEffect, RefObject } from "react"; interface UseTimelineZoomProps { containerRef: RefObject; isInTimeline?: boolean; + onFitToWindow?: () => void; } interface UseTimelineZoomReturn { @@ -11,22 +12,81 @@ interface UseTimelineZoomReturn { handleWheel: (e: React.WheelEvent) => void; } +const ZOOM_STORAGE_KEY = 'opencut-timeline-zoom'; + export function useTimelineZoom({ containerRef, isInTimeline = false, + onFitToWindow, }: UseTimelineZoomProps): UseTimelineZoomReturn { - const [zoomLevel, setZoomLevel] = useState(1); + // Load initial zoom level from localStorage + const [zoomLevel, setZoomLevel] = useState(() => { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem(ZOOM_STORAGE_KEY); + if (stored) { + const parsed = parseFloat(stored); + if (!isNaN(parsed) && parsed >= 0.1 && parsed <= 5) { + return parsed; + } + } + } + return 1; + }); + + // Save zoom level to localStorage whenever it changes + useEffect(() => { + if (typeof window !== 'undefined') { + localStorage.setItem(ZOOM_STORAGE_KEY, zoomLevel.toString()); + } + }, [zoomLevel]); const handleWheel = useCallback((e: React.WheelEvent) => { // Only zoom if user is using pinch gesture (ctrlKey or metaKey is true) if (e.ctrlKey || e.metaKey) { e.preventDefault(); const delta = e.deltaY > 0 ? -0.15 : 0.15; - setZoomLevel((prev) => Math.max(0.1, Math.min(10, prev + delta))); + setZoomLevel((prev) => Math.max(0.1, Math.min(5, prev + delta))); } // Otherwise, allow normal scrolling }, []); + // Keyboard shortcuts for zoom + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Only handle zoom shortcuts when in timeline or when timeline is focused + if (!isInTimeline) return; + + if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { + switch (e.key) { + case '=': + case '+': + e.preventDefault(); + setZoomLevel((prev) => Math.min(5, prev + 0.2)); + break; + case '-': + e.preventDefault(); + setZoomLevel((prev) => Math.max(0.1, prev - 0.2)); + break; + case '0': + e.preventDefault(); + setZoomLevel(1); + break; + case '9': + e.preventDefault(); + if (onFitToWindow) { + onFitToWindow(); + } + break; + } + } + }; + + if (isInTimeline) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [isInTimeline, setZoomLevel]); + // Prevent browser zooming in/out when in timeline useEffect(() => { const preventZoom = (e: WheelEvent) => { -- cgit v1.3.1