feat(album): single cover on album detail, track-number rows
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:
@@ -15,6 +15,10 @@ interface Props {
|
|||||||
track: Track;
|
track: Track;
|
||||||
index?: number;
|
index?: number;
|
||||||
showAlbum?: boolean;
|
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;
|
onAddToPlaylist?: (track: Track) => void;
|
||||||
onEditMetadata?: (track: Track) => void;
|
onEditMetadata?: (track: Track) => void;
|
||||||
onDelete?: (track: Track) => void;
|
onDelete?: (track: Track) => void;
|
||||||
@@ -24,6 +28,7 @@ export function TrackRow({
|
|||||||
track,
|
track,
|
||||||
index,
|
index,
|
||||||
showAlbum = false,
|
showAlbum = false,
|
||||||
|
hideArt = false,
|
||||||
onAddToPlaylist,
|
onAddToPlaylist,
|
||||||
onEditMetadata,
|
onEditMetadata,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -58,24 +63,43 @@ export function TrackRow({
|
|||||||
selected={isActive}
|
selected={isActive}
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
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',
|
gap: '0.75rem',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0.375rem 0.75rem',
|
padding: '0.375rem 0.75rem',
|
||||||
cursor: 'default',
|
cursor: 'default',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
{!hideArt && (
|
||||||
style={{
|
<span
|
||||||
fontSize: '0.75rem',
|
style={{
|
||||||
color: 'var(--color-text-3)',
|
fontSize: '0.75rem',
|
||||||
textAlign: 'right',
|
color: 'var(--color-text-3)',
|
||||||
}}
|
textAlign: 'right',
|
||||||
>
|
}}
|
||||||
{isActive && isPlaying ? '▶' : index !== undefined ? index + 1 : ''}
|
>
|
||||||
</span>
|
{isActive && isPlaying ? '▶' : index !== undefined ? index + 1 : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<div className="track-art">
|
<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
|
<img
|
||||||
src={artUrl}
|
src={artUrl}
|
||||||
alt=""
|
alt=""
|
||||||
|
|||||||
@@ -9,16 +9,17 @@ import { TrackRow } from '../../components/track/TrackRow';
|
|||||||
import { LoadingSkeleton } from '../../components/common/LoadingSkeleton';
|
import { LoadingSkeleton } from '../../components/common/LoadingSkeleton';
|
||||||
import { ErrorState } from '../../components/common/ErrorState';
|
import { ErrorState } from '../../components/common/ErrorState';
|
||||||
import { EmptyState } from '../../components/common/EmptyState';
|
import { EmptyState } from '../../components/common/EmptyState';
|
||||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
import { useAppDispatch, useAppSelector } from '../../hooks/useAppDispatch';
|
||||||
import { setQueue } from '../../store/slices/queue';
|
import { setQueue } from '../../store/slices/queue';
|
||||||
import { formatDuration } from '../../lib/format';
|
import { formatDuration } from '../../lib/format';
|
||||||
import { getCoverUrl } from '../../api/endpoints/streaming';
|
import { getCoverUrl, getTrackCoverUrl } from '../../api/endpoints/streaming';
|
||||||
|
|
||||||
export function AlbumDetailPage() {
|
export function AlbumDetailPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { albumId } = useParams<{ albumId: string }>();
|
const { albumId } = useParams<{ albumId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const token = useAppSelector((s) => s.auth.accessToken);
|
||||||
|
|
||||||
const albumQuery = useGetAlbumQuery(albumId ?? '', { skip: !albumId });
|
const albumQuery = useGetAlbumQuery(albumId ?? '', { skip: !albumId });
|
||||||
const tracksQuery = useGetAlbumTracksQuery(albumId ?? '', { skip: !albumId });
|
const tracksQuery = useGetAlbumTracksQuery(albumId ?? '', { skip: !albumId });
|
||||||
@@ -42,7 +43,13 @@ export function AlbumDetailPage() {
|
|||||||
|
|
||||||
const album = albumQuery.data;
|
const album = albumQuery.data;
|
||||||
const tracks = tracksQuery.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 = () => {
|
const handlePlayAll = () => {
|
||||||
if (!tracks.length || !album) return;
|
if (!tracks.length || !album) return;
|
||||||
@@ -178,7 +185,7 @@ export function AlbumDetailPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tracks.map((track, i) => (
|
{tracks.map((track, i) => (
|
||||||
<TrackRow key={track.id} track={track} index={i} />
|
<TrackRow key={track.id} track={track} index={i} hideArt />
|
||||||
))}
|
))}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user