diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx index deae3975..85825658 100644 --- a/frontend/src/components/PlexPlayer.tsx +++ b/frontend/src/components/PlexPlayer.tsx @@ -1,13 +1,4 @@ -import { - Fragment, - lazy, - Suspense, - useCallback, - useEffect, - useRef, - useState, - type WheelEvent as ReactWheelEvent, -} from "react"; +import { Fragment, lazy, Suspense, useCallback, useEffect, useRef, useState } from "react"; import { qk } from "../lib/queryKeys"; import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; @@ -46,7 +37,13 @@ import { type PlexPlayerPrefs, } from "../lib/plexPlayerPrefs"; import { Ctrl, PanelHead, Slider, Toggle } from "./plexPlayerControls"; -import { useControlsVisibility, useFullscreen, useWallClock } from "../lib/plexPlayerHooks"; +import { + useControlsVisibility, + useFullscreen, + usePlayerVolume, + usePlexProgress, + useWallClock, +} from "../lib/plexPlayerHooks"; import { fullscreenElement } from "../lib/fullscreen"; import { LS, useAccountPersistedObject } from "../lib/storage"; import { useDismiss } from "../lib/useDismiss"; @@ -352,51 +349,10 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { }; }, [id, patchPrefs]); - // Periodic + on-unmount progress checkpoint. + // Periodic + on-unmount progress checkpoint (interval flush + pause/pagehide + reopen cache patch). const absRef = useRef(0); absRef.current = abs; - useEffect(() => { - // `final` = a settled checkpoint (pause / page-hide / leaving the player) vs. the periodic tick. - // Only final checkpoints get mirrored to a linked Plex account (backend-side debounce), so Plex - // sees the resume position when the user actually stops — not a timeline call every 10 seconds. - const flush = (final = false) => { - if (absRef.current > 0 && durationRef.current > 0) - api - .plexProgress(id, Math.floor(absRef.current), durationRef.current, final) - .catch(() => {}); - }; - const iv = window.setInterval(flush, 10000); - // Pausing is a settled position — push it to Plex now rather than waiting for unmount/unload. - const onPauseFlush = () => flush(true); - const video = videoRef.current; - video?.addEventListener("pause", onPauseFlush); - // Save on page unload too (F5/close/navigate): React effect cleanup does NOT run on a full reload, - // so without this the resume point lags up to 10s — e.g. a seek right before F5 was lost. Uses a - // keepalive beacon so the POST survives the unload (a final checkpoint — the user is leaving). - const onHide = () => { - if (absRef.current > 0 && durationRef.current > 0) - api.plexProgressBeacon(id, Math.floor(absRef.current), durationRef.current, true); - }; - window.addEventListener("pagehide", onHide); - return () => { - window.clearInterval(iv); - window.removeEventListener("pagehide", onHide); - video?.removeEventListener("pause", onPauseFlush); - flush(true); - // Back (unmount) persists the position server-side via flush(), but reopening the same item in - // the SAME session read react-query's CACHED detail — whose position_seconds predated this - // watch — so the resume landed at the old spot (the progress GET could also race the POST). - // Patch the cached detail here so a reopen resumes from where we left off. (F5 was always fine: - // a reload starts with an empty cache and fetches the fresh position.) Safe to do only on - // unmount — patching while mounted would retrigger the loadSession effect and reseek mid-play. - if (absRef.current > 0 && durationRef.current > 0) { - const pos = Math.floor(absRef.current); - qc.setQueryData(qk.plexItem(id), (old) => (old ? { ...old, position_seconds: pos } : old)); - } - qc.invalidateQueries({ queryKey: qk.plexLibrary() }); - qc.invalidateQueries({ queryKey: qk.plexShow() }); - }; - }, [id, qc]); + usePlexProgress({ id, qc, videoRef, absRef, durationRef }); // --- seeking (native within the loaded region, else restart the session) --------------------- const seekTo = useCallback( @@ -425,41 +381,16 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { else v.pause(); }, []); - const applyVolume = useCallback((vol: number, mute: boolean) => { - const v = videoRef.current; - if (!v) return; - v.volume = clamp01(vol); - v.muted = mute; - }, []); - // Set volume/mute on the