feat(plex): sort direction, genre any/all, multi-person filters; fix back-nav + player stop

UAT follow-ups on the Plex filter epic:
- Sort now has an asc/desc toggle (sort_dir), applied to any sort field.
- Genre multi-select gains an Any/All mode (genre_mode: OR vs AND containment).
- Director/actor/studio become multi-value: people AND (titles featuring all selected),
  studios OR; clicking them on the info page stacks (unions) instead of replacing, and
  the sidebar shows each as a removable 'Active' chip.
- fix(history): clicking a metadata filter on the info page now pushes a fresh grid
  entry instead of history.back(), so browser Back returns to the info page rather than
  leaving the Plex module.
- fix(player): fully tear down the <video> + hls on unmount and guard late play() calls,
  so backing out of a just-started video no longer leaves audio playing in the background.
i18n en/hu/de (match any/all, sort direction).
This commit is contained in:
npeter83 2026-07-06 00:15:31 +02:00
parent eefd7e3abd
commit 1982cfa7b9
10 changed files with 160 additions and 60 deletions

View file

@ -53,6 +53,9 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<Hls | null>(null);
// False once unmounted, so a late play() (from MANIFEST_PARSED / loadedmetadata firing after Back)
// can't start a detached <video> playing audio in the background.
const aliveRef = useRef(true);
const wrapRef = useRef<HTMLDivElement>(null);
const sessionStartRef = useRef(0); // absolute offset the current HLS/direct session begins at
const durationRef = useRef(0);
@ -143,7 +146,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
hls.on(Hls.Events.MANIFEST_PARSED, () => {
enableSubs();
setReady(true);
video.play().catch(() => {});
if (aliveRef.current) video.play().catch(() => {});
});
// A fatal HLS error (e.g. the remux session died / the file became unreadable) would
// otherwise leave the spinner up forever — surface it instead.
@ -157,7 +160,7 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
const onMeta = () => {
if (sess.mode === "direct" && startAt > 0) video.currentTime = startAt;
setReady(true);
video.play().catch(() => {});
if (aliveRef.current) video.play().catch(() => {});
video.removeEventListener("loadedmetadata", onMeta);
};
video.addEventListener("loadedmetadata", onMeta);
@ -179,6 +182,28 @@ export default function PlexPlayer({ itemId, onClose }: Props) {
};
}, [detail, loadSession]);
// Full media teardown on unmount (Back): stop + detach the <video> and destroy hls, so a late
// play() can't leave audio playing on a detached element after leaving the player.
useEffect(() => {
return () => {
aliveRef.current = false;
if (hlsRef.current) {
hlsRef.current.destroy();
hlsRef.current = null;
}
const v = videoRef.current;
if (v) {
try {
v.pause();
v.removeAttribute("src");
v.load();
} catch {
/* ignore */
}
}
};
}, []);
// --- time tracking + progress checkpoints ----------------------------------------------------
useEffect(() => {
const video = videoRef.current;