feat(album): single cover on album detail, track-number rows
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

Album detail header falls back to a track's cover when the album
record itself has none, and each track row hides its per-track art in
favour of a large album-position number, since the header already
shows the album's cover once.
This commit is contained in:
Senko-san
2026-06-13 17:51:55 +03:00
parent f5767ff55e
commit df8c67b368
2 changed files with 46 additions and 15 deletions
+35 -11
View File
@@ -15,6 +15,10 @@ interface Props {
track: Track;
index?: number;
showAlbum?: boolean;
/** Hide cover art and show the track's album position instead — used on
* the album detail page, where the album cover is already shown once in
* the header and per-track art would be redundant. */
hideArt?: boolean;
onAddToPlaylist?: (track: Track) => void;
onEditMetadata?: (track: Track) => void;
onDelete?: (track: Track) => void;
@@ -24,6 +28,7 @@ export function TrackRow({
track,
index,
showAlbum = false,
hideArt = false,
onAddToPlaylist,
onEditMetadata,
onDelete,
@@ -58,24 +63,43 @@ export function TrackRow({
selected={isActive}
style={{
display: 'grid',
gridTemplateColumns: '2rem 2.5rem 1fr auto auto',
gridTemplateColumns: hideArt
? '2.5rem 1fr auto auto'
: '2rem 2.5rem 1fr auto auto',
gap: '0.75rem',
alignItems: 'center',
padding: '0.375rem 0.75rem',
cursor: 'default',
}}
>
<span
style={{
fontSize: '0.75rem',
color: 'var(--color-text-3)',
textAlign: 'right',
}}
>
{isActive && isPlaying ? '▶' : index !== undefined ? index + 1 : ''}
</span>
{!hideArt && (
<span
style={{
fontSize: '0.75rem',
color: 'var(--color-text-3)',
textAlign: 'right',
}}
>
{isActive && isPlaying ? '▶' : index !== undefined ? index + 1 : ''}
</span>
)}
<div className="track-art">
{artUrl ? (
{hideArt ? (
<div
style={{
width: 36,
height: 36,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1.0625rem',
fontWeight: 600,
color: isActive ? 'var(--color-accent)' : 'var(--color-text-3)',
}}
>
{track.trackNumber ?? (index !== undefined ? index + 1 : '')}
</div>
) : artUrl ? (
<img
src={artUrl}
alt=""
+11 -4
View File
@@ -9,16 +9,17 @@ import { TrackRow } from '../../components/track/TrackRow';
import { LoadingSkeleton } from '../../components/common/LoadingSkeleton';
import { ErrorState } from '../../components/common/ErrorState';
import { EmptyState } from '../../components/common/EmptyState';
import { useAppDispatch } from '../../hooks/useAppDispatch';
import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
import { setQueue } from '../../store/slices/queue';
import { formatDuration } from '../../lib/format';
import { getCoverUrl } from '../../api/endpoints/streaming';
import { getCoverUrl, getTrackCoverUrl } from '../../api/endpoints/streaming';
export function AlbumDetailPage() {
const { t } = useTranslation();
const { albumId } = useParams<{ albumId: string }>();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const token = useAppSelector((s) => s.auth.accessToken);
const albumQuery = useGetAlbumQuery(albumId ?? '', { skip: !albumId });
const tracksQuery = useGetAlbumTracksQuery(albumId ?? '', { skip: !albumId });
@@ -42,7 +43,13 @@ export function AlbumDetailPage() {
const album = albumQuery.data;
const tracks = tracksQuery.data ?? [];
const artUrl = getCoverUrl(album?.artUrl);
// The album record itself carries no cover; fall back to a track's cover.
const coverTrack = tracks.find((t) => t.hasCover);
const artUrl =
getCoverUrl(album?.artUrl) ??
(token && coverTrack
? getTrackCoverUrl(coverTrack.id, token, true)
: undefined);
const handlePlayAll = () => {
if (!tracks.length || !album) return;
@@ -178,7 +185,7 @@ export function AlbumDetailPage() {
/>
)}
{tracks.map((track, i) => (
<TrackRow key={track.id} track={track} index={i} />
<TrackRow key={track.id} track={track} index={i} hideArt />
))}
</ScrollArea>
</div>