import type { CSSProperties, Ref } from "react"; import { useTranslation } from "react-i18next"; import { Repeat, Repeat1, Shuffle, SkipBack, SkipForward } from "lucide-react"; import type { VideoDetail } from "../lib/api"; import type { AutoMode, LoopMode } from "../lib/playerModes"; import { Button } from "./ui/Button"; import { StateMessage } from "./ui/QueryState"; import { ClampedText } from "./ui/ClampedText"; import { renderDescription } from "./descriptionLinks"; // Presentational controls for the YouTube modal player (PlayerModal), split out of the god component — // the sibling of plexPlayerControls.tsx. No state or effects here; all behaviour comes in via props. // The auto-advance / loop modes it renders are defined in `lib/playerModes`. /** The queue transport cluster — prev / counter / next, then the auto-advance and loop mode toggles. * Rendered in two places with identical logic: the modal's queue bar (`variant:"modal"`, normal glass * theme, labels always shown) and the big-view docked control bar (`variant:"docked"`, white-on-dark, * labels hidden until a breakpoint). Only the theme + label-visibility differ; the behaviour is one, * so a mode/icon change lands once. The wrapper (fragment vs. bordered row) stays at the call site. */ export function PlayerTransportControls({ variant, index, queueLength, autoMode, loopMode, onPrev, onNext, onCycleAuto, onCycleLoop, }: { variant: "docked" | "modal"; index: number; queueLength: number; autoMode: AutoMode; loopMode: LoopMode; onPrev: () => void; onNext: () => void; onCycleAuto: () => void; onCycleLoop: () => void; }) { const { t } = useTranslation(); const docked = variant === "docked"; // The docked bar sits on the video (always dark): the overlay variant; the modal's is a page surface. const chrome = docked ? "overlay" : "ghost"; const navLabel = docked ? "hidden sm:inline" : ""; const counter = docked ? "tabular-nums text-white/70" : "text-muted tabular-nums"; const divider = docked ? "h-4 w-px bg-white/20" : "w-px h-4 bg-border"; const toggleLabel = docked ? "hidden md:inline" : ""; return ( <> {index + 1} / {queueLength} {/* Persistent playback settings — cycle on click, saved to your account. */} ); } /** The description, said the same way in its two places (the popover under 1280 px, the panel beside * the video above it): loading, failed with a retry, the text in the caller's box, or none. One * ladder, so the two cannot drift (the failed state was once tested in the panel only). * `ref` / `style` / `className` dress the text's box only. `clamp` cuts it to three lines with a * More / Less (the panel, where the description sits under the actions and is rarely read whole). */ export function PlayerDescription({ detail, currentId, onSeek, onLoadVideo, className, style, ref, clamp = false, }: { detail: { isLoading: boolean; isError: boolean; data?: VideoDetail; refetch: () => unknown }; currentId: string; onSeek: (seconds: number) => void; onLoadVideo: (id: string, start: number | null) => void; className: string; style?: CSSProperties; ref?: Ref; clamp?: boolean; }) { const { t } = useTranslation(); if (detail.isLoading) return
{t("player.loading")}
; if (detail.isError && !detail.data) return ( void detail.refetch()}> {t("player.descriptionFailed")} ); const text = detail.data?.description; if (!text) return
{t("player.noDescription")}
; if (clamp) return ( {renderDescription(text, { currentId, onSeek, onLoadVideo, t })} ); return (
{renderDescription(text, { currentId, onSeek, onLoadVideo, t })}
); }