refactor(frontend): the two players — one real ordering bug avoided, five suppressions gone (E27 S2)
The interesting part of this batch is a change that was WRONG and the test suite said so. PlayerModal synced its local watch-status mirror and `currentVideoId` from an effect on `active.id`. Moving that into render is right — as an effect there was a frame per queue advance showing the new episode's chrome over the previous item's watch state, with `navigated` still true so the "you have navigated away" affordance flashed. But the same effect also advanced `currentIdRef`, and that line must NOT move: useYtProgress's per-item cleanup persists the OUTGOING item's position and guards it on `currentIdRef.current === id`. React runs every cleanup before any setup, so in an effect the ref still names the item being left. Advanced to render time the flip happens first, the checkpoint is skipped, and stepping next→prev back to an item resumes from the frozen launch queue's stale position. "resumes an item played earlier in the SAME session" caught it immediately (748 instead of 830); the ref write stays in its own effect with the ordering written down. The rest: - PlayerModal's keyboard handler is a useEffectEvent, so the "bound once, reads live state" contract is enforced instead of asserted. `focusModal` is a useCallback now — it reads a ref and nothing else, and three hooks take it as a parameter where a fresh identity would spread. - PlexPlayer's session reclaim called `loadSession` from inside loadSession's own useCallback, which the checker reads as a forward reference. Routed through a ref, the file's existing answer for exactly this (detachSessionRef, cancelAutoSkipRef). - PlexPlayer's item-load effect excluded `prefs` by hand so a track pick from the player's own menu would not double-load the session. The two language prefs go through a ref instead — "latest value, not a trigger", said in code rather than in a comment. - PlexPlayer closes its up-next card during the render that changes `id`, not a commit later. - The one PlexPlayer directive that stays is the live session counter, whose cleanup must bump the CURRENT value: the rule's advice to copy it into a variable is precisely wrong there, and the comment now says why rather than pointing at another comment. Suppressions 75 -> 70; exhaustive-deps 20 -> 17.
This commit is contained in:
@@ -208,6 +208,13 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
// Refs for values read inside stable callbacks / the keyboard handler (avoid stale closures).
|
||||
const barAutoHideRef = useRef(prefs.barAutoHide);
|
||||
barAutoHideRef.current = prefs.barAutoHide;
|
||||
// The two language prefs are read only at ITEM LOAD (the detail effect below), so they go through
|
||||
// a ref for the same reason `tRef` above does: naming them in that effect's deps would re-run it
|
||||
// when the user picks a track from the player's own menu, double-loading the session the picker
|
||||
// has already reloaded. The ref says "latest value, but not a trigger" in code, which an excused
|
||||
// dep list only said in prose. Same shape as barAutoHideRef/seekStepRef either side of this.
|
||||
const langPrefsRef = useRef({ audioLang: prefs.audioLang, subLang: prefs.subLang });
|
||||
langPrefsRef.current = { audioLang: prefs.audioLang, subLang: prefs.subLang };
|
||||
const seekStepRef = useRef(prefs.seekStep);
|
||||
seekStepRef.current = prefs.seekStep;
|
||||
const fineSeekStepRef = useRef(prefs.fineSeekStep);
|
||||
@@ -474,7 +481,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
restartsRef.current.at = now;
|
||||
if (restartsRef.current.n < MAX_SESSION_RECLAIMS) {
|
||||
restartsRef.current.n += 1;
|
||||
loadSession(playhead(absRef, loadStartRef), !video.paused);
|
||||
// Through the ref, not by name: this is loadSession calling ITSELF from inside its own
|
||||
// useCallback, which the checker rightly reads as "accessed before it is declared".
|
||||
// The ref (assigned right below the callback) is the file's existing answer to that —
|
||||
// detachSessionRef and cancelAutoSkipRef do the same for the same reason.
|
||||
void loadSessionRef.current(playhead(absRef, loadStartRef), !video.paused);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -497,6 +508,10 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
},
|
||||
[id], // NOT `t` — see tRef above (a language switch must not rebuild this / reload the session).
|
||||
);
|
||||
// The self-restart path above reaches the CURRENT loadSession through this, so the recursion is
|
||||
// not a forward reference to a binding that is still being defined.
|
||||
const loadSessionRef = useRef(loadSession);
|
||||
loadSessionRef.current = loadSession;
|
||||
|
||||
// Audio/subtitle track selection + subtitle rendering. Owns the selected-ordinal state; the shared
|
||||
// media refs stay component-owned (loadSession above + the detail effect below write audioRef too).
|
||||
@@ -552,16 +567,14 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
restartsRef.current = { n: 0, at: 0 };
|
||||
// Restore the per-account audio/subtitle language choice, resolved to THIS item's tracks. Set the
|
||||
// audio ref synchronously so the loadSession below picks the right track on the first request.
|
||||
const aOrd = audioOrdForLang(detail.audio_streams ?? [], prefs.audioLang);
|
||||
const { audioLang, subLang } = langPrefsRef.current;
|
||||
const aOrd = audioOrdForLang(detail.audio_streams ?? [], audioLang);
|
||||
audioRef.current = aOrd;
|
||||
setAudioOrd(aOrd);
|
||||
setSubOrd(subOrdForLang(detail.subtitle_streams ?? [], prefs.subLang));
|
||||
setSubOrd(subOrdForLang(detail.subtitle_streams ?? [], subLang));
|
||||
const resume = detail.status !== "watched" ? detail.position_seconds || 0 : 0;
|
||||
loadSession(resume);
|
||||
// Prefs are applied at item load only; changing one via the picker must NOT re-run this (it would
|
||||
// double-load the session — the picker handlers already reload/toggle).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [detail, id, loadSession]);
|
||||
void loadSession(resume);
|
||||
}, [detail, id, loadSession, audioRef, setAudioOrd, setSubOrd]);
|
||||
// Leaving the item: a session still in flight for it must not land after it, and its stream goes.
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -593,7 +606,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
const v = videoRef.current;
|
||||
return () => {
|
||||
aliveRef.current = false;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- the live counter, as above
|
||||
// The rule's advice — copy the ref into a variable and use the copy — is precisely wrong here.
|
||||
// sessGenRef is a LIVE counter: bumping it is how the unmount tells every in-flight session
|
||||
// that its response no longer belongs to anything, and a value captured when the effect ran
|
||||
// would invalidate a generation that has since been superseded instead of the current one.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
sessGenRef.current++;
|
||||
if (hlsRef.current) {
|
||||
hlsRef.current.destroy();
|
||||
@@ -839,7 +856,14 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
setUpNextOpen(false);
|
||||
if (nextId) go(nextId);
|
||||
}, [nextId, go]);
|
||||
useEffect(() => setUpNextOpen(false), [id]);
|
||||
// The "up next" card belongs to the item that was playing, so a queue advance closes it. Adjusted
|
||||
// during the render that changes `id` rather than in an effect: as an effect it closed one commit
|
||||
// after the new episode had already drawn behind it.
|
||||
const [upNextForId, setUpNextForId] = useState(id);
|
||||
if (upNextForId !== id) {
|
||||
setUpNextForId(id);
|
||||
setUpNextOpen(false);
|
||||
}
|
||||
|
||||
// "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 info overlay) before leaving the
|
||||
|
||||
Reference in New Issue
Block a user