fix(artist-detail): Last.fm/Wikipedia/Favorite hover keeps button border (#966)

* fix(artist-detail): keep ext-link border visible on hover

Hover used --border-subtle, which on Catppuccin matches --bg-card and
visually erased the rim while only the fill changed. Match btn-surface:
--ctp-surface1 border, --ctp-overlay0 on hover.

* docs(changelog): credit zunoz on Psysonic Discord for PR #966

* fix(playlists): tooltips on Play/Add Songs and song count pluralization

Add data-tooltip to Play and Add Songs in playlist hero; switch
playlists.songs to count-based _one/_other forms (was {{n}} without
i18next plural suffix, breaking spacing and singular).

* fix(playlists): render BPM and optional cols in Suggested Songs rows

PlaylistSuggestions shared column headers with the main tracklist but
its row switch omitted bpm, genre, playCount, and lastPlayed.
This commit is contained in:
cucadmuh
2026-06-03 23:19:51 +03:00
committed by GitHub
parent 5990d84f5a
commit f3a0b3f7af
14 changed files with 71 additions and 15 deletions
+8 -2
View File
@@ -133,7 +133,7 @@ export default function PlaylistHero({
)}
</>
<div className="album-detail-info">
<span>{t('playlists.songs', { n: songs.length })}</span>
<span>{t('playlists.songs', { count: songs.length })}</span>
{songs.length > 0 && <span>· {totalDurationLabel(songs)}</span>}
{playlist.public !== undefined && (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3 }}>
@@ -146,7 +146,12 @@ export default function PlaylistHero({
</div>
<div className="album-detail-actions">
<div className="album-detail-actions-primary">
<button className="btn btn-primary" disabled={songs.length === 0} onClick={handlePlayAll}>
<button
className="btn btn-primary"
disabled={songs.length === 0}
onClick={handlePlayAll}
data-tooltip={t('playlists.playTooltip')}
>
<Play size={15} /> {t('common.play', 'Reproducir')}
</button>
<button
@@ -170,6 +175,7 @@ export default function PlaylistHero({
<button
className={`btn btn-ghost ${searchOpen ? 'active' : ''}`}
onClick={() => { setSearchOpen(v => !v); setSearchQuery(''); setSearchResults([]); setSelectedSearchIds(new Set()); setSearchPlPickerOpen(false); }}
data-tooltip={t('playlists.addSongsTooltip')}
>
<Search size={16} /> {t('playlists.addSongs')}
</button>
@@ -11,9 +11,11 @@ import { usePlaylistLayoutStore } from '../../store/playlistLayoutStore';
import { songToTrack } from '../../utils/playback/songToTrack';
import { getQueueTracksView } from '../../utils/library/queueTrackView';
import { codecLabel } from '../../utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers';
import { formatTrackTime } from '../../utils/format/formatDuration';
import i18n from '../../i18n';
const PL_CENTERED = new Set(['favorite', 'rating', 'duration']);
const PL_CENTERED = new Set(['favorite', 'rating', 'duration', 'playCount', 'bpm']);
interface Props {
songs: SubsonicSong[];
@@ -172,6 +174,18 @@ export default function PlaylistSuggestions({
{(song.suffix || (showBitrate && song.bitRate)) && <span className="track-codec">{codecLabel(song, showBitrate)}</span>}
</div>
);
case 'genre': return (
<div key="genre" className="track-genre">{song.genre ?? '—'}</div>
);
case 'playCount': return (
<div key="playCount" className="track-duration">{song.playCount ?? '—'}</div>
);
case 'lastPlayed': return (
<div key="lastPlayed" className="track-genre">{song.played ? formatLastSeen(song.played, i18n.language, '—') : '—'}</div>
);
case 'bpm': return (
<div key="bpm" className="track-duration">{song.bpm && song.bpm > 0 ? song.bpm : '—'}</div>
);
case 'delete': return (
<div key="delete" className="playlist-row-delete-cell">
<button className="playlist-row-delete-btn" style={{ color: hoveredSuggestionId === song.id ? 'var(--accent)' : undefined }} onClick={e => { e.stopPropagation(); addSong(song); }} data-tooltip={t('playlists.addSong')} data-tooltip-pos="left">
+1 -1
View File
@@ -161,7 +161,7 @@ export default function PlaylistCard({
<span>{displayPlaylistName(pl.name)}</span>
</div>
<div className="album-card-artist">
{t('playlists.songs', { n: filteredSongCountByPlaylist[pl.id] ?? pl.songCount })}
{t('playlists.songs', { count: filteredSongCountByPlaylist[pl.id] ?? pl.songCount })}
{(filteredDurationByPlaylist[pl.id] ?? pl.duration) > 0 && (
<> · {formatHumanHoursMinutes(filteredDurationByPlaylist[pl.id] ?? pl.duration)}</>
)}