s.queue);
const isOpen = useAppSelector((s) => s.player.isQueueOpen);
- const now =
+ const nowEntry =
queue.currentIndex >= 0 ? queue.entries[queue.currentIndex] : undefined;
+ const now = useResolvedQueueEntry(nowEntry);
const upNext = queue.entries
.map((entry, index) => ({ entry, index }))
.filter(({ index }) => index > queue.currentIndex);
@@ -120,33 +123,12 @@ export function QueuePanel() {
{t('queue.nothingNext')}
) : (
upNext.map(({ entry, index }) => (
-
dispatch(goToIndex(index))}
- title={t('queue.doubleClickPlay')}
- >
-
-
-
-
-
-
{entry.title}
-
{entry.artistName}
-
-
-
+ entry={entry}
+ onPlay={() => dispatch(goToIndex(index))}
+ onRemove={() => dispatch(removeFromQueue(index))}
+ />
))
)}
@@ -162,3 +144,40 @@ export function QueuePanel() {
);
}
+
+/** An "up next" row, resolving its display fields against the live Track cache
+ * (same read-through as the now-playing entry) so enrichment updates show. */
+function QueueRow({
+ entry,
+ onPlay,
+ onRemove,
+}: {
+ entry: QueueEntry;
+ onPlay: () => void;
+ onRemove: () => void;
+}) {
+ const { t } = useTranslation();
+ const resolved = useResolvedQueueEntry(entry);
+ const albumTitle = resolved?.albumTitle ?? entry.albumTitle;
+
+ return (
+
+
+
+
+
+
+
{resolved?.title ?? entry.title}
+
{resolved?.artistName ?? entry.artistName}
+
+
+
+ );
+}
diff --git a/src/hooks/useResolvedQueueEntry.ts b/src/hooks/useResolvedQueueEntry.ts
new file mode 100644
index 0000000..16c30af
--- /dev/null
+++ b/src/hooks/useResolvedQueueEntry.ts
@@ -0,0 +1,37 @@
+import { skipToken } from '@reduxjs/toolkit/query';
+import { useGetTrackQuery } from '../api/endpoints/library';
+import type { QueueEntry } from '../store/slices/queue';
+
+export interface ResolvedQueueEntry {
+ trackId: string;
+ title: string;
+ artistName: string;
+ albumTitle: string;
+ durationMs: number;
+ hasCover: boolean;
+}
+
+/**
+ * Merge a queue entry's play-time snapshot with the live `Track` cache.
+ *
+ * The queue slice stores denormalized display fields (title/artist/…) captured
+ * when a track was queued, so they go stale after metadata enrichment updates
+ * the track. This reads through to the RTKQ `Track` cache — invalidated by the
+ * same tags that refresh the library — and prefers its fresh values, falling
+ * back to the snapshot for instant render and offline. Returns undefined when
+ * there is no current entry.
+ */
+export function useResolvedQueueEntry(
+ entry: QueueEntry | undefined,
+): ResolvedQueueEntry | undefined {
+ const { data } = useGetTrackQuery(entry?.trackId ?? skipToken);
+ if (!entry) return undefined;
+ return {
+ trackId: entry.trackId,
+ title: data?.title ?? entry.title,
+ artistName: data?.artistName ?? entry.artistName,
+ albumTitle: data?.albumTitle ?? entry.albumTitle,
+ durationMs: data?.durationMs ?? entry.durationMs,
+ hasCover: data?.hasCover ?? false,
+ };
+}