mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
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:
committed by
GitHub
parent
603660d407
commit
efd85ffde3
@@ -8,23 +8,26 @@ export function codecLabel(song: { suffix?: string; bitRate?: number }, showBitr
|
||||
}
|
||||
|
||||
export const 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: '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: 'genre', i18nKey: 'trackGenre', minWidth: 60, defaultWidth: 90, required: false },
|
||||
{ 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: '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: 'genre', i18nKey: 'trackGenre', 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 },
|
||||
];
|
||||
|
||||
export type ColKey = 'num' | 'title' | 'artist' | 'favorite' | 'rating' | 'duration' | 'format' | 'genre';
|
||||
export type ColKey = 'num' | 'title' | 'artist' | 'favorite' | 'rating' | 'duration' | 'format' | 'genre' | 'playCount' | 'lastPlayed' | 'bpm';
|
||||
|
||||
export const CENTERED_COLS = new Set<ColKey>(['favorite', 'rating', 'duration']);
|
||||
export const CENTERED_COLS = new Set<ColKey>(['favorite', 'rating', 'duration', 'playCount', 'bpm']);
|
||||
|
||||
export type SortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration';
|
||||
export type SortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm';
|
||||
|
||||
export const SORTABLE_COLS = new Set<ColKey | 'album'>(['title', 'artist', 'album', 'favorite', 'rating', 'duration']);
|
||||
export const SORTABLE_COLS = new Set<ColKey | 'album'>(['title', 'artist', 'album', 'favorite', 'rating', 'duration', 'playCount', 'lastPlayed', 'bpm']);
|
||||
|
||||
export function isSortable(key: ColKey | string): key is SortKey {
|
||||
return SORTABLE_COLS.has(key as ColKey);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
|
||||
export type PlaylistSortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration';
|
||||
export type PlaylistSortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm';
|
||||
export type PlaylistSortDir = 'asc' | 'desc';
|
||||
|
||||
export interface DisplayedSongsOptions {
|
||||
@@ -31,6 +31,9 @@ export function getDisplayedSongs(songs: SubsonicSong[], opts: DisplayedSongsOpt
|
||||
case 'favorite': av = effectiveStarred(a); bv = effectiveStarred(b); break;
|
||||
case 'rating': av = effectiveRating(a); bv = effectiveRating(b); break;
|
||||
case 'duration': av = a.duration ?? 0; bv = b.duration ?? 0; break;
|
||||
case 'playCount': av = a.playCount ?? 0; bv = b.playCount ?? 0; break;
|
||||
case 'lastPlayed': av = a.played ? Date.parse(a.played) || 0 : 0; bv = b.played ? Date.parse(b.played) || 0 : 0; break;
|
||||
case 'bpm': av = a.bpm ?? 0; bv = b.bpm ?? 0; break;
|
||||
default: av = a.title; bv = b.title;
|
||||
}
|
||||
if (typeof av === 'number' && typeof bv === 'number') {
|
||||
|
||||
@@ -71,7 +71,14 @@ export function useTracklistColumns(columns: readonly ColDef[], storageKey: stri
|
||||
const gridTemplate = useMemo(
|
||||
() =>
|
||||
visibleCols
|
||||
.map(c => (c.flex ? `minmax(${c.minWidth}px, 1fr)` : `${colWidths[c.key]}px`))
|
||||
.map(c => {
|
||||
if (c.flex) return `minmax(${c.minWidth}px, 1fr)`;
|
||||
// Defensive fallback: a column added since the last persist would have
|
||||
// no saved width, leaving the grid template with `undefinedpx` and
|
||||
// collapsing the row visually until the user resets defaults.
|
||||
const w = colWidths[c.key];
|
||||
return `${typeof w === 'number' && w > 0 ? w : c.defaultWidth}px`;
|
||||
})
|
||||
.join(' '),
|
||||
[visibleCols, colWidths],
|
||||
);
|
||||
@@ -83,7 +90,11 @@ export function useTracklistColumns(columns: readonly ColDef[], storageKey: stri
|
||||
const gapPx = 12; // --space-3
|
||||
const boxPaddingH = 24; // var(--space-3) * 2
|
||||
const colSum = visibleCols.reduce<number>(
|
||||
(s, c) => s + (c.flex ? c.minWidth : colWidths[c.key]),
|
||||
(s, c) => {
|
||||
if (c.flex) return s + c.minWidth;
|
||||
const w = colWidths[c.key];
|
||||
return s + (typeof w === 'number' && w > 0 ? w : c.defaultWidth);
|
||||
},
|
||||
0,
|
||||
);
|
||||
const gaps = Math.max(0, visibleCols.length - 1) * gapPx;
|
||||
|
||||
Reference in New Issue
Block a user