$Message
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
clearQueue,
|
||||
toggleShuffle,
|
||||
toggleLoop,
|
||||
setExploration,
|
||||
type QueueEntry,
|
||||
} from '../../store/slices/queue';
|
||||
import { toggleQueue } from '../../store/slices/player';
|
||||
@@ -148,7 +149,8 @@ export function QueuePanel() {
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
defaultValue={[42]}
|
||||
value={[Math.round((queue.radio?.exploration ?? 0.25) * 100)]}
|
||||
onValueChange={([v]) => dispatch(setExploration(v / 100))}
|
||||
aria-label="Exploration"
|
||||
/>
|
||||
<span className="lab">{t('queue.new')}</span>
|
||||
@@ -277,6 +279,14 @@ function QueueRow({
|
||||
className="r"
|
||||
text={resolved?.artistName ?? entry.artistName}
|
||||
/>
|
||||
{entry.reason && (
|
||||
<span
|
||||
style={{ fontSize: 11, color: 'var(--color-accent)' }}
|
||||
title={t('queue.whyPlaying')}
|
||||
>
|
||||
{t(`queue.reason.${entry.reason}`)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Menu>
|
||||
<MenuTrigger asChild>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
|
||||
import { useRadioNextMutation } from '../../api/endpoints/radio';
|
||||
import { appendToQueue } from '../../store/slices/queue';
|
||||
import { pickToEntry } from '../../lib/radio';
|
||||
|
||||
// Prefetch more radio tracks once the current position is within this many of
|
||||
// the end of the queue.
|
||||
const LOOKAHEAD = 3;
|
||||
|
||||
/**
|
||||
* Headless controller: while a radio drives the queue, top it up as it drains
|
||||
* (§6.5 / B6 infinite feed). Mounted once in AppShell. A failed prefetch just
|
||||
* stops the feed — playback of what's already queued is unaffected.
|
||||
*/
|
||||
export function RadioController() {
|
||||
const dispatch = useAppDispatch();
|
||||
const radio = useAppSelector((s) => s.queue.radio);
|
||||
const currentIndex = useAppSelector((s) => s.queue.currentIndex);
|
||||
const entries = useAppSelector((s) => s.queue.entries);
|
||||
const [radioNext] = useRadioNextMutation();
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!radio) return;
|
||||
const nearEnd = currentIndex >= entries.length - LOOKAHEAD;
|
||||
if (!nearEnd || loadingRef.current) return;
|
||||
|
||||
loadingRef.current = true;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await radioNext({
|
||||
seedTrackId: radio.seedTrackId ?? undefined,
|
||||
fromLikes: radio.fromLikes,
|
||||
exploration: radio.exploration,
|
||||
count: 10,
|
||||
excludeIds: entries.map((e) => e.trackId),
|
||||
}).unwrap();
|
||||
if (res.picks.length > 0) {
|
||||
dispatch(appendToQueue(res.picks.map(pickToEntry)));
|
||||
}
|
||||
} catch {
|
||||
// best-effort — leave the queue as-is
|
||||
} finally {
|
||||
loadingRef.current = false;
|
||||
}
|
||||
})();
|
||||
}, [radio, currentIndex, entries, radioNext, dispatch]);
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user