feat(playlists): keep the player queue live across tabs
Track the playing item by id instead of a frozen index, deriving the index from the live queue so the N / M counter and prev/next neighbours stay correct when the playlist changes underneath (e.g. an item removed in another tab) without disrupting playback of the current video. Refetch the playlist list/detail on window focus so such changes flow in, updating the player's queue length in near-real-time.
This commit is contained in:
parent
4689d2cbbd
commit
ddb1890e84
2 changed files with 20 additions and 6 deletions
|
|
@ -203,8 +203,15 @@ export default function PlayerModal({
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
// The active item is queue[index] (falls back to the single opened video).
|
// Track the playing item by id (not a frozen index) so it survives the queue changing
|
||||||
const [index, setIndex] = useState(startIndex ?? 0);
|
// under us — e.g. an item removed in another tab. The index is derived from the *live*
|
||||||
|
// queue, so the N / M counter and prev/next neighbours stay correct without disrupting
|
||||||
|
// playback of the current video.
|
||||||
|
const [playingId, setPlayingId] = useState<string>(
|
||||||
|
queue?.[startIndex ?? 0]?.id ?? video.id
|
||||||
|
);
|
||||||
|
let index = queue ? queue.findIndex((v) => v.id === playingId) : 0;
|
||||||
|
if (index < 0) index = 0;
|
||||||
const active = queue && queue[index] ? queue[index] : video;
|
const active = queue && queue[index] ? queue[index] : video;
|
||||||
const hasQueue = !!queue && queue.length > 1;
|
const hasQueue = !!queue && queue.length > 1;
|
||||||
// startAt only applies to the item we opened with; advanced items resume their own pos.
|
// startAt only applies to the item we opened with; advanced items resume their own pos.
|
||||||
|
|
@ -369,7 +376,7 @@ export default function PlayerModal({
|
||||||
autoMarkedRef.current = true;
|
autoMarkedRef.current = true;
|
||||||
setWatched(true);
|
setWatched(true);
|
||||||
}
|
}
|
||||||
if (queue && index < queue.length - 1) setIndex(index + 1);
|
if (queue && index < queue.length - 1) setPlayingId(queue[index + 1].id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -418,7 +425,7 @@ export default function PlayerModal({
|
||||||
{hasQueue && (
|
{hasQueue && (
|
||||||
<div className="flex items-center justify-center gap-5 px-4 py-2 border-b border-border text-sm">
|
<div className="flex items-center justify-center gap-5 px-4 py-2 border-b border-border text-sm">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIndex((i) => Math.max(0, i - 1))}
|
onClick={() => index > 0 && setPlayingId(queue![index - 1].id)}
|
||||||
disabled={index === 0}
|
disabled={index === 0}
|
||||||
className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
|
className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
|
||||||
>
|
>
|
||||||
|
|
@ -428,7 +435,7 @@ export default function PlayerModal({
|
||||||
{index + 1} / {queue!.length}
|
{index + 1} / {queue!.length}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIndex((i) => Math.min(queue!.length - 1, i + 1))}
|
onClick={() => index < queue!.length - 1 && setPlayingId(queue![index + 1].id)}
|
||||||
disabled={index === queue!.length - 1}
|
disabled={index === queue!.length - 1}
|
||||||
className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
|
className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,11 @@ export default function Playlists() {
|
||||||
// The open player, as an index into `items` (the queue); null = closed.
|
// The open player, as an index into `items` (the queue); null = closed.
|
||||||
const [playingIndex, setPlayingIndex] = useState<number | null>(null);
|
const [playingIndex, setPlayingIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const listQuery = useQuery({ queryKey: ["playlists"], queryFn: () => api.playlists() });
|
const listQuery = useQuery({
|
||||||
|
queryKey: ["playlists"],
|
||||||
|
queryFn: () => api.playlists(),
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
const playlists = listQuery.data ?? [];
|
const playlists = listQuery.data ?? [];
|
||||||
|
|
||||||
// Keep the selection valid: default to the first playlist, and if the selected one
|
// Keep the selection valid: default to the first playlist, and if the selected one
|
||||||
|
|
@ -127,6 +131,9 @@ export default function Playlists() {
|
||||||
queryKey: ["playlist", selectedId],
|
queryKey: ["playlist", selectedId],
|
||||||
queryFn: () => api.playlist(selectedId as number),
|
queryFn: () => api.playlist(selectedId as number),
|
||||||
enabled: selectedId != null,
|
enabled: selectedId != null,
|
||||||
|
// Refetch when returning to the tab so a change made elsewhere (another tab/device)
|
||||||
|
// shows up — including the player's live N / M count, which reads the queue length.
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
});
|
});
|
||||||
const detail = detailQuery.data;
|
const detail = detailQuery.data;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue