diff options
| author | WOGGER7 <saneenmecheri@gmail.com> | 2025-07-21 14:04:45 +0530 |
|---|---|---|
| committer | WOGGER7 <saneenmecheri@gmail.com> | 2025-07-21 14:04:45 +0530 |
| commit | 5915989669e43ddc51313cfe7f77514bff935837 (patch) | |
| tree | 6e722b9f9af16c4bef09e0f63ea22bc060c96f9b /apps/web/src/components/editor | |
| parent | a2c21dc3c474b5aa00e83d8894f74230a12f6a54 (diff) | |
| download | opencut-5915989669e43ddc51313cfe7f77514bff935837.tar.gz | |
Add video player timeline controls and keyboard shortcuts
Diffstat (limited to 'apps/web/src/components/editor')
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 238 |
1 files 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<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(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} /> </div> </div> @@ -475,6 +548,10 @@ export function PreviewPanel() { hasAnyElements={hasAnyElements} onToggleExpanded={toggleExpanded} isExpanded={isExpanded} + currentTime={currentTime} + setCurrentTime={setCurrentTime} + toggle={toggle} + getTotalDuration={getTotalDuration} /> </div> </div> @@ -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<HTMLDivElement>) => { + 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<HTMLDivElement>) => { + 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 ( - <div - data-toolbar - className="flex items-end justify-between gap-2 p-1 pt-2 w-full" - > - <div> - <p - className={cn( - "text-[0.75rem] text-muted-foreground flex items-center gap-1", - !hasAnyElements && "opacity-50", - )} - > - <span className="text-primary tabular-nums"> + <div data-toolbar className="flex flex-col gap-2 p-1 pt-2 w-full"> + {/* Timeline Controls */} + <div className="flex items-center gap-2 w-full"> + {/* Time Display */} + <div className="flex items-center gap-1 text-[0.70rem] text-muted-foreground tabular-nums"> + <span className="text-primary"> {formatTimeCode( currentTime, "HH:MM:SS:FF", @@ -533,29 +650,76 @@ function PreviewToolbar({ )} </span> <span className="opacity-50">/</span> - <span className="tabular-nums"> + <span> {formatTimeCode( - getTotalDuration(), + totalDuration, "HH:MM:SS:FF", activeProject?.fps || 30, )} </span> - </p> + </div> + + {/* Transport Controls */} + <div className="flex items-center gap-1"> + <Button + variant="text" + size="icon" + onClick={skipBackward} + disabled={!hasAnyElements} + className="h-auto p-0" + title="Skip backward 1s" + > + <SkipBack className="h-3 w-3" /> + </Button> + <Button + variant="text" + size="icon" + onClick={toggle} + disabled={!hasAnyElements} + className="h-auto p-0" + > + {isPlaying ? ( + <Pause className="h-3 w-3" /> + ) : ( + <Play className="h-3 w-3" /> + )} + </Button> + <Button + variant="text" + size="icon" + onClick={skipForward} + disabled={!hasAnyElements} + className="h-auto p-0" + title="Skip forward 1s" + > + <SkipForward className="h-3 w-3" /> + </Button> + </div> + + {/* Timeline Scrubber */} + <div className="flex-1 flex items-center gap-2"> + <div + className={cn( + "relative h-1 bg-muted rounded-full cursor-pointer flex-1", + !hasAnyElements && "opacity-50 cursor-not-allowed", + )} + onClick={hasAnyElements ? handleTimelineClick : undefined} + onMouseDown={hasAnyElements ? handleTimelineDrag : undefined} + > + <div + className="absolute top-0 left-0 h-full bg-primary rounded-full transition-all duration-100" + style={{ width: `${progress}%` }} + /> + <div + className="absolute top-1/2 w-3 h-3 bg-primary rounded-full -translate-y-1/2 -translate-x-1/2 shadow-sm border border-background" + style={{ left: `${progress}%` }} + /> + </div> + </div> </div> - <Button - variant="text" - size="icon" - onClick={toggle} - disabled={!hasAnyElements} - className="h-auto p-0" - > - {isPlaying ? ( - <Pause className="h-3 w-3" /> - ) : ( - <Play className="h-3 w-3" /> - )} - </Button> - <div className="flex items-center gap-3"> + + {/* Bottom Row - Aspect Ratio and Fullscreen */} + <div className="flex items-center justify-end gap-3"> <BackgroundSettings /> <DropdownMenu> <DropdownMenuTrigger asChild> |
