import { useLayoutEffect, type RefObject } from "react"; import { useTranslation } from "react-i18next"; import { Slider, Toggle } from "./plexPlayerControls"; import { clockParts, type PlexPlayerPrefs } from "../lib/plexPlayerPrefs"; import { Pressable } from "./ui/Pressable"; import { ColorInput, CheckRow } from "./ui/NativeControls"; import Tabs from "./ui/Tabs"; import { useTabPanel } from "./ui/tabPanel"; type SettingsTab = "sync" | "playback" | "subtitle" | "clock"; // The gear/tuning panel of the Plex player — a tabbed (sync / playback / subtitle / clock) sheet of // Slider/Toggle rows over the per-account prefs. Extracted verbatim from PlexPlayer to keep that // component focused on the media engine; every row is still `prefs.` + `patchPrefs`, so the // panel owns no state of its own. `now` is the live wall clock (passed in, not a second ticker) used // only for the clock-date-style preview buttons. export function PlexPlayerSettings(props: { // `| null` is not optional under React 19's types: `useRef(null)` is now `RefObject`, // which is the honest shape — the element is null until the div mounts. The old `RefObject` lied // about that and 18's types let it. menuRef: RefObject; /** The ⋯ that opened the panel: the focus goes back to it when the panel closes with the focus * inside — by Escape, Back or the ⋯ itself — instead of falling to (E28 review D3). */ returnFocusTo: RefObject; settingsTabs: { key: SettingsTab; label: string }[]; activeTab: SettingsTab; setSettingsTab: (tab: SettingsTab) => void; prefs: PlexPlayerPrefs; patchPrefs: (patch: Partial) => void; changeAudioOffset: (v: number) => void; now: Date; }) { const { menuRef, returnFocusTo, settingsTabs, activeTab, setSettingsTab, prefs, patchPrefs, changeAudioOffset, now, } = props; const { t } = useTranslation(); const panel = useTabPanel(); // Into the panel on open (its selected tab), and back to the ⋯ on close. The cleanup runs while the // panel is still in the DOM, so "the focus was inside" is read before its removal drops it to // ; focus the reader moved elsewhere (a click on the video) stays where they put it. useLayoutEffect(() => { const root = menuRef.current; const back = returnFocusTo.current; // the ⋯ outlives the panel: it is the panel's own trigger root ?.querySelector('[role="tab"][aria-selected="true"]') ?.focus({ preventScroll: true }); return () => { if (root?.contains(document.activeElement)) queueMicrotask(() => back?.focus({ preventScroll: true })); }; }, [menuRef, returnFocusTo]); return (
e.stopPropagation()} onClick={(e) => e.stopPropagation()} className="border border-white/15 bg-media-chrome/95 absolute bottom-full right-0 z-menu mb-2 w-80 rounded-xl p-2 text-sm text-white shadow-2xl" > ({ id: tab.key, label: tab.label }))} active={activeTab} onChange={(id) => setSettingsTab(id as SettingsTab)} className="mb-1.5 flex gap-1" />
{activeTab === "sync" && ( <> patchPrefs({ subOffset: v })} onReset={() => patchPrefs({ subOffset: 0 })} /> changeAudioOffset(0)} /> )} {activeTab === "playback" && ( <> patchPrefs({ seekStep: v })} /> patchPrefs({ fineSeekStep: v })} /> patchPrefs({ barAutoHide: v })} /> patchPrefs({ autoSkipIntro: v })} /> patchPrefs({ autoSkipCredits: v })} /> patchPrefs({ upNext: v })} /> {(prefs.autoSkipIntro || prefs.autoSkipCredits) && ( patchPrefs({ autoSkipDelay: v })} /> )} )} {activeTab === "subtitle" && ( <> patchPrefs({ subSize: v })} /> patchPrefs({ subPos: v })} />
{t("plex.player.subColor")} patchPrefs({ subColor: v })} />
{t("plex.player.subBg")}
patchPrefs({ subBg: checked ? "transparent" : "#000000", }) } className="gap-1 text-2xs text-white/70" > {t("plex.player.subBgNone")} {prefs.subBg !== "transparent" && ( patchPrefs({ subBg: v })} /> )}
patchPrefs({ subShadow: v })} /> {prefs.subShadow > 0 && (
{t("plex.player.subShadowColor")} patchPrefs({ subShadowColor: v })} />
)} )} {activeTab === "clock" && ( <> patchPrefs({ clockShow: v })} /> patchPrefs({ clock24h: v })} /> patchPrefs({ clockSize: v })} />
{t("plex.player.clockColor")} patchPrefs({ clockColor: v })} />
patchPrefs({ clockDate: v })} /> {prefs.clockDate && (
{(["hu", "en"] as const).map((st) => ( patchPrefs({ clockDateStyle: st })} className={`touch-target flex-1 rounded px-2 py-1 text-2xs ${ prefs.clockDateStyle === st ? "bg-white/15 text-accent" : "text-white/70 hover:bg-white/10" }`} > {clockParts(now, { ...prefs, clockDate: true, clockDateStyle: st }).date} ))}
)} )}
); }