From f90c12beaaf8ca7fbd48902838e14cef3b513423 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 11 Jul 2026 23:13:10 +0200 Subject: [PATCH] fix(plex): player marker-skip reset, lang-switch reload, tracks wheel, sidebar badge clamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PP1: reset cancelledMarkerRef on item change — a cancelled intro/credits auto-skip on one episode suppressed a same-offset marker on the next (auto-skip silently dead for the rest of a binge). - PP2: loadSession no longer depends on the i18n `t` (read via a ref). A mid-playback language switch rebuilt loadSession → re-ran the detail effect → reloaded the session from the STALE saved resume position, losing live progress. - PP3: tracks (audio/subtitle) menu now stopPropagation on wheel, like the gear menu — scrolling the list no longer changes volume. - PS1: collapsed PlexSidebar filter badge clamps to "9+" (was a raw count), matching the feed Sidebar. --- frontend/src/components/PlexPlayer.tsx | 23 ++++++++++++++++++----- frontend/src/components/PlexSidebar.tsx | 2 +- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx index fe9848e..dd98ae7 100644 --- a/frontend/src/components/PlexPlayer.tsx +++ b/frontend/src/components/PlexPlayer.tsx @@ -172,6 +172,12 @@ function fmt(t: number): string { export default function PlexPlayer({ itemId, onClose, queue }: Props) { const { t } = useTranslation(); + // `t` changes identity on a language switch. loadSession only uses it for error strings, so read it + // through a ref — keeping it OUT of loadSession's deps. Otherwise a mid-playback language change + // rebuilt loadSession → re-ran the detail effect → reloaded the session from the STALE saved resume + // position (losing live progress). See the detail effect's deps. + const tRef = useRef(t); + tRef.current = t; const qc = useQueryClient(); const [id, setId] = useState(itemId); const detailQ = useQuery({ queryKey: ["plex-item", id], queryFn: () => api.plexItem(id) }); @@ -284,10 +290,10 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { // needs full transcoding (a later phase). setLoadError( e?.status === 501 - ? t("plex.player.errTranscode") + ? tRef.current("plex.player.errTranscode") : e?.status === 404 - ? t("plex.player.errNotFound") - : t("plex.player.errGeneric"), + ? tRef.current("plex.player.errNotFound") + : tRef.current("plex.player.errGeneric"), ); return; } @@ -339,7 +345,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { // A fatal HLS error (e.g. the remux session died / the file became unreadable) would // otherwise leave the spinner up forever — surface it instead. hls.on(Hls.Events.ERROR, (_e, data) => { - if (data.fatal) setLoadError(t("plex.player.errGeneric")); + if (data.fatal) setLoadError(tRef.current("plex.player.errGeneric")); }); } else { // direct file (or Safari native HLS) @@ -354,7 +360,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { video.addEventListener("loadedmetadata", onMeta); } }, - [id, t], + [id], // NOT `t` — see tRef above (a language switch must not rebuild this / reload the session). ); // Start playback when the detail arrives (resume from the saved position unless already watched). @@ -841,6 +847,12 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { const [skipProgress, setSkipProgress] = useState(null); skipProgressRef.current = skipProgress; const cancelledMarkerRef = useRef(null); + // Forget a cancelled-skip when the item changes: the ref holds a `${type}-${start_s}` key, and a + // marker at the same offset on the NEXT episode would otherwise be suppressed by a cancel made on + // the previous one (auto-skip silently dead for the rest of a binge). + useEffect(() => { + cancelledMarkerRef.current = null; + }, [id]); // The running countdown interval, so cancelAutoSkip can actually STOP it — clearing skipProgress // alone left the interval ticking, which re-set the progress ~100ms later and skipped anyway. const skipTimerRef = useRef(null); @@ -1212,6 +1224,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
e.stopPropagation()} + onWheel={(e) => e.stopPropagation()} className="glass-menu absolute bottom-full right-0 mb-2 w-56 max-h-[60vh] overflow-auto rounded-xl p-2 text-sm text-white shadow-2xl" > {detail.audio_streams.length > 1 && ( diff --git a/frontend/src/components/PlexSidebar.tsx b/frontend/src/components/PlexSidebar.tsx index dce76cb..65ee75c 100644 --- a/frontend/src/components/PlexSidebar.tsx +++ b/frontend/src/components/PlexSidebar.tsx @@ -133,7 +133,7 @@ export default function PlexSidebar({ {activeCount > 0 && ( - {activeCount} + {activeCount > 9 ? "9+" : activeCount} )}