feat(player): persistent auto-advance + loop playback modes

Two saved-to-account playback settings on the player's prev/next bar (any queued
player — feed or playlist): Auto-advance (Off/Next/Prev/Random — what plays when a
video ends) and Loop (Off / One = repeat the current video / All = wrap the list
at its ends; a single-item list repeats). Stored in users.preferences
(playerAutoAdvance/playerLoop), read from the cached me + written via savePrefs so
they apply everywhere and survive reloads.

The feed's player queue is now the live filtered feed with the watch-state filter
NOT applied — marking the current video watched keeps it in the sequence (no
reindex/reload mid-play), while a hidden video still drops out. Removes the old
frozen-queue workaround and the boolean autoAdvance prop. i18n en/hu/de.
This commit is contained in:
npeter83 2026-07-05 00:20:50 +02:00
parent 597cec6517
commit d02c338465
5 changed files with 161 additions and 27 deletions

View file

@ -306,10 +306,16 @@ export default function Feed({
const loaded: Video[] = (query.data?.pages ?? []).flatMap((p) => p.items);
loadedRef.current = loaded;
const items: Video[] = loaded
.map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
.map((v) => (savedOverrides[v.id] !== undefined ? { ...v, saved: savedOverrides[v.id] } : v))
.filter((v) => matchesView(v.status, filters.show));
const withOverrides = (list: Video[]): Video[] =>
list
.map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
.map((v) => (savedOverrides[v.id] !== undefined ? { ...v, saved: savedOverrides[v.id] } : v));
const items: Video[] = withOverrides(loaded).filter((v) => matchesView(v.status, filters.show));
// The in-app player's queue (auto-advance / loop / prev-next). It's the filtered feed, but the
// watch-state view filter is NOT applied — so marking the current video watched keeps it in the
// sequence (no reindex/reload mid-play), matching "watched doesn't affect the list". A video that
// goes hidden still drops out ("visible affects").
const playerQueue: Video[] = withOverrides(loaded).filter((v) => v.status !== "hidden");
// --- Live YouTube search mode -------------------------------------------------------------
// A separate data source rendered in the same cards/player. No watch-state view filter (the
@ -317,9 +323,8 @@ export default function Feed({
if (ytActive) {
const ytLoaded: Video[] = (ytQuery.data?.pages ?? []).flatMap((p) => p.items);
loadedRef.current = ytLoaded;
const ytItems: Video[] = ytLoaded
.map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
.map((v) => (savedOverrides[v.id] !== undefined ? { ...v, saved: savedOverrides[v.id] } : v));
const ytItems: Video[] = withOverrides(ytLoaded);
const ytPlayerQueue: Video[] = ytItems.filter((v) => v.status !== "hidden");
const ytError =
ytQuery.error instanceof HttpError
? ytQuery.error.detail || t("feed.yt.error")
@ -446,8 +451,7 @@ export default function Feed({
<PlayerModal
video={activeVideo.video}
startAt={activeVideo.startAt}
queue={ytItems}
autoAdvance={false}
queue={ytPlayerQueue}
onClose={() => setActiveVideo(null)}
onState={onState}
onOpenChannel={onOpenChannel}
@ -656,8 +660,7 @@ export default function Feed({
<PlayerModal
video={activeVideo.video}
startAt={activeVideo.startAt}
queue={items}
autoAdvance={false}
queue={playerQueue}
onClose={() => setActiveVideo(null)}
onState={onState}
onOpenChannel={onOpenChannel}