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.
129 lines
5.9 KiB
TypeScript
129 lines
5.9 KiB
TypeScript
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);
|
|
}
|