diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx index a9d6e464..9907c555 100644 --- a/frontend/src/components/PlexPlayer.tsx +++ b/frontend/src/components/PlexPlayer.tsx @@ -6,7 +6,6 @@ import { useEffect, useRef, useState, - type ReactNode, type WheelEvent as ReactWheelEvent, } from "react"; import { qk } from "../lib/queryKeys"; @@ -35,7 +34,18 @@ import { X, } from "lucide-react"; import { api, HttpError, type PlexMarker } from "../lib/api"; -import { formatDuration } from "../lib/format"; +import { + audioOrdForLang, + clamp01, + clockParts, + DEFAULT_PREFS, + fmt, + HLS_SUB_LAG, + subOrdForLang, + subShadowCss, + type PlexPlayerPrefs, +} from "../lib/plexPlayerPrefs"; +import { Ctrl, PanelHead, Slider, Toggle } from "./plexPlayerControls"; import { exitFullscreen, fullscreenElement, @@ -51,126 +61,6 @@ import { pushLayer, removeLayer, anyLayerAbove } from "../lib/layerStack"; // The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page. const PlexInfo = lazy(() => import("./PlexInfo")); -// Audio/subtitle selections are persisted per-account by LANGUAGE, not by ordinal — the same track -// ordinal means different languages in different files, so we re-resolve the saved language to this -// item's track layout on load (and it carries across episodes/movies, like Plex's preferred-language). -type AudioStream = { ord: number; language?: string | null; default: boolean }; -type SubStream = { ord: number; language?: string | null; text: boolean }; -function audioOrdForLang(streams: AudioStream[], lang: string): number | null { - if (!lang) return null; // "" = use the file's default first track (keeps direct-play, no forced HLS) - const i = streams.findIndex((s) => (s.language ?? "") === lang); - // absent (-1) or already the FIRST track (0, which the backend plays by default) → no override. - // (Match by index, NOT the `default` flag — some files mark a non-first track default, which made - // the restored language silently fall back to track 0.) - return i <= 0 ? null : streams[i]!.ord; // i > 0 here, so streams[i] exists -} -function subOrdForLang(subs: SubStream[], lang: string): number | null { - if (!lang) return null; // "" = subtitles off - const m = subs.find((s) => s.text && (s.language ?? "") === lang); - return m ? m.ord : null; // preferred language not available here → off -} - -// Per-account player preferences, persisted so they survive F5 and carry across items. -type PlexPlayerPrefs = { - volume: number; // 0..1 - muted: boolean; - audioLang: string; // "" = default first audio - subLang: string; // "" = subtitles off - subOffset: number; // seconds, +later / -earlier (client-side cue shift) - audioOffset: number; // seconds, +later / -earlier (server-side -itsoffset) - barAutoHide: boolean; // auto-hide the control bar during playback - seekStep: number; // plain ←/→ jump (s) - fineSeekStep: number; // Ctrl+←/→ jump (s) - subSize: number; // subtitle font size (%) - subColor: string; // subtitle text color (hex) - subBg: string; // subtitle background ("transparent" or hex) - subPos: number; // subtitle vertical position (cue line %, higher = lower on screen) - subShadow: number; // subtitle text-shadow blur px (0 = off) - subShadowColor: string; // subtitle text-shadow color (hex) - clockShow: boolean; // top-right wall clock - clock24h: boolean; // 24h (European) vs 12h am/pm - clockSize: number; // clock font size (%) - clockColor: string; // clock color (hex) - clockDate: boolean; // show the date alongside the time - clockDateStyle: "en" | "hu"; // DD-MON-YYYY Ddd vs YYYY-MON-DD Ddd - autoSkipIntro: boolean; // auto-skip the intro marker after a countdown - autoSkipCredits: boolean; // auto-skip credits → jump to the next item (binge) - autoSkipDelay: number; // countdown before an auto-skip fires (s, 0 = immediate) -}; -const DEFAULT_PREFS: PlexPlayerPrefs = { - volume: 1, - muted: false, - audioLang: "", - subLang: "", - subOffset: 0, - audioOffset: 0, - barAutoHide: true, - seekStep: 10, - fineSeekStep: 1, - subSize: 100, - subColor: "#ffffff", - subBg: "transparent", - subPos: 88, - subShadow: 0, - subShadowColor: "#000000", - clockShow: true, - clock24h: true, - clockSize: 100, - clockColor: "#ffffff", - clockDate: false, - clockDateStyle: "hu", - autoSkipIntro: false, - autoSkipCredits: false, - autoSkipDelay: 5, -}; -const clamp01 = (n: number) => Math.max(0, Math.min(1, n)); - -// A punchy subtitle shadow. The old single 0-offset blur washed out to nothing on bright scenes even -// at the max slider value; instead lay down eight offset copies (a solid dark OUTLINE around the -// glyphs) plus a soft glow, all scaled by the slider — legible over anything, e.g. yellow text on a -// snow-white frame. Empty string when the slider is 0 (shadow off). -function subShadowCss(px: number, color: string): string { - if (px <= 0) return ""; - const o = px; - const b = Math.max(1, Math.round(px / 2)); - const dirs: [number, number][] = [ - [o, 0], - [-o, 0], - [0, o], - [0, -o], - [o, o], - [o, -o], - [-o, o], - [-o, -o], - ]; - const outline = dirs.map(([x, y]) => `${x}px ${y}px ${b}px ${color}`).join(","); - return `text-shadow:${outline},0 0 ${o * 2}px ${color};`; -} - -// Top-right wall clock text: time (24h European or 12h am/pm) + optional date in one of two fixed -// styles — EN "08-JUL-2026 Wed" or HU "2026-JÚL-08 Sze" (uppercase short month, short weekday). -function clockParts(now: Date, p: PlexPlayerPrefs): { time: string; date: string | null } { - const time = now.toLocaleTimeString(p.clock24h ? "en-GB" : "en-US", { - hour: "2-digit", - minute: "2-digit", - hour12: !p.clock24h, - }); - if (!p.clockDate) return { time, date: null }; - const loc = p.clockDateStyle === "hu" ? "hu-HU" : "en-GB"; - const dd = String(now.getDate()).padStart(2, "0"); - const yyyy = now.getFullYear(); - const mon = now.toLocaleString(loc, { month: "short" }).replace(/\./g, "").toUpperCase(); - const dow = now.toLocaleString(loc, { weekday: "short" }).replace(/\./g, ""); - const date = - p.clockDateStyle === "hu" ? `${yyyy}-${mon}-${dd} ${dow}` : `${dd}-${mon}-${yyyy} ${dow}`; - return { time, date }; -} -// hls.js plays our non-zero-start `-copyts` HLS stream ~1.0s BEHIND the media clock (measured by -// frame-capture: the first frame at source K lands at currentTime≈1.0, not 0 — constant, framerate- and -// segment-type-independent). Native-time subtitles (shifted by K) would therefore run 1s early, so we -// bake this compensation into the shift. Only on the HLS path; direct-play uses the file's real clock. -const HLS_SUB_LAG = 1.0; - // The Plex module's rich, full-page player. Plays the LOCAL file: direct-playable files stream raw // (native