feat(playlists): queue playback in the player (auto-advance + prev/next)

PlayerModal now accepts an optional queue + startIndex. The active item drives the
player; it recreates per item (reusing the single-video resume / auto-watch / progress
logic), auto-advances to the next item when one ends, and shows Previous / Next controls
with an N / M indicator. The Playlists page passes the playlist as the queue from Play all
or a clicked row. Trilingual previous/next strings.
This commit is contained in:
npeter83 2026-06-15 16:11:37 +02:00
parent dea740b728
commit 4689d2cbbd
5 changed files with 99 additions and 43 deletions

View file

@ -105,7 +105,8 @@ export default function Playlists() {
const [renaming, setRenaming] = useState(false);
const [renameValue, setRenameValue] = useState("");
const [items, setItems] = useState<Video[]>([]);
const [active, setActive] = useState<{ video: Video; startAt: number | null } | null>(null);
// The open player, as an index into `items` (the queue); null = closed.
const [playingIndex, setPlayingIndex] = useState<number | null>(null);
const listQuery = useQuery({ queryKey: ["playlists"], queryFn: () => api.playlists() });
const playlists = listQuery.data ?? [];
@ -312,7 +313,7 @@ export default function Playlists() {
</div>
<div className="flex gap-2 mt-3 flex-wrap">
<button
onClick={() => items[0] && setActive({ video: items[0], startAt: null })}
onClick={() => items.length && setPlayingIndex(0)}
disabled={!items.length}
className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg bg-accent text-accent-fg disabled:opacity-40 hover:opacity-90 transition"
>
@ -353,7 +354,7 @@ export default function Playlists() {
key={v.id}
video={v}
index={i}
onPlay={() => setActive({ video: v, startAt: null })}
onPlay={() => setPlayingIndex(i)}
onRemove={() => removeItem(v.id)}
/>
))}
@ -365,11 +366,13 @@ export default function Playlists() {
)}
</main>
{active && (
{playingIndex != null && items[playingIndex] && (
<PlayerModal
video={active.video}
startAt={active.startAt}
onClose={() => setActive(null)}
video={items[playingIndex]}
queue={items}
startIndex={playingIndex}
startAt={null}
onClose={() => setPlayingIndex(null)}
onState={playerState}
/>
)}