Files
vfxreview/components/player/PlaybackControls.tsx
T
twotalesanimation 0fbe856dce Initial commit
2026-05-19 22:20:29 +02:00

285 lines
8.1 KiB
TypeScript

"use client";
import { RefObject } from "react";
import {
Play,
Pause,
SkipBack,
SkipForward,
ChevronFirst,
ChevronLast,
Maximize2,
MessageSquarePlus,
Pencil,
Eye,
EyeOff,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useReviewStore } from "@/hooks/use-review-player";
import { frameToTimecode } from "@/lib/frame-utils";
import { cn } from "@/lib/utils";
const PLAYBACK_RATES = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2];
interface PlaybackControlsProps {
videoRef: RefObject<HTMLVideoElement | null>;
fps: number;
isReversing: boolean;
onStepBackward: () => void;
onStepForward: () => void;
onReverse: () => void;
onTogglePlay: () => void;
onToggleFullscreen: () => void;
onAddComment: () => void;
}
export function PlaybackControls({
videoRef,
fps,
isReversing,
onStepBackward,
onStepForward,
onReverse,
onTogglePlay,
onToggleFullscreen,
onAddComment,
}: PlaybackControlsProps) {
const {
isPlaying,
currentFrame,
currentTime,
totalFrames,
playbackRate,
isAnnotating,
showAnnotations,
setPlaybackRate,
setAnnotating,
setShowAnnotations,
} = useReviewStore();
const handleRateChange = (val: string) => {
const rate = parseFloat(val);
if (videoRef.current) videoRef.current.playbackRate = rate;
setPlaybackRate(rate);
};
const timecode = frameToTimecode(currentFrame, fps);
return (
<div className="flex items-center gap-2 bg-black/90 px-3 py-2 border-t border-white/5">
{/* Frame info */}
<div className="flex items-center gap-3 font-mono text-xs text-zinc-300 min-w-0">
<span className="hidden sm:block text-zinc-500">
{timecode}
</span>
<span className="text-white font-semibold">
F{String(currentFrame).padStart(4, "0")}
</span>
<span className="text-zinc-600 hidden md:block">
/ {totalFrames}
</span>
</div>
{/* Divider */}
<div className="h-5 w-px bg-white/10 mx-1" />
{/* Transport Controls */}
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="text-zinc-400 hover:text-white"
onClick={() => {
if (videoRef.current) videoRef.current.currentTime = 0;
}}
>
<ChevronFirst className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Go to start</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className={cn(
"text-zinc-400 hover:text-white",
isReversing && "text-amber-400 bg-amber-400/10"
)}
onClick={onReverse}
>
<SkipBack className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Reverse (J)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="text-zinc-400 hover:text-white"
onClick={onStepBackward}
>
<SkipBack className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Step back ()</TooltipContent>
</Tooltip>
{/* Play/Pause — slightly larger */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-white hover:text-white hover:bg-white/10 h-9 w-9"
onClick={onTogglePlay}
>
{isPlaying ? (
<Pause className="h-5 w-5 fill-current" />
) : (
<Play className="h-5 w-5 fill-current" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>Play / Pause (K or Space)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="text-zinc-400 hover:text-white"
onClick={onStepForward}
>
<SkipForward className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>Step forward ()</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="text-zinc-400 hover:text-white"
onClick={() => {
if (videoRef.current)
videoRef.current.currentTime = videoRef.current.duration;
}}
>
<ChevronLast className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Go to end</TooltipContent>
</Tooltip>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Right Controls */}
<div className="flex items-center gap-2">
{/* Playback speed */}
<Select value={String(playbackRate)} onValueChange={handleRateChange}>
<SelectTrigger className="h-7 w-16 text-xs border-0 bg-white/5 text-zinc-300 px-2">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PLAYBACK_RATES.map((r) => (
<SelectItem key={r} value={String(r)} className="text-xs">
{r}x
</SelectItem>
))}
</SelectContent>
</Select>
{/* Annotation toggle */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className={cn(
"text-zinc-400 hover:text-white",
isAnnotating && "text-amber-400 bg-amber-400/10"
)}
onClick={() => setAnnotating(!isAnnotating)}
>
<Pencil className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
{isAnnotating ? "Stop drawing" : "Draw annotation"}
</TooltipContent>
</Tooltip>
{/* Show/hide annotations */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="text-zinc-400 hover:text-white"
onClick={() => setShowAnnotations(!showAnnotations)}
>
{showAnnotations ? (
<Eye className="h-4 w-4" />
) : (
<EyeOff className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
{showAnnotations ? "Hide annotations" : "Show annotations"}
</TooltipContent>
</Tooltip>
{/* Add Comment */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="text-zinc-400 hover:text-primary"
onClick={onAddComment}
>
<MessageSquarePlus className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Add comment at frame {currentFrame}</TooltipContent>
</Tooltip>
{/* Fullscreen */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon-sm"
className="text-zinc-400 hover:text-white"
onClick={onToggleFullscreen}
>
<Maximize2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Fullscreen (F)</TooltipContent>
</Tooltip>
</div>
</div>
);
}