fix(queue): resolve real track covers in queue panel
Docker Build & Publish / build (push) Has been cancelled
Docker Build & Publish / push (push) Has been cancelled
Docker Build & Publish / Prune old image versions (push) Has been cancelled

Queue rows passed albumArtUrl straight to ArtTile, but for tracks that
field is usually empty — the real cover is served per-track from
/tracks/{id}/cover. Apply the same resolution PersistentPlayer uses
(getCoverUrl ?? getTrackCoverUrl) for both the now-playing tile and the
up-next rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Senko-san
2026-06-13 17:06:21 +03:00
parent d1b2b40ffd
commit df2531171e
5 changed files with 47 additions and 9 deletions
+14 -1
View File
@@ -16,7 +16,8 @@
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.8",
"react-redux": "^9.3.0", "react-redux": "^9.3.0",
"react-router": "^7.16.0" "react-router": "^7.16.0",
"use-debounce": "^10.1.1"
}, },
"devDependencies": { "devDependencies": {
"@rsbuild/core": "^2.0.7", "@rsbuild/core": "^2.0.7",
@@ -4469,6 +4470,18 @@
} }
} }
}, },
"node_modules/use-debounce": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.1.tgz",
"integrity": "sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ==",
"license": "MIT",
"engines": {
"node": ">= 16.0.0"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/use-sidecar": { "node_modules/use-sidecar": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+2 -1
View File
@@ -21,7 +21,8 @@
"react-dom": "^19.2.6", "react-dom": "^19.2.6",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.8",
"react-redux": "^9.3.0", "react-redux": "^9.3.0",
"react-router": "^7.16.0" "react-router": "^7.16.0",
"use-debounce": "^10.1.1"
}, },
"devDependencies": { "devDependencies": {
"@rsbuild/core": "^2.0.7", "@rsbuild/core": "^2.0.7",
+16 -1
View File
@@ -12,16 +12,24 @@ import {
import { toggleQueue } from '../../store/slices/player'; import { toggleQueue } from '../../store/slices/player';
import { openTrackInfo } from '../../store/slices/ui'; import { openTrackInfo } from '../../store/slices/ui';
import { useResolvedQueueEntry } from '../../hooks/useResolvedQueueEntry'; import { useResolvedQueueEntry } from '../../hooks/useResolvedQueueEntry';
import { getCoverUrl, getTrackCoverUrl } from '../../api/endpoints/streaming';
export function QueuePanel() { export function QueuePanel() {
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const queue = useAppSelector((s) => s.queue); const queue = useAppSelector((s) => s.queue);
const token = useAppSelector((s) => s.auth.accessToken);
const isOpen = useAppSelector((s) => s.player.isQueueOpen); const isOpen = useAppSelector((s) => s.player.isQueueOpen);
const nowEntry = const nowEntry =
queue.currentIndex >= 0 ? queue.entries[queue.currentIndex] : undefined; queue.currentIndex >= 0 ? queue.entries[queue.currentIndex] : undefined;
const now = useResolvedQueueEntry(nowEntry); const now = useResolvedQueueEntry(nowEntry);
const nowArtUrl = now
? (getCoverUrl(now.albumArtUrl) ??
(token && now.hasCover
? getTrackCoverUrl(now.trackId, token, true)
: undefined))
: undefined;
const upNext = queue.entries const upNext = queue.entries
.map((entry, index) => ({ entry, index })) .map((entry, index) => ({ entry, index }))
.filter(({ index }) => index > queue.currentIndex); .filter(({ index }) => index > queue.currentIndex);
@@ -81,6 +89,7 @@ export function QueuePanel() {
seed={now.albumTitle} seed={now.albumTitle}
size={44} size={44}
label={now.albumTitle} label={now.albumTitle}
src={nowArtUrl}
/> />
<div className="qt"> <div className="qt">
<div className="t">{now.title}</div> <div className="t">{now.title}</div>
@@ -171,8 +180,14 @@ function QueueRow({
onRemove: () => void; onRemove: () => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const token = useAppSelector((s) => s.auth.accessToken);
const resolved = useResolvedQueueEntry(entry); const resolved = useResolvedQueueEntry(entry);
const albumTitle = resolved?.albumTitle ?? entry.albumTitle; const albumTitle = resolved?.albumTitle ?? entry.albumTitle;
const artUrl =
getCoverUrl(resolved?.albumArtUrl) ??
(token && resolved?.hasCover
? getTrackCoverUrl(resolved.trackId, token, true)
: undefined);
return ( return (
<div <div
@@ -183,7 +198,7 @@ function QueueRow({
<span className="grip"> <span className="grip">
<Icon name="dots-six-vertical" /> <Icon name="dots-six-vertical" />
</span> </span>
<ArtTile seed={albumTitle} size={36} label={albumTitle} /> <ArtTile seed={albumTitle} size={36} label={albumTitle} src={artUrl} />
<div className="qt"> <div className="qt">
<div className="t">{resolved?.title ?? entry.title}</div> <div className="t">{resolved?.title ?? entry.title}</div>
<div className="r">{resolved?.artistName ?? entry.artistName}</div> <div className="r">{resolved?.artistName ?? entry.artistName}</div>
+13 -6
View File
@@ -5,9 +5,9 @@ import {
Tabs, Tabs,
TabsList, TabsList,
TabsContent, TabsContent,
SearchField,
ScrollArea, ScrollArea,
Card, Card,
TextField,
} from '@olly/modern-sk'; } from '@olly/modern-sk';
import { import {
useGetTracksQuery, useGetTracksQuery,
@@ -23,6 +23,7 @@ import { setQueue } from '../../store/slices/queue';
import type { Track, Album, Artist } from '../../api/types'; import type { Track, Album, Artist } from '../../api/types';
import { getCoverUrl } from '../../api/endpoints/streaming'; import { getCoverUrl } from '../../api/endpoints/streaming';
import { formatDuration } from '../../lib/format'; import { formatDuration } from '../../lib/format';
import { useDebounce } from 'use-debounce';
export function LibraryPage() { export function LibraryPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -30,10 +31,17 @@ export function LibraryPage() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [tab, setTab] = useState('tracks'); const [tab, setTab] = useState('tracks');
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [debouncedSearch] = useDebounce(search, 300);
const tracksQuery = useGetTracksQuery(search ? { search } : undefined); const tracksQuery = useGetTracksQuery(
const albumsQuery = useGetAlbumsQuery(search ? { search } : undefined); debouncedSearch ? { search } : undefined,
const artistsQuery = useGetArtistsQuery(search ? { search } : undefined); );
const albumsQuery = useGetAlbumsQuery(
debouncedSearch ? { search } : undefined,
);
const artistsQuery = useGetArtistsQuery(
debouncedSearch ? { search } : undefined,
);
const handlePlayAll = (tracks: Track[]) => { const handlePlayAll = (tracks: Track[]) => {
dispatch( dispatch(
@@ -68,11 +76,10 @@ export function LibraryPage() {
{t('library.title')} {t('library.title')}
</h2> </h2>
<div style={{ flex: 1, maxWidth: '20rem' }}> <div style={{ flex: 1, maxWidth: '20rem' }}>
<SearchField <TextField
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder={t('library.searchPlaceholder')} placeholder={t('library.searchPlaceholder')}
icon="⌕"
/> />
</div> </div>
</div> </div>
+2
View File
@@ -9,6 +9,7 @@ export interface ResolvedQueueEntry {
albumTitle: string; albumTitle: string;
durationMs: number; durationMs: number;
hasCover: boolean; hasCover: boolean;
albumArtUrl?: string;
} }
/** /**
@@ -33,5 +34,6 @@ export function useResolvedQueueEntry(
albumTitle: data?.albumTitle ?? entry.albumTitle, albumTitle: data?.albumTitle ?? entry.albumTitle,
durationMs: data?.durationMs ?? entry.durationMs, durationMs: data?.durationMs ?? entry.durationMs,
hasCover: data?.hasCover ?? false, hasCover: data?.hasCover ?? false,
albumArtUrl: data?.albumArtUrl ?? entry.albumArtUrl,
}; };
} }