refactor(plex): extract PlexPlayer pure helpers + control primitives
S4a-1 of the PlexPlayer decomposition. Move the module-level pure helpers (prefs types/defaults, track-language resolution, subtitle shadow CSS, wall-clock formatting, HLS_SUB_LAG, fmt) to lib/plexPlayerPrefs.ts and the presentational building blocks (Ctrl/Slider/Toggle/PanelHead) to plexPlayerControls.tsx. No hooks or behaviour touched; PlexPlayer.tsx 1889->1661.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { formatDuration } from "./format";
|
||||
|
||||
// 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).
|
||||
export type AudioStream = { ord: number; language?: string | null; default: boolean };
|
||||
export type SubStream = { ord: number; language?: string | null; text: boolean };
|
||||
export 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
|
||||
}
|
||||
export 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.
|
||||
export 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)
|
||||
};
|
||||
export 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,
|
||||
};
|
||||
export 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).
|
||||
export 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).
|
||||
export 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.
|
||||
export const HLS_SUB_LAG = 1.0;
|
||||
|
||||
// Seekbar clock — same H:MM:SS / M:SS shape as the shared formatDuration, but the live media clock
|
||||
// can be NaN/negative before metadata loads, so clamp to 0 first (formatDuration never sees null here).
|
||||
export function fmt(t: number): string {
|
||||
return formatDuration(isFinite(t) && t >= 0 ? t : 0);
|
||||
}
|
||||
Reference in New Issue
Block a user