From f3a0b3f7af72fa381efcf1b81e2c09959fb3a31a Mon Sep 17 00:00:00 2001
From: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
Date: Wed, 3 Jun 2026 23:19:51 +0300
Subject: [PATCH] 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.
---
CHANGELOG.md | 9 +++++++++
src/components/playlist/PlaylistHero.tsx | 10 ++++++++--
src/components/playlist/PlaylistSuggestions.tsx | 16 +++++++++++++++-
src/components/playlists/PlaylistCard.tsx | 2 +-
src/locales/de/playlists.ts | 5 ++++-
src/locales/en/playlists.ts | 5 ++++-
src/locales/es/playlists.ts | 5 ++++-
src/locales/fr/playlists.ts | 5 ++++-
src/locales/nb/playlists.ts | 5 ++++-
src/locales/nl/playlists.ts | 5 ++++-
src/locales/ro/playlists.ts | 5 ++++-
src/locales/ru/playlists.ts | 5 ++++-
src/locales/zh/playlists.ts | 5 ++++-
src/styles/components/playlist-edit-modal.css | 4 ++--
14 files changed, 71 insertions(+), 15 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98c83206..fbb800c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -465,6 +465,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Genre Mix loads genres through the scoped catalog (`fetchGenreCatalog` / local index) instead of server-wide `getGenres`, matching the sidebar library filter.
+### Artist detail — external link buttons keep border on hover
+
+**By [@cucadmuh](https://github.com/cucadmuh), reported by zunoz on the Psysonic Discord, PR [#966](https://github.com/Psychotoxical/psysonic/pull/966)**
+
+* Last.fm, Wikipedia, and Favorite used a hover border color that matched the card background — the rim disappeared instead of highlighting the inner fill like Play/Shuffle/Radio (`btn-surface`).
+* Playlist detail — Play and Add Songs now show tooltips like the other header actions; track count uses proper pluralization (`1 song` vs `N songs`) with standard `count` interpolation.
+* Suggested Songs rows now render BPM (and other optional columns like genre, play count, last played) — the column switch was missing those cases.
+
+
### In-page browse — virtual scroll and cover-art priority
**By [@cucadmuh](https://github.com/cucadmuh), PR [#783](https://github.com/Psychotoxical/psysonic/pull/783)**
diff --git a/src/components/playlist/PlaylistHero.tsx b/src/components/playlist/PlaylistHero.tsx
index b04240a3..ea40b1b3 100644
--- a/src/components/playlist/PlaylistHero.tsx
+++ b/src/components/playlist/PlaylistHero.tsx
@@ -133,7 +133,7 @@ export default function PlaylistHero({
)}
>
-
+
{t('common.play', 'Reproducir')}
{ setSearchOpen(v => !v); setSearchQuery(''); setSearchResults([]); setSelectedSearchIds(new Set()); setSearchPlPickerOpen(false); }}
+ data-tooltip={t('playlists.addSongsTooltip')}
>
{t('playlists.addSongs')}
diff --git a/src/components/playlist/PlaylistSuggestions.tsx b/src/components/playlist/PlaylistSuggestions.tsx
index c37690a4..217a70a8 100644
--- a/src/components/playlist/PlaylistSuggestions.tsx
+++ b/src/components/playlist/PlaylistSuggestions.tsx
@@ -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)) && {codecLabel(song, showBitrate)} }
);
+ case 'genre': return (
+
{song.genre ?? '—'}
+ );
+ case 'playCount': return (
+
{song.playCount ?? '—'}
+ );
+ case 'lastPlayed': return (
+
{song.played ? formatLastSeen(song.played, i18n.language, '—') : '—'}
+ );
+ case 'bpm': return (
+
{song.bpm && song.bpm > 0 ? song.bpm : '—'}
+ );
case 'delete': return (
{ e.stopPropagation(); addSong(song); }} data-tooltip={t('playlists.addSong')} data-tooltip-pos="left">
diff --git a/src/components/playlists/PlaylistCard.tsx b/src/components/playlists/PlaylistCard.tsx
index a9b432d3..c5f98480 100644
--- a/src/components/playlists/PlaylistCard.tsx
+++ b/src/components/playlists/PlaylistCard.tsx
@@ -161,7 +161,7 @@ export default function PlaylistCard({
{displayPlaylistName(pl.name)}
- {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)}>
)}
diff --git a/src/locales/de/playlists.ts b/src/locales/de/playlists.ts
index 4bb36c81..f111d701 100644
--- a/src/locales/de/playlists.ts
+++ b/src/locales/de/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'Diese Playlist ist leer.',
addFirstSong: 'Ersten Song hinzufügen',
notFound: 'Playlist nicht gefunden.',
- songs: '{{n}} Songs',
+ songs_one: '{{count}} Song',
+ songs_other: '{{count}} Songs',
playAll: 'Alle abspielen',
+ playTooltip: 'Playlist abspielen',
shuffle: 'Zufallswiedergabe',
addToQueue: 'Zur Warteschlange',
back: 'Zurück zu Playlists',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Nochmals klicken zum Bestätigen',
removeSong: 'Aus Playlist entfernen',
addSongs: 'Songs hinzufügen',
+ addSongsTooltip: 'Bibliothek durchsuchen und Titel hinzufügen',
searchPlaceholder: 'Bibliothek durchsuchen…',
noResults: 'Keine Ergebnisse.',
suggestions: 'Vorgeschlagene Songs',
diff --git a/src/locales/en/playlists.ts b/src/locales/en/playlists.ts
index 272f0d14..11fb75c9 100644
--- a/src/locales/en/playlists.ts
+++ b/src/locales/en/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'This playlist is empty.',
addFirstSong: 'Add your first song',
notFound: 'Playlist not found.',
- songs: '{{n}} songs',
+ songs_one: '{{count}} song',
+ songs_other: '{{count}} songs',
playAll: 'Play All',
+ playTooltip: 'Play playlist',
shuffle: 'Shuffle',
addToQueue: 'Add to Queue',
back: 'Back to Playlists',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Click again to confirm',
removeSong: 'Remove from playlist',
addSongs: 'Add Songs',
+ addSongsTooltip: 'Search your library to add tracks',
searchPlaceholder: 'Search your library…',
noResults: 'No results.',
suggestions: 'Suggested Songs',
diff --git a/src/locales/es/playlists.ts b/src/locales/es/playlists.ts
index 2f7e015e..1544bbb6 100644
--- a/src/locales/es/playlists.ts
+++ b/src/locales/es/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'Esta lista está vacía.',
addFirstSong: 'Agrega tu primera canción',
notFound: 'Lista no encontrada.',
- songs: '{{n}} canciones',
+ songs_one: '{{count}} canción',
+ songs_other: '{{count}} canciones',
playAll: 'Reproducir Todo',
+ playTooltip: 'Reproducir lista',
shuffle: 'Aleatorio',
addToQueue: 'Agregar a Cola',
back: 'Volver a Listas',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Click de nuevo para confirmar',
removeSong: 'Quitar de lista',
addSongs: 'Agregar Canciones',
+ addSongsTooltip: 'Buscar en la biblioteca y agregar canciones',
searchPlaceholder: 'Buscar en tu biblioteca…',
noResults: 'Sin resultados.',
suggestions: 'Canciones Sugeridas',
diff --git a/src/locales/fr/playlists.ts b/src/locales/fr/playlists.ts
index 32da819c..c73d20e5 100644
--- a/src/locales/fr/playlists.ts
+++ b/src/locales/fr/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'Cette playlist est vide.',
addFirstSong: 'Ajouter votre premier titre',
notFound: 'Playlist introuvable.',
- songs: '{{n}} titres',
+ songs_one: '{{count}} titre',
+ songs_other: '{{count}} titres',
playAll: 'Tout lire',
+ playTooltip: 'Lire la playlist',
shuffle: 'Aléatoire',
addToQueue: 'Ajouter à la file',
back: 'Retour aux playlists',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Cliquer à nouveau pour confirmer',
removeSong: 'Retirer de la playlist',
addSongs: 'Ajouter des titres',
+ addSongsTooltip: 'Rechercher dans la bibliothèque et ajouter des titres',
searchPlaceholder: 'Rechercher dans la bibliothèque…',
noResults: 'Aucun résultat.',
suggestions: 'Titres suggérés',
diff --git a/src/locales/nb/playlists.ts b/src/locales/nb/playlists.ts
index 2e59e242..1ea5969f 100644
--- a/src/locales/nb/playlists.ts
+++ b/src/locales/nb/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'Denne spillelisten er tom.',
addFirstSong: 'Legg til din første sang',
notFound: 'Spillelisten ble ikke funnet.',
- songs: '{{n}} sanger',
+ songs_one: '{{count}} sang',
+ songs_other: '{{count}} sanger',
playAll: 'Spill alle',
+ playTooltip: 'Spill spilleliste',
shuffle: 'Bland',
addToQueue: 'Legg til i kø',
back: 'Tilbake til spillelister',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Klikk igjen for å bekrefte',
removeSong: 'Fjern fra spilleliste',
addSongs: 'Legg til sanger',
+ addSongsTooltip: 'Søk i biblioteket og legg til sanger',
searchPlaceholder: 'Søk i biblioteket ditt…',
noResults: 'Ingen resultater.',
suggestions: 'Foreslåtte sanger',
diff --git a/src/locales/nl/playlists.ts b/src/locales/nl/playlists.ts
index 87d7c1a7..ba1c93f9 100644
--- a/src/locales/nl/playlists.ts
+++ b/src/locales/nl/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'Deze playlist is leeg.',
addFirstSong: 'Voeg je eerste nummer toe',
notFound: 'Playlist niet gevonden.',
- songs: '{{n}} nummers',
+ songs_one: '{{count}} nummer',
+ songs_other: '{{count}} nummers',
playAll: 'Alles afspelen',
+ playTooltip: 'Playlist afspelen',
shuffle: 'Willekeurig',
addToQueue: 'Aan wachtrij toevoegen',
back: 'Terug naar playlists',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Nogmaals klikken om te bevestigen',
removeSong: 'Uit playlist verwijderen',
addSongs: 'Nummers toevoegen',
+ addSongsTooltip: 'Bibliotheek doorzoeken en nummers toevoegen',
searchPlaceholder: 'Doorzoek bibliotheek…',
noResults: 'Geen resultaten.',
suggestions: 'Aanbevolen nummers',
diff --git a/src/locales/ro/playlists.ts b/src/locales/ro/playlists.ts
index d1734d63..81da1aa8 100644
--- a/src/locales/ro/playlists.ts
+++ b/src/locales/ro/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'Acest playlist este gol.',
addFirstSong: 'Adaugă prima ta piesă',
notFound: 'Playlistul nu a fost găsit.',
- songs: '{{n}} piese',
+ songs_one: '{{count}} piesă',
+ songs_other: '{{count}} piese',
playAll: 'Redă tot',
+ playTooltip: 'Redă playlistul',
shuffle: 'Amestecă',
addToQueue: 'Adaugă la Coadă',
back: 'Înapoi la Playlisturi',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Apasă din nou pentru a confirma',
removeSong: 'Șterge din playlist',
addSongs: 'Adaugă Piese',
+ addSongsTooltip: 'Caută în librărie și adaugă piese',
searchPlaceholder: 'Caută în librărie…',
noResults: 'Niciun rezultat.',
suggestions: 'Piese sugerate',
diff --git a/src/locales/ru/playlists.ts b/src/locales/ru/playlists.ts
index 7aee0d16..7b90f20d 100644
--- a/src/locales/ru/playlists.ts
+++ b/src/locales/ru/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: 'Плейлист пуст.',
addFirstSong: 'Добавьте первый трек',
notFound: 'Плейлист не найден.',
- songs: 'Треков: {{n}}',
+ songs_one: '{{count}} трек',
+ songs_other: '{{count}} треков',
playAll: 'Воспроизвести всё',
+ playTooltip: 'Воспроизвести плейлист',
shuffle: 'Перемешать',
addToQueue: 'В очередь',
back: 'К списку плейлистов',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: 'Нажмите ещё раз для подтверждения',
removeSong: 'Убрать из плейлиста',
addSongs: 'Добавить треки',
+ addSongsTooltip: 'Найти треки в библиотеке и добавить в плейлист',
searchPlaceholder: 'Поиск по библиотеке…',
noResults: 'Ничего не найдено.',
suggestions: 'Рекомендации',
diff --git a/src/locales/zh/playlists.ts b/src/locales/zh/playlists.ts
index b17b1074..9ed73631 100644
--- a/src/locales/zh/playlists.ts
+++ b/src/locales/zh/playlists.ts
@@ -9,8 +9,10 @@ export const playlists = {
emptyPlaylist: '此播放列表为空。',
addFirstSong: '添加第一首歌曲',
notFound: '未找到播放列表。',
- songs: '{{n}} 首歌曲',
+ songs_one: '{{count}} 首歌曲',
+ songs_other: '{{count}} 首歌曲',
playAll: '全部播放',
+ playTooltip: '播放播放列表',
shuffle: '随机播放',
addToQueue: '添加到队列',
back: '返回播放列表',
@@ -18,6 +20,7 @@ export const playlists = {
confirmDelete: '再次点击确认删除',
removeSong: '从播放列表中移除',
addSongs: '添加歌曲',
+ addSongsTooltip: '搜索音乐库并添加歌曲',
searchPlaceholder: '搜索音乐库…',
noResults: '无结果。',
suggestions: '推荐歌曲',
diff --git a/src/styles/components/playlist-edit-modal.css b/src/styles/components/playlist-edit-modal.css
index fc2b5ba0..b8cb703f 100644
--- a/src/styles/components/playlist-edit-modal.css
+++ b/src/styles/components/playlist-edit-modal.css
@@ -216,7 +216,7 @@
padding: 5px 12px;
border-radius: var(--radius-sm);
background: var(--bg-card);
- border: 1px solid var(--border);
+ border: 1px solid var(--ctp-surface1);
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
@@ -227,7 +227,7 @@
.artist-ext-link:hover {
background: var(--bg-hover);
color: var(--text-primary);
- border-color: var(--border-subtle);
+ border-color: var(--ctp-overlay0);
}
/* Bio section */