From 5915989669e43ddc51313cfe7f77514bff935837 Mon Sep 17 00:00:00 2001 From: WOGGER7 Date: Mon, 21 Jul 2025 14:04:45 +0530 Subject: [PATCH] Add video player timeline controls and keyboard shortcuts --- .../src/components/editor/preview-panel.tsx | 238 +++++++++++++++--- 1 file changed, 201 insertions(+), 37 deletions(-) diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index b20b0eed..8f594d6c 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -16,7 +16,7 @@ import { DropdownMenuTrigger, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; -import { Play, Pause, Expand } from "lucide-react"; +import { Play, Pause, Expand, SkipBack, SkipForward } from "lucide-react"; import { useState, useRef, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { formatTimeCode } from "@/lib/time"; @@ -31,9 +31,9 @@ interface ActiveElement { } export function PreviewPanel() { - const { tracks } = useTimelineStore(); + const { tracks, getTotalDuration } = useTimelineStore(); const { mediaItems } = useMediaStore(); - const { currentTime } = usePlaybackStore(); + const { currentTime, toggle, setCurrentTime } = usePlaybackStore(); const { canvasSize } = useEditorStore(); const previewRef = useRef(null); const containerRef = useRef(null); @@ -149,6 +149,78 @@ export function PreviewPanel() { setIsExpanded((prev) => !prev); }, []); + // Check if there are any elements in the timeline at all + const hasAnyElements = tracks.some((track) => track.elements.length > 0); + + // Handle keyboard shortcuts for timeline navigation + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + // Only handle shortcuts when preview panel is focused or no input is focused + const activeElement = document.activeElement; + const isInputFocused = + activeElement?.tagName === "INPUT" || + activeElement?.tagName === "TEXTAREA" || + activeElement?.contentEditable === "true"; + + if (isInputFocused) return; + + switch (event.key) { + case " ": + event.preventDefault(); + if (hasAnyElements) { + toggle(); + } + break; + case "ArrowLeft": + event.preventDefault(); + if (hasAnyElements) { + const frameTime = 1 / (activeProject?.fps || 30); + const newTime = Math.max( + 0, + currentTime - (event.shiftKey ? frameTime : 1), + ); + setCurrentTime(newTime); + } + break; + case "ArrowRight": + event.preventDefault(); + if (hasAnyElements) { + const frameTime = 1 / (activeProject?.fps || 30); + const totalDuration = getTotalDuration(); + const newTime = Math.min( + totalDuration, + currentTime + (event.shiftKey ? frameTime : 1), + ); + setCurrentTime(newTime); + } + break; + case "Home": + event.preventDefault(); + if (hasAnyElements) { + setCurrentTime(0); + } + break; + case "End": + event.preventDefault(); + if (hasAnyElements) { + setCurrentTime(getTotalDuration()); + } + break; + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [ + hasAnyElements, + toggle, + currentTime, + setCurrentTime, + activeProject?.fps, + getTotalDuration, + toggle, + ]); + // Get active elements at current time const getActiveElements = (): ActiveElement[] => { const activeElements: ActiveElement[] = []; @@ -182,9 +254,6 @@ export function PreviewPanel() { const activeElements = getActiveElements(); - // Check if there are any elements in the timeline at all - const hasAnyElements = tracks.some((track) => track.elements.length > 0); - // Get media elements for blur background (video/image only) const getBlurBackgroundElements = (): ActiveElement[] => { return activeElements.filter( @@ -431,6 +500,10 @@ export function PreviewPanel() { hasAnyElements={hasAnyElements} onToggleExpanded={toggleExpanded} isExpanded={isExpanded} + currentTime={currentTime} + setCurrentTime={setCurrentTime} + toggle={toggle} + getTotalDuration={getTotalDuration} /> @@ -475,6 +548,10 @@ export function PreviewPanel() { hasAnyElements={hasAnyElements} onToggleExpanded={toggleExpanded} isExpanded={isExpanded} + currentTime={currentTime} + setCurrentTime={setCurrentTime} + toggle={toggle} + getTotalDuration={getTotalDuration} /> @@ -487,14 +564,21 @@ function PreviewToolbar({ hasAnyElements, onToggleExpanded, isExpanded, + currentTime, + setCurrentTime, + toggle, + getTotalDuration, }: { hasAnyElements: boolean; onToggleExpanded: () => void; isExpanded: boolean; + currentTime: number; + setCurrentTime: (time: number) => void; + toggle: () => void; + getTotalDuration: () => number; }) { - const { isPlaying, toggle, currentTime } = usePlaybackStore(); + const { isPlaying } = usePlaybackStore(); const { setCanvasSize, setCanvasSizeToOriginal } = useEditorStore(); - const { getTotalDuration } = useTimelineStore(); const { activeProject } = useProjectStore(); const { currentPreset, @@ -513,19 +597,52 @@ function PreviewToolbar({ setCanvasSizeToOriginal(aspectRatio); }; + const totalDuration = getTotalDuration(); + const progress = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0; + + const handleTimelineClick = (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const clickX = e.clientX - rect.left; + const percentage = clickX / rect.width; + const newTime = percentage * totalDuration; + setCurrentTime(Math.max(0, Math.min(newTime, totalDuration))); + }; + + const handleTimelineDrag = (e: React.MouseEvent) => { + const handleMouseMove = (moveEvent: MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const dragX = moveEvent.clientX - rect.left; + const percentage = Math.max(0, Math.min(1, dragX / rect.width)); + const newTime = percentage * totalDuration; + setCurrentTime(Math.max(0, Math.min(newTime, totalDuration))); + }; + + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + }; + + const skipBackward = () => { + const newTime = Math.max(0, currentTime - 1); // Skip 1 second back + setCurrentTime(newTime); + }; + + const skipForward = () => { + const newTime = Math.min(totalDuration, currentTime + 1); // Skip 1 second forward + setCurrentTime(newTime); + }; + return ( -
-
-

- +

+ {/* Timeline Controls */} +
+ {/* Time Display */} +
+ {formatTimeCode( currentTime, "HH:MM:SS:FF", @@ -533,29 +650,76 @@ function PreviewToolbar({ )} / - + {formatTimeCode( - getTotalDuration(), + totalDuration, "HH:MM:SS:FF", activeProject?.fps || 30, )} -

+
+ + {/* Transport Controls */} +
+ + + +
+ + {/* Timeline Scrubber */} +
+
+
+
+
+
- -
+ + {/* Bottom Row - Aspect Ratio and Fullscreen */} +