fix(player): freeze the launch queue so Loop All + Next/Previous use the original list

The player's queue came live from the feed query, which refetches under a
watch-state filter as each video is auto-marked watched — steadily shrinking
the list mid-session. Loop "All" could then never wrap back to the first
video (earlier items had dropped out), and prev/next drifted. Snapshot the
queue once at mount (useState lazy init) and step through that frozen list for
the whole session; the live prop is ignored thereafter. Supersedes the earlier
active-item pin with a list-level freeze that also fixes Loop All.
This commit is contained in:
npeter83 2026-07-05 20:50:05 +02:00
parent 0fff5ad35d
commit c84cb22e5a
2 changed files with 17 additions and 15 deletions

View file

@ -50,7 +50,7 @@ function loadYouTubeApi(): Promise<any> {
export default function PlayerModal({
video,
startAt,
queue,
queue: queueProp,
startIndex,
onClose,
onState,
@ -75,6 +75,14 @@ export default function PlayerModal({
const qc = useQueryClient();
// Browser/mouse Back closes the player instead of leaving the page.
useBackToClose(onClose);
// Freeze the launch queue: the player steps through the SAME filtered + sorted list it was
// opened with, for its whole session. The live feed refetches and reorders/drops items under
// us — e.g. auto-marking each video watched near its end removes it from a filtered feed — which
// would otherwise shrink the queue mid-playback and break prev/next + Loop "All" (it could never
// wrap back to the first item once earlier items dropped out). Snapshot once at mount via
// useState's lazy initializer and ignore later prop changes. Items that turn out gone/
// non-embeddable still surface the existing playback-error overlay when reached.
const [queue] = useState(() => queueProp);
// Precise upload date shown inline after the relative time (e.g. "9 yr ago · 12 Jan 2017").
const fullDate = (d: string | null | undefined) =>
d
@ -93,18 +101,12 @@ export default function PlayerModal({
// `video` in the queue (the feed passes its whole list + the clicked video, no index).
startIndex != null ? queue?.[startIndex]?.id ?? video.id : video.id
);
// Resolve the playing item from the LIVE queue so prev/next + the N/M counter track it. But
// the feed query reorders/refetches under us — e.g. auto-marking the current video watched near
// its end invalidates the feed — and the playing item can drop OUT of the loaded queue. When
// that happens we must KEEP PLAYING it, not snap back to queue[0] (which would silently jump to
// an unrelated video mid-playback, ignoring loop/auto-advance). So pin the last resolved video
// in a ref and fall back to it; neighbour stepping just treats it as sitting before queue[0].
const foundIndex = queue ? queue.findIndex((v) => v.id === playingId) : -1;
const inQueue = foundIndex >= 0;
const activeRef = useRef<Video>(video);
const active = inQueue ? queue![foundIndex] : activeRef.current;
activeRef.current = active;
const index = inQueue ? foundIndex : 0;
// Locate the playing item in the frozen launch queue → drives prev/next + the N/M counter.
// Because the queue is frozen, the playing id always stays in it (unlike the old live queue,
// where a just-watched item could drop out and snap playback to queue[0]).
let index = queue ? queue.findIndex((v) => v.id === playingId) : 0;
if (index < 0) index = 0;
const active = queue && queue[index] ? queue[index] : video;
const hasQueue = !!queue && queue.length > 1;
// startAt only applies to the item we opened with; advanced items resume their own pos.
const resumeAt =

View file

@ -17,9 +17,9 @@ export const RELEASE_NOTES: ReleaseEntry[] = [
{
version: "0.23.2",
date: "2026-07-05",
summary: "Fixed the player jumping to a different video when the current one is marked watched.",
summary: "The player now steps through the exact list it was opened with, so watching along no longer breaks Loop and Next/Previous.",
fixes: [
"Player: watching a video to the end (or otherwise marking it watched) no longer makes playback jump to an unrelated video. This also fixes Loop “One”, which the jump could override — the current video now correctly repeats even after it drops out of the live feed order.",
"Player: it now plays through a frozen snapshot of the filtered, sorted list it was launched with. Previously, as each video was auto-marked watched it dropped out of a filtered feed, silently shrinking the queue mid-playback — which jumped playback to an unrelated video, and stopped Loop “All” from ever wrapping back to the first video once earlier ones were watched. Next/Previous and both Loop modes now work against the original list for the whole session.",
],
},
{