Distinct circular song cards with jump-to-album badge (#953)

* feat(tracks): distinct circular song cards with jump-to-album badge

Single-track cards looked identical to album tiles, so clicking the body
read as album behaviour even though it starts playback. Give them a round
vinyl-style cover (square stays album-only) via a shared .cover-circle
utility, and add a 'To album' badge under the artist that navigates to the
track's album. Card click still plays; the badge is the explicit nav path.

* i18n(tracks): add toAlbum label across 9 locales

* docs(changelog): track-card redesign + to-album badge (#953)
This commit is contained in:
Frank Stellmacher
2026-06-02 20:34:54 +02:00
committed by GitHub
parent 47832632fd
commit cc04a0c93d
13 changed files with 100 additions and 10 deletions
+22 -2
View File
@@ -2,7 +2,7 @@ import type { SubsonicSong } from '../api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack';
import React, { memo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus, Star } from 'lucide-react';
import { Play, ListPlus, Star, Disc3 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { usePlayerStore } from '../store/playerStore';
import { CoverArtImage } from '../cover/CoverArtImage';
@@ -12,6 +12,7 @@ import { COVER_DENSE_RAIL_CELL_CSS_PX } from '../cover/layoutSizes';
import { enqueueAndPlay } from '../utils/playback/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
interface SongCardProps {
song: SubsonicSong;
@@ -41,6 +42,7 @@ function SongCard({
const coverUrl = coverHandle.src;
const psyDrag = useDragDrop();
const { orbitActive, addTrackToOrbit } = useOrbitSongRowBehavior();
const navigateToAlbum = useNavigateToAlbum();
const handlePlay = () => {
if (orbitActive) { addTrackToOrbit(song.id); return; }
@@ -60,6 +62,12 @@ function SongCard({
navigate(`/artist/${song.artistId}`);
};
const handleAlbumClick = (e: React.MouseEvent) => {
if (!song.albumId) return;
e.stopPropagation();
navigateToAlbum(song.albumId);
};
return (
<div
className="song-card card"
@@ -93,7 +101,7 @@ function SongCard({
document.addEventListener('mouseup', onUp);
}}
>
<div className="song-card-cover">
<div className="song-card-cover cover-circle">
{!disableArtwork && coverRef ? (
<CoverArtImage
coverRef={coverRef}
@@ -140,6 +148,18 @@ function SongCard({
onClick={handleArtistClick}
title={song.artist}
>{song.artist}</p>
{song.albumId && (
<button
type="button"
className="song-card-album-badge"
onClick={handleAlbumClick}
aria-label={`${t('tracks.toAlbum')} ${song.album}`}
title={song.album}
>
<Disc3 size={11} />
<span>{t('tracks.toAlbum')}</span>
</button>
)}
{(song.userRating ?? 0) > 0 && (
<div className="song-card-rating" aria-label={`${song.userRating} stars`}>
{Array.from({ length: 5 }, (_, i) => (