fix(queue): resolve real track covers in queue panel
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:
Generated
+14
-1
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user