This commit is contained in:
Цвылев Александр Вадимович
2026-07-28 22:15:47 +03:00
parent e45ef785cf
commit e0603c9f42
12 changed files with 254 additions and 1 deletions
+11 -1
View File
@@ -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>
+51
View File
@@ -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;
}