refactor(plex): extract PlexPlayer leaf hooks (clock/fullscreen/chrome)
S4a-2 of the PlexPlayer decomposition. Move three self-contained, byte-equivalent behaviours into lib/plexPlayerHooks.ts: useWallClock (15s top-bar clock), useFullscreen (wrapper fs toggle + reactive flag), and useControlsVisibility (auto-hiding control bar + wake). The ref mirrors the hooks read (barAutoHide/menuOpen/video-paused) stay in the component and are passed in, so the timer sees fresh state. No behaviour change; verified by self-E2E (real HLS playback: clock renders, controls auto-hide after 3s while playing, wake restores + stays while paused).
This commit is contained in:
@@ -46,12 +46,8 @@ import {
|
||||
type PlexPlayerPrefs,
|
||||
} from "../lib/plexPlayerPrefs";
|
||||
import { Ctrl, PanelHead, Slider, Toggle } from "./plexPlayerControls";
|
||||
import {
|
||||
exitFullscreen,
|
||||
fullscreenElement,
|
||||
onFullscreenChange,
|
||||
requestFullscreen,
|
||||
} from "../lib/fullscreen";
|
||||
import { useControlsVisibility, useFullscreen, useWallClock } from "../lib/plexPlayerHooks";
|
||||
import { fullscreenElement } from "../lib/fullscreen";
|
||||
import { LS, useAccountPersistedObject } from "../lib/storage";
|
||||
import { useDismiss } from "../lib/useDismiss";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
@@ -105,11 +101,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [abs, setAbs] = useState(0); // absolute current time
|
||||
const [seekableEnd, setSeekableEnd] = useState(0); // absolute end of the loaded region
|
||||
const [fs, setFs] = useState(false);
|
||||
const { fs, toggleFs } = useFullscreen(wrapRef);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [uiVisible, setUiVisible] = useState(true);
|
||||
const hideTimer = useRef<number | null>(null);
|
||||
// Selected audio / subtitle stream ordinals (null audio = default first; null subtitle = off).
|
||||
const [audioOrd, setAudioOrd] = useState<number | null>(null);
|
||||
const [subOrd, setSubOrd] = useState<number | null>(null);
|
||||
@@ -152,6 +146,8 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
const tracksTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
audioRef.current = audioOrd;
|
||||
menuOpenRef.current = menuOpen || tracksOpen; // keep the control bar up while either menu is open
|
||||
// Auto-hiding control bar (reveal on mouse move / any key; hide after 3s while playing).
|
||||
const { uiVisible, wake } = useControlsVisibility({ barAutoHideRef, menuOpenRef, videoRef });
|
||||
// Each menu stays open until a click/Escape OUTSIDE it (so sliders can be dragged freely).
|
||||
useDismiss(menuOpen, () => setMenuOpen(false), [menuRef, menuTriggerRef]);
|
||||
useDismiss(tracksOpen, () => setTracksOpen(false), [tracksRef, tracksTriggerRef]);
|
||||
@@ -174,11 +170,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
const [infoOpen, setInfoOpen] = useState(false);
|
||||
const infoOpenRef = useRef(false);
|
||||
infoOpenRef.current = infoOpen;
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
useEffect(() => {
|
||||
const iv = window.setInterval(() => setNow(new Date()), 15000);
|
||||
return () => window.clearInterval(iv);
|
||||
}, []);
|
||||
const now = useWallClock();
|
||||
|
||||
durationRef.current = duration;
|
||||
|
||||
@@ -469,18 +461,6 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
[nudgeVolume],
|
||||
);
|
||||
|
||||
// Via lib/fullscreen, not the raw API: WebKit (Safari/iPadOS) ships only the prefixed spelling,
|
||||
// so reading `document.fullscreenElement` alone made this player believe it is never fullscreen
|
||||
// — the button did nothing, `fs` never flipped, and Escape (below) read "not fullscreen" as
|
||||
// permission to close the whole player instead of just leaving fullscreen.
|
||||
const toggleFs = useCallback(() => {
|
||||
const el = wrapRef.current;
|
||||
if (!el) return;
|
||||
if (fullscreenElement()) exitFullscreen();
|
||||
else requestFullscreen(el);
|
||||
}, []);
|
||||
useEffect(() => onFullscreenChange(() => setFs(!!fullscreenElement())), []);
|
||||
|
||||
const go = useCallback((otherId: string | null | undefined) => {
|
||||
if (otherId) {
|
||||
// Stop the outgoing media BEFORE switching. If the next item fails to load (e.g. no local
|
||||
@@ -659,17 +639,6 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
return () => video.removeEventListener("ended", onEnded);
|
||||
}, [id, nextId, go, onClose]);
|
||||
|
||||
// Reveal the controls and re-arm the auto-hide timer (on mouse move OR any key).
|
||||
const wake = useCallback(() => {
|
||||
setUiVisible(true);
|
||||
if (hideTimer.current) window.clearTimeout(hideTimer.current);
|
||||
if (!barAutoHideRef.current) return; // per-user setting: never auto-hide the control bar
|
||||
hideTimer.current = window.setTimeout(() => {
|
||||
// Keep the controls up while the audio/subtitle menu is open, else it would vanish mid-choice.
|
||||
if (!videoRef.current?.paused && !menuOpenRef.current) setUiVisible(false);
|
||||
}, 3000);
|
||||
}, []);
|
||||
|
||||
// "Back" cascade shared by the on-screen Back arrow AND the keyboard/remote Backspace·Escape: peel
|
||||
// off whatever is layered on top (a tuning/tracks menu, then the shortcuts sheet, then the info
|
||||
// overlay) before leaving the player. The arrow used to call onClose() unconditionally, so Back
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useCallback, useEffect, useRef, useState, type RefObject } from "react";
|
||||
import {
|
||||
exitFullscreen,
|
||||
fullscreenElement,
|
||||
onFullscreenChange,
|
||||
requestFullscreen,
|
||||
} from "./fullscreen";
|
||||
|
||||
// A live wall clock for the top bar, re-rendered every 15s (minute resolution is enough for the
|
||||
// HH:MM display, and a coarse tick keeps the whole player from re-rendering each second).
|
||||
export function useWallClock(): Date {
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
useEffect(() => {
|
||||
const iv = window.setInterval(() => setNow(new Date()), 15000);
|
||||
return () => window.clearInterval(iv);
|
||||
}, []);
|
||||
return now;
|
||||
}
|
||||
|
||||
// Fullscreen toggle for `el` (the player wrapper) + a reactive `fs` flag.
|
||||
// Via lib/fullscreen, not the raw API: WebKit (Safari/iPadOS) ships only the prefixed spelling, so
|
||||
// reading `document.fullscreenElement` alone made this player believe it is never fullscreen — the
|
||||
// button did nothing, `fs` never flipped, and Escape read "not fullscreen" as permission to close the
|
||||
// whole player instead of just leaving fullscreen.
|
||||
export function useFullscreen(wrapRef: RefObject<HTMLElement | null>): {
|
||||
fs: boolean;
|
||||
toggleFs: () => void;
|
||||
} {
|
||||
const [fs, setFs] = useState(false);
|
||||
const toggleFs = useCallback(() => {
|
||||
const el = wrapRef.current;
|
||||
if (!el) return;
|
||||
if (fullscreenElement()) exitFullscreen();
|
||||
else requestFullscreen(el);
|
||||
}, [wrapRef]);
|
||||
useEffect(() => onFullscreenChange(() => setFs(!!fullscreenElement())), []);
|
||||
return { fs, toggleFs };
|
||||
}
|
||||
|
||||
// Auto-hiding control bar: `uiVisible` drives the chrome opacity; `wake` reveals it and (re)arms the
|
||||
// 3s hide timer. Reads the caller's live refs so the timer callback sees fresh state without a
|
||||
// closure rebuild: barAutoHide (per-user "never auto-hide" setting), whether a menu is open (keep the
|
||||
// bar up so it can't vanish mid-choice), and the <video> paused state (a paused video keeps the bar).
|
||||
export function useControlsVisibility(opts: {
|
||||
barAutoHideRef: RefObject<boolean>;
|
||||
menuOpenRef: RefObject<boolean>;
|
||||
videoRef: RefObject<HTMLVideoElement | null>;
|
||||
}): { uiVisible: boolean; wake: () => void } {
|
||||
const { barAutoHideRef, menuOpenRef, videoRef } = opts;
|
||||
const [uiVisible, setUiVisible] = useState(true);
|
||||
const hideTimer = useRef<number | null>(null);
|
||||
const wake = useCallback(() => {
|
||||
setUiVisible(true);
|
||||
if (hideTimer.current) window.clearTimeout(hideTimer.current);
|
||||
if (!barAutoHideRef.current) return; // per-user setting: never auto-hide the control bar
|
||||
hideTimer.current = window.setTimeout(() => {
|
||||
// Keep the controls up while the audio/subtitle menu is open, else it would vanish mid-choice.
|
||||
if (!videoRef.current?.paused && !menuOpenRef.current) setUiVisible(false);
|
||||
}, 3000);
|
||||
}, [barAutoHideRef, menuOpenRef, videoRef]);
|
||||
return { uiVisible, wake };
|
||||
}
|
||||
Reference in New Issue
Block a user