feat(tracklist): play count, last played, BPM columns + Song Info rows (#730)

* feat(tracklist): play count, last played, and BPM columns (#516)

Adds three opt-in columns to Album / Playlist / Favorites tracklists,
plus the same fields in the Song Info modal. Picks up Navidrome's
existing `playCount` / `played` / `bpm` from the Subsonic response — no
new API calls.

- `SubsonicSong` gains `playCount`, `played`, `bpm` (already populated
  by Navidrome's Subsonic API, just unmapped in the TS model).
- `albumTrackListHelpers.COLUMNS`, `PL_COLUMNS` (PlaylistDetail), and
  `FAV_COLUMNS` (Favorites) get the three new entries. Genre also added
  to the playlist column set for parity with the other two lists.
- Render uses `.track-duration` (12px tabular, centered, muted) for the
  numeric stats and `.track-genre` (11px text, muted) for the relative
  last-played timestamp via the existing `formatLastSeen` helper.
- Sort logic extended in `playlistDisplayedSongs`,
  `useAlbumDetailSort`, and `useFavoritesSongFiltering` for the three
  new keys.
- `SongInfoModal` gets matching rows (BPM / Play count / Last played).
- `useTracklistColumns.gridTemplate` / `gridMinWidth` fall back to the
  ColDef's `defaultWidth` when a visible column has no saved width
  (newly added column on an old prefs blob would otherwise emit
  `undefinedpx` and collapse the row layout until reset-to-defaults).
- BPM cells skip the rendering when Navidrome returns 0 (default for
  untagged files) — show `—` instead of `0`.
- i18n: `albumDetail.trackPlayCount / trackLastPlayed / trackBpm` and
  `songInfo.playCount / lastPlayed / bpm` in all 9 locales.

Suggested by jbigginswyl (#516).

* docs(changelog): tracklist Plays / Last played / BPM columns (#730)
This commit is contained in:
Frank Stellmacher
2026-05-15 22:01:05 +02:00
committed by GitHub
parent 603660d407
commit efd85ffde3
31 changed files with 189 additions and 36 deletions
+12 -9
View File
@@ -26,15 +26,18 @@ import { AddToPlaylistSubmenu } from '../components/ContextMenu';
import GenreFilterBar from '../components/GenreFilterBar';
const FAV_COLUMNS: readonly ColDef[] = [
{ key: 'num', i18nKey: null, minWidth: 60, defaultWidth: 60, required: true },
{ key: 'title', i18nKey: 'trackTitle', minWidth: 150, defaultWidth: 0, required: true, flex: true },
{ key: 'artist', i18nKey: 'trackArtist', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'album', i18nKey: 'trackAlbum', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'genre', i18nKey: 'trackGenre', minWidth: 60, defaultWidth: 120, required: false },
{ key: 'rating', i18nKey: 'trackRating', minWidth: 80, defaultWidth: 120, required: false },
{ key: 'duration', i18nKey: 'trackDuration', minWidth: 72, defaultWidth: 92, required: false },
{ key: 'format', i18nKey: 'trackFormat', minWidth: 60, defaultWidth: 80, required: false },
{ key: 'remove', i18nKey: null, minWidth: 36, defaultWidth: 36, required: true },
{ key: 'num', i18nKey: null, minWidth: 60, defaultWidth: 60, required: true },
{ key: 'title', i18nKey: 'trackTitle', minWidth: 150, defaultWidth: 0, required: true, flex: true },
{ key: 'artist', i18nKey: 'trackArtist', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'album', i18nKey: 'trackAlbum', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'genre', i18nKey: 'trackGenre', minWidth: 60, defaultWidth: 120, required: false },
{ key: 'rating', i18nKey: 'trackRating', minWidth: 80, defaultWidth: 120, required: false },
{ key: 'duration', i18nKey: 'trackDuration', minWidth: 72, defaultWidth: 92, required: false },
{ key: 'format', i18nKey: 'trackFormat', minWidth: 60, defaultWidth: 80, required: false },
{ key: 'playCount', i18nKey: 'trackPlayCount', minWidth: 60, defaultWidth: 80, required: false },
{ key: 'lastPlayed', i18nKey: 'trackLastPlayed', minWidth: 90, defaultWidth: 130, required: false },
{ key: 'bpm', i18nKey: 'trackBpm', minWidth: 50, defaultWidth: 70, required: false },
{ key: 'remove', i18nKey: null, minWidth: 36, defaultWidth: 36, required: true },
];
const CURRENT_YEAR = new Date().getFullYear();
+13 -9
View File
@@ -55,15 +55,19 @@ import { usePlaylistDnDReorder } from '../hooks/usePlaylistDnDReorder';
// ── Column configuration ──────────────────────────────────────────────────────
const PL_COLUMNS: readonly ColDef[] = [
{ key: 'num', i18nKey: null, minWidth: 60, defaultWidth: 60, required: true },
{ key: 'title', i18nKey: 'trackTitle', minWidth: 150, defaultWidth: 0, required: true, flex: true },
{ key: 'artist', i18nKey: 'trackArtist', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'album', i18nKey: 'trackAlbum', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'favorite', i18nKey: 'trackFavorite', minWidth: 50, defaultWidth: 70, required: false },
{ key: 'rating', i18nKey: 'trackRating', minWidth: 80, defaultWidth: 120, required: false },
{ key: 'duration', i18nKey: 'trackDuration', minWidth: 72, defaultWidth: 92, required: false },
{ key: 'format', i18nKey: 'trackFormat', minWidth: 60, defaultWidth: 90, required: false },
{ key: 'delete', i18nKey: null, minWidth: 36, defaultWidth: 36, required: true },
{ key: 'num', i18nKey: null, minWidth: 60, defaultWidth: 60, required: true },
{ key: 'title', i18nKey: 'trackTitle', minWidth: 150, defaultWidth: 0, required: true, flex: true },
{ key: 'artist', i18nKey: 'trackArtist', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'album', i18nKey: 'trackAlbum', minWidth: 80, defaultWidth: 180, required: false },
{ key: 'genre', i18nKey: 'trackGenre', minWidth: 60, defaultWidth: 120, required: false },
{ key: 'favorite', i18nKey: 'trackFavorite', minWidth: 50, defaultWidth: 70, required: false },
{ key: 'rating', i18nKey: 'trackRating', minWidth: 80, defaultWidth: 120, required: false },
{ key: 'duration', i18nKey: 'trackDuration', minWidth: 72, defaultWidth: 92, required: false },
{ key: 'format', i18nKey: 'trackFormat', minWidth: 60, defaultWidth: 90, required: false },
{ key: 'playCount', i18nKey: 'trackPlayCount', minWidth: 60, defaultWidth: 80, required: false },
{ key: 'lastPlayed', i18nKey: 'trackLastPlayed', minWidth: 90, defaultWidth: 130, required: false },
{ key: 'bpm', i18nKey: 'trackBpm', minWidth: 50, defaultWidth: 70, required: false },
{ key: 'delete', i18nKey: null, minWidth: 36, defaultWidth: 36, required: true },
];
export default function PlaylistDetail() {