import { useT } from "@agent-native/core/client/i18n"; import { IconPlayerPlay, IconPlayerPause, IconPlayerSkipForward, IconVolume, IconVolumeOff, IconMaximize, IconPictureInPicture, IconSubtitles, IconRectangle, } from "@tabler/icons-react"; import { useState, type FocusEvent } from "react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuLabel, DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { PLAYBACK_SPEED_OPTIONS } from "@/lib/playback-speed"; import { cn } from "@/lib/utils"; import { Scrubber, msToClock } from "./scrubber"; export const SPEED_OPTIONS = PLAYBACK_SPEED_OPTIONS; export const PLAYER_SEEK_STEP_MS = 5_000; export interface PlayerControlsProps { isPlaying: boolean; durationMs: number; currentMs: number; volume: number; muted: boolean; speed: number; captionsOn: boolean; hasCaptions: boolean; isFullscreen: boolean; isPip: boolean; theaterMode: boolean; comments?: { id: string; videoTimestampMs: number; content: string }[]; chapters?: { startMs: number; title: string }[]; reactions?: { id: string; emoji: string; videoTimestampMs: number }[]; excludedRanges?: { startMs: number; endMs: number }[]; onPlayPause: () => void; onSeek: (ms: number) => void; onSeekRelative: (deltaMs: number) => void; onVolumeChange: (volume: number) => void; onToggleMute: () => void; onSpeedChange: (rate: number) => void; onToggleCaptions: () => void; onTogglePip: () => void; onToggleFullscreen: () => void; onToggleTheater?: () => void; menuPortalContainer?: HTMLElement | null; } export function PlayerControls(props: PlayerControlsProps) { const t = useT(); const { isPlaying, durationMs, currentMs, volume, muted, speed, captionsOn, hasCaptions, isFullscreen, isPip, theaterMode, comments, chapters, reactions, excludedRanges, onPlayPause, onSeek, onSeekRelative, onVolumeChange, onToggleMute, onSpeedChange, onToggleCaptions, onTogglePip, onToggleFullscreen, onToggleTheater, menuPortalContainer, } = props; const [volumePopoverOpen, setVolumePopoverOpen] = useState(false); const handleVolumeBlur = (event: FocusEvent) => { const nextFocusedElement = event.relatedTarget as Node | null; if (!event.currentTarget.contains(nextFocusedElement)) { setVolumePopoverOpen(false); } }; return (
{isPlaying ? ( ) : ( )} onSeekRelative(-PLAYER_SEEK_STEP_MS)} tooltip="Back 5 seconds" ariaLabel="Back 5 seconds" > onSeekRelative(PLAYER_SEEK_STEP_MS)} tooltip="Forward 5 seconds" ariaLabel="Forward 5 seconds" >
setVolumePopoverOpen(true)} onMouseLeave={() => setVolumePopoverOpen(false)} onFocus={() => setVolumePopoverOpen(true)} onBlur={handleVolumeBlur} > event.preventDefault()} onCloseAutoFocus={(event) => event.preventDefault()} >
onVolumeChange(parseFloat(e.target.value))} className="h-2 w-24 -rotate-90 cursor-pointer accent-white" />
{msToClock(currentMs)} /{msToClock(durationMs)}
{hasCaptions ? (
) : null} {t("playerControls.playbackSpeed")} Speed {SPEED_OPTIONS.map((rate) => ( onSpeedChange(rate)} className={cn( "tabular-nums", rate === speed && "bg-accent font-semibold", )} > {rate}x ))}
{onToggleTheater ? (
) : null}
); } function IconBtn({ children, onClick, tooltip, ariaLabel, active, }: { children: React.ReactNode; onClick?: () => void; tooltip?: string; ariaLabel?: string; active?: boolean; }) { return ( {tooltip} ); } function SkipIcon({ direction }: { direction: "back" | "forward" }) { return ( 5 ); }