feat(plex): up-next end-card at episode end (E6 S4)
At episode end, when a next item exists and the pref is on, show an up-next card (poster + title + ~10s countdown + Cancel) instead of the old instant jump, so binge stays in the viewer's control (D6-A). Countdown or Play now advances; Cancel/✕ dismisses and stays on the finished frame. Lives only on the play-to-the-end path — credits-auto-skip advances earlier and never reaches the card, so the two reconcile by construction (no double-advance). New pref 'upNext' (default on) with a Playback-tab toggle; the card closes on item change and joins the Back/Escape cascade. i18n HU/EN.
This commit is contained in:
@@ -42,6 +42,7 @@ import {
|
||||
import { Ctrl, PanelHead, Skip10 } from "./plexPlayerControls";
|
||||
import { PlexPlayerSettings } from "./PlexPlayerSettings";
|
||||
import { PlexStatsOverlay } from "./PlexStatsOverlay";
|
||||
import { UpNextCard } from "./UpNextCard";
|
||||
import {
|
||||
useAutoSkip,
|
||||
useControlsVisibility,
|
||||
@@ -177,6 +178,12 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
const [statsOpen, setStatsOpen] = useState(false);
|
||||
const statsOpenRef = useRef(false);
|
||||
statsOpenRef.current = statsOpen;
|
||||
// Up-next end-card: shown at episode end (with a next item) instead of an instant jump (S4).
|
||||
const [upNextOpen, setUpNextOpen] = useState(false);
|
||||
const upNextOpenRef = useRef(false);
|
||||
upNextOpenRef.current = upNextOpen;
|
||||
const upNextPrefRef = useRef(prefs.upNext);
|
||||
upNextPrefRef.current = prefs.upNext;
|
||||
const now = useWallClock();
|
||||
|
||||
durationRef.current = duration;
|
||||
@@ -520,7 +527,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
// (absRef 0) — its stray 'ended' must not mark anything watched or cascade another advance.
|
||||
if (absRef.current <= 0) return;
|
||||
api.plexSetState(id, "watched").catch(() => {});
|
||||
if (nextId) go(nextId);
|
||||
// With a next item and the up-next pref on, surface the end-card (poster + countdown + Cancel)
|
||||
// and let IT advance — binge stays in the viewer's control (S4/D6-A). The card only appears here,
|
||||
// on play-to-the-end; credits-auto-skip advances earlier and this handler never fires for it.
|
||||
if (nextId && upNextPrefRef.current) setUpNextOpen(true);
|
||||
else if (nextId) go(nextId);
|
||||
// Nothing queued next → dismiss the player back to the previous page (same as Stop/Escape),
|
||||
// instead of stranding a black, ended <video> on screen.
|
||||
else onClose();
|
||||
@@ -529,6 +540,14 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
return () => video.removeEventListener("ended", onEnded);
|
||||
}, [id, nextId, go, onClose]);
|
||||
|
||||
// Up-next card actions: advance = go to the next item (it was already marked watched on 'ended');
|
||||
// cancel = dismiss and stay on the finished frame. Loading a new item closes any lingering card.
|
||||
const advanceUpNext = useCallback(() => {
|
||||
setUpNextOpen(false);
|
||||
if (nextId) go(nextId);
|
||||
}, [nextId, go]);
|
||||
useEffect(() => setUpNextOpen(false), [id]);
|
||||
|
||||
// "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
|
||||
@@ -540,6 +559,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
} else if (helpOpenRef.current) setHelpOpen(false);
|
||||
else if (infoOpenRef.current) setInfoOpen(false);
|
||||
else if (statsOpenRef.current) setStatsOpen(false);
|
||||
else if (upNextOpenRef.current) setUpNextOpen(false);
|
||||
else onClose();
|
||||
}, [onClose]);
|
||||
|
||||
@@ -651,6 +671,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
} else if (helpOpenRef.current) setHelpOpen(false);
|
||||
else if (infoOpenRef.current) setInfoOpen(false);
|
||||
else if (statsOpenRef.current) setStatsOpen(false);
|
||||
else if (upNextOpenRef.current) setUpNextOpen(false);
|
||||
else if (!fullscreenElement()) onClose();
|
||||
break;
|
||||
}
|
||||
@@ -1169,6 +1190,16 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Up-next end-card (episode end, next item queued, pref on). Non-modal — the finished frame
|
||||
stays behind it; the card's own countdown advances unless the viewer cancels. */}
|
||||
{upNextOpen && nextId && (
|
||||
<UpNextCard
|
||||
nextId={nextId}
|
||||
onAdvance={advanceUpNext}
|
||||
onCancel={() => setUpNextOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Keyboard-shortcut cheat sheet (toggle with "h" or the keyboard button). */}
|
||||
{helpOpen && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user