!isRadio && !showPreviewMeta && currentTrack?.artistId && navigate(`/artist/${currentTrack.artistId}`)}
/>
)}
- {currentTrack && !isRadio && !showPreviewMeta && (
+ {currentTrack && !isRadio && !showPreviewMeta && isLayoutVisible('starRating') && (
{ setUserRatingOverride(currentTrack.id, r); setRating(currentTrack.id, r).catch(() => {}); }}
@@ -156,7 +164,7 @@ export function PlayerTrackInfo({
)}
- {currentTrack && !isRadio && (
+ {currentTrack && !isRadio && isLayoutVisible('favorite') && (
)}
- {currentTrack && !isRadio && lastfmSessionKey && (
+ {currentTrack && !isRadio && lastfmSessionKey && isLayoutVisible('lastfmLove') && (
+
+ }
+ advanced
+ action={
+ usePlayerBarLayoutStore.getState().reset()}
+ data-tooltip={t('settings.playerBarReset')}
+ aria-label={t('settings.playerBarReset')}
+ >
+
+
+ }
+ >
+
+
>
);
}
diff --git a/src/components/settings/PlayerBarLayoutCustomizer.tsx b/src/components/settings/PlayerBarLayoutCustomizer.tsx
new file mode 100644
index 00000000..97dde109
--- /dev/null
+++ b/src/components/settings/PlayerBarLayoutCustomizer.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { Heart, PictureInPicture2, SlidersVertical, Star } from 'lucide-react';
+import LastfmIcon from '../LastfmIcon';
+import {
+ usePlayerBarLayoutStore,
+ type PlayerBarLayoutItemId,
+} from '../../store/playerBarLayoutStore';
+
+const PLAYER_BAR_LAYOUT_LABEL_KEYS: Record = {
+ starRating: 'settings.playerBarStarRating',
+ favorite: 'settings.playerBarFavorite',
+ lastfmLove: 'settings.playerBarLastfmLove',
+ equalizer: 'settings.playerBarEqualizer',
+ miniPlayer: 'settings.playerBarMiniPlayer',
+};
+
+const PLAYER_BAR_LAYOUT_ICONS: Record = {
+ starRating: ,
+ favorite: ,
+ lastfmLove: (
+
+
+
+ ),
+ equalizer: ,
+ miniPlayer: ,
+};
+
+export function PlayerBarLayoutCustomizer() {
+ const { t } = useTranslation();
+ const items = usePlayerBarLayoutStore(s => s.items);
+ const toggleItem = usePlayerBarLayoutStore(s => s.toggleItem);
+
+ return (
+
+ {items.map((it) => {
+ const label = t(PLAYER_BAR_LAYOUT_LABEL_KEYS[it.id]);
+ return (
+
+ {PLAYER_BAR_LAYOUT_ICONS[it.id]}
+ {label}
+
+ toggleItem(it.id)} />
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/settings/settingsTabs.ts b/src/components/settings/settingsTabs.ts
index 343619b4..70324e62 100644
--- a/src/components/settings/settingsTabs.ts
+++ b/src/components/settings/settingsTabs.ts
@@ -46,6 +46,8 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'personalisation',titleKey: 'settings.artistLayoutTitle', keywords: 'artist page layout sections order' },
{ tab: 'personalisation',titleKey: 'settings.homeCustomizerTitle', keywords: 'home page customize sections' },
{ tab: 'personalisation',titleKey: 'settings.queueToolbarTitle', keywords: 'queue toolbar buttons reorder customize shuffle save load' },
+ { tab: 'personalisation',titleKey: 'settings.playlistLayoutTitle', keywords: 'playlist page layout add songs import csv download zip cache offline suggestions controls hide show' },
+ { tab: 'personalisation',titleKey: 'settings.playerBarTitle', keywords: 'player bar playback favorites stars rating lastfm love equalizer mini player controls hide show' },
{ tab: 'appearance', titleKey: 'settings.libraryGridMaxColumnsTitle', keywords: 'grid columns album artist playlist cards layout appearance performance scroll paint' },
{ tab: 'library', titleKey: 'settings.randomMixTitle', keywords: 'random mix blacklist genre keywords filter audiobook' },
{ tab: 'library', titleKey: 'settings.ratingsSectionTitle', keywords: 'ratings stars skip threshold manual' },
diff --git a/src/config/settingsCredits.ts b/src/config/settingsCredits.ts
index a24fe3bc..8e0b6508 100644
--- a/src/config/settingsCredits.ts
+++ b/src/config/settingsCredits.ts
@@ -169,6 +169,7 @@ const CONTRIBUTOR_ENTRIES = [
'Community themes — redesign pass: removed 5 overlapping / eye-straining palettes, added 8 new dark themes (Obsidian Black, Carbon Grey, Volcanic Dark, Forest Green, Violet Haze, Copper Oxide, Sakura Night, Obsidian Gold) (PR #490)',
'Customizable queue toolbar — drag-and-drop button reordering, per-button visibility toggles, optional separator, persisted across restarts (PR #534)',
'Playlist page layout — per-element visibility toggles (Add Songs, Import CSV, Download ZIP, Cache Offline, Suggestions) under Settings → Personalisation (PR #556)',
+ 'Player bar layout — per-control visibility toggles (Star rating, Favorite, Last.fm love, Equalizer, Mini player) under Settings → Personalisation (PR #627)',
],
},
{
diff --git a/src/locales/de/settings.ts b/src/locales/de/settings.ts
index ff9d51a7..d6661531 100644
--- a/src/locales/de/settings.ts
+++ b/src/locales/de/settings.ts
@@ -273,6 +273,13 @@ export const settings = {
playlistLayoutTitle: 'Playlist-Seitenlayout',
playlistLayoutDesc: 'Einzelne Elemente auf der Playlist-Seite ein- oder ausblenden.',
playlistLayoutReset: 'Zurücksetzen',
+ playerBarTitle: 'Player-Leiste',
+ playerBarReset: 'Zurücksetzen',
+ playerBarStarRating: 'Sterne-Bewertung',
+ playerBarFavorite: 'Favorit (Herz)',
+ playerBarLastfmLove: 'Last.fm Love',
+ playerBarEqualizer: 'Equalizer',
+ playerBarMiniPlayer: 'Mini-Player',
advancedMode: 'Erweitert',
advancedModeTooltip: 'Erweiterte Optionen in allen Settings-Tabs anzeigen. Community-Beiträge, die nicht zwingend der Design-Philosophie der Maintainer von Psysonic entsprechen.',
advancedBadge: 'Erweitert',
diff --git a/src/locales/en/settings.ts b/src/locales/en/settings.ts
index 0537da93..22ae18f7 100644
--- a/src/locales/en/settings.ts
+++ b/src/locales/en/settings.ts
@@ -276,6 +276,13 @@ export const settings = {
playlistLayoutTitle: 'Playlist page layout',
playlistLayoutDesc: 'Show or hide individual elements on the playlist page.',
playlistLayoutReset: 'Reset to default',
+ playerBarTitle: 'Player bar',
+ playerBarReset: 'Reset to default',
+ playerBarStarRating: 'Star rating',
+ playerBarFavorite: 'Favorite (heart)',
+ playerBarLastfmLove: 'Last.fm love',
+ playerBarEqualizer: 'Equalizer',
+ playerBarMiniPlayer: 'Mini player',
advancedMode: 'Advanced',
advancedModeTooltip: 'Show advanced options across all Settings tabs. Community contributions that do not necessarily reflect the design philosophy of the Psysonic maintainers.',
advancedBadge: 'Advanced',
diff --git a/src/locales/es/settings.ts b/src/locales/es/settings.ts
index 3f90cdb9..f40138f1 100644
--- a/src/locales/es/settings.ts
+++ b/src/locales/es/settings.ts
@@ -273,6 +273,13 @@ export const settings = {
playlistLayoutTitle: 'Diseño de la página de playlists',
playlistLayoutDesc: 'Mostrar u ocultar elementos individuales en la página de playlists.',
playlistLayoutReset: 'Restablecer a predeterminado',
+ playerBarTitle: 'Barra de reproducción',
+ playerBarReset: 'Restablecer a predeterminado',
+ playerBarStarRating: 'Calificación con estrellas',
+ playerBarFavorite: 'Favorito (corazón)',
+ playerBarLastfmLove: 'Last.fm love',
+ playerBarEqualizer: 'Ecualizador',
+ playerBarMiniPlayer: 'Mini reproductor',
advancedMode: 'Avanzado',
advancedModeTooltip: 'Mostrar opciones avanzadas en todas las pestañas de ajustes. Aportes de la comunidad que no reflejan necesariamente la filosofía de diseño de los mantenedores de Psysonic.',
advancedBadge: 'Avanzado',
diff --git a/src/locales/fr/settings.ts b/src/locales/fr/settings.ts
index 1492db6b..8347f9fc 100644
--- a/src/locales/fr/settings.ts
+++ b/src/locales/fr/settings.ts
@@ -271,6 +271,13 @@ export const settings = {
playlistLayoutTitle: 'Mise en page de la playlist',
playlistLayoutDesc: 'Afficher ou masquer des éléments individuels sur la page de la playlist.',
playlistLayoutReset: 'Réinitialiser',
+ playerBarTitle: 'Barre de lecture',
+ playerBarReset: 'Réinitialiser',
+ playerBarStarRating: 'Note par étoiles',
+ playerBarFavorite: 'Favori (cœur)',
+ playerBarLastfmLove: 'Last.fm love',
+ playerBarEqualizer: 'Égaliseur',
+ playerBarMiniPlayer: 'Mini-lecteur',
advancedMode: 'Avancé',
advancedModeTooltip: 'Afficher les options avancées dans tous les onglets des paramètres. Contributions de la communauté qui ne reflètent pas nécessairement la philosophie de conception des mainteneurs de Psysonic.',
advancedBadge: 'Avancé',
diff --git a/src/locales/nb/settings.ts b/src/locales/nb/settings.ts
index 249e1df8..7647fc44 100644
--- a/src/locales/nb/settings.ts
+++ b/src/locales/nb/settings.ts
@@ -270,6 +270,13 @@ export const settings = {
playlistLayoutTitle: 'Sideoppsett for spilleliste',
playlistLayoutDesc: 'Vis eller skjul individuelle elementer på spillelistesiden.',
playlistLayoutReset: 'Tilbakestill til standard',
+ playerBarTitle: 'Spillerlinje',
+ playerBarReset: 'Tilbakestill til standard',
+ playerBarStarRating: 'Stjernevurdering',
+ playerBarFavorite: 'Favoritt (hjerte)',
+ playerBarLastfmLove: 'Last.fm love',
+ playerBarEqualizer: 'Equalizer',
+ playerBarMiniPlayer: 'Miniavspiller',
advancedMode: 'Avansert',
advancedModeTooltip: 'Vis avanserte alternativer i alle innstillinger-faner. Bidrag fra fellesskapet som ikke nødvendigvis gjenspeiler design-filosofien til Psysonic-vedlikeholderne.',
advancedBadge: 'Avansert',
diff --git a/src/locales/nl/settings.ts b/src/locales/nl/settings.ts
index 1f7f358a..67b7b915 100644
--- a/src/locales/nl/settings.ts
+++ b/src/locales/nl/settings.ts
@@ -271,6 +271,13 @@ export const settings = {
playlistLayoutTitle: 'Afspeellijst pagina-indeling',
playlistLayoutDesc: 'Individuele elementen op de afspeellijstpagina tonen of verbergen.',
playlistLayoutReset: 'Standaard herstellen',
+ playerBarTitle: 'Speelbalk',
+ playerBarReset: 'Standaard herstellen',
+ playerBarStarRating: 'Sterbeoordeling',
+ playerBarFavorite: 'Favoriet (hart)',
+ playerBarLastfmLove: 'Last.fm love',
+ playerBarEqualizer: 'Equalizer',
+ playerBarMiniPlayer: 'Mini-speler',
advancedMode: 'Geavanceerd',
advancedModeTooltip: 'Toon geavanceerde opties in alle instellingstabbladen. Bijdragen van de community die niet noodzakelijk de ontwerpfilosofie van de Psysonic-onderhouders weerspiegelen.',
advancedBadge: 'Geavanceerd',
diff --git a/src/locales/ro/settings.ts b/src/locales/ro/settings.ts
index 558a2f96..7e69bdf7 100644
--- a/src/locales/ro/settings.ts
+++ b/src/locales/ro/settings.ts
@@ -276,6 +276,13 @@ export const settings = {
playlistLayoutTitle: 'Aspect pagină playlist',
playlistLayoutDesc: 'Afișează sau ascunde elemente individuale pe pagina playlistului.',
playlistLayoutReset: 'Resetare la implicit',
+ playerBarTitle: 'Bara de redare',
+ playerBarReset: 'Resetare la implicit',
+ playerBarStarRating: 'Evaluare cu stele',
+ playerBarFavorite: 'Favorit (inimă)',
+ playerBarLastfmLove: 'Last.fm love',
+ playerBarEqualizer: 'Egalizator',
+ playerBarMiniPlayer: 'Mini player',
advancedMode: 'Avansat',
advancedModeTooltip: 'Afișează opțiuni avansate în toate filele de setări. Contribuții ale comunității care nu reflectă neapărat filozofia de design a mentenanților Psysonic.',
advancedBadge: 'Avansat',
diff --git a/src/locales/ru/settings.ts b/src/locales/ru/settings.ts
index f5f79457..24af8c3f 100644
--- a/src/locales/ru/settings.ts
+++ b/src/locales/ru/settings.ts
@@ -287,6 +287,13 @@ export const settings = {
playlistLayoutTitle: 'Макет страницы плейлиста',
playlistLayoutDesc: 'Показать или скрыть отдельные элементы на странице плейлиста.',
playlistLayoutReset: 'Сбросить',
+ playerBarTitle: 'Панель плеера',
+ playerBarReset: 'Сбросить',
+ playerBarStarRating: 'Оценка звёздами',
+ playerBarFavorite: 'Избранное (сердечко)',
+ playerBarLastfmLove: 'Last.fm love',
+ playerBarEqualizer: 'Эквалайзер',
+ playerBarMiniPlayer: 'Мини-плеер',
advancedMode: 'Дополнительно',
advancedModeTooltip: 'Показывать расширенные параметры во всех вкладках настроек. Вклады сообщества, которые не обязательно соответствуют дизайн-философии мейнтейнеров Psysonic.',
advancedBadge: 'Дополнительно',
diff --git a/src/locales/zh/settings.ts b/src/locales/zh/settings.ts
index 63c147e6..e91c9ed8 100644
--- a/src/locales/zh/settings.ts
+++ b/src/locales/zh/settings.ts
@@ -271,6 +271,13 @@ export const settings = {
playlistLayoutTitle: '播放列表页面布局',
playlistLayoutDesc: '在播放列表页面上显示或隐藏各个元素。',
playlistLayoutReset: '重置为默认',
+ playerBarTitle: '播放器栏',
+ playerBarReset: '重置为默认',
+ playerBarStarRating: '星级评分',
+ playerBarFavorite: '收藏(心形)',
+ playerBarLastfmLove: 'Last.fm 喜欢',
+ playerBarEqualizer: '均衡器',
+ playerBarMiniPlayer: '迷你播放器',
advancedMode: '高级',
advancedModeTooltip: '在所有设置选项卡中显示高级选项。由社区贡献,未必体现 Psysonic 维护者的设计理念。',
advancedBadge: '高级',
diff --git a/src/store/playerBarLayoutStore.test.ts b/src/store/playerBarLayoutStore.test.ts
new file mode 100644
index 00000000..6c1dc32f
--- /dev/null
+++ b/src/store/playerBarLayoutStore.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it, beforeEach } from 'vitest';
+import {
+ DEFAULT_PLAYER_BAR_LAYOUT_ITEMS,
+ usePlayerBarLayoutStore,
+} from './playerBarLayoutStore';
+
+describe('playerBarLayoutStore', () => {
+ beforeEach(() => {
+ usePlayerBarLayoutStore.getState().reset();
+ });
+
+ it('starts with all five items visible in declared order', () => {
+ const items = usePlayerBarLayoutStore.getState().items;
+ expect(items.map(i => i.id)).toEqual([
+ 'starRating', 'favorite', 'lastfmLove', 'equalizer', 'miniPlayer',
+ ]);
+ expect(items.every(i => i.visible)).toBe(true);
+ });
+
+ it('toggleItem flips the matching id without disturbing the others', () => {
+ usePlayerBarLayoutStore.getState().toggleItem('equalizer');
+ const items = usePlayerBarLayoutStore.getState().items;
+ expect(items.find(i => i.id === 'equalizer')?.visible).toBe(false);
+ expect(items.find(i => i.id === 'starRating')?.visible).toBe(true);
+ expect(items.find(i => i.id === 'miniPlayer')?.visible).toBe(true);
+ });
+
+ it('reset restores defaults after toggles', () => {
+ const { toggleItem, reset } = usePlayerBarLayoutStore.getState();
+ toggleItem('favorite');
+ toggleItem('lastfmLove');
+ reset();
+ expect(usePlayerBarLayoutStore.getState().items).toEqual(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS);
+ });
+});
diff --git a/src/store/playerBarLayoutStore.ts b/src/store/playerBarLayoutStore.ts
new file mode 100644
index 00000000..de7931b6
--- /dev/null
+++ b/src/store/playerBarLayoutStore.ts
@@ -0,0 +1,58 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+export type PlayerBarLayoutItemId =
+ | 'starRating'
+ | 'favorite'
+ | 'lastfmLove'
+ | 'equalizer'
+ | 'miniPlayer';
+
+export interface PlayerBarLayoutItemConfig {
+ id: PlayerBarLayoutItemId;
+ visible: boolean;
+}
+
+export const DEFAULT_PLAYER_BAR_LAYOUT_ITEMS: PlayerBarLayoutItemConfig[] = [
+ { id: 'starRating', visible: true },
+ { id: 'favorite', visible: true },
+ { id: 'lastfmLove', visible: true },
+ { id: 'equalizer', visible: true },
+ { id: 'miniPlayer', visible: true },
+];
+
+interface PlayerBarLayoutStore {
+ items: PlayerBarLayoutItemConfig[];
+ setItems: (items: PlayerBarLayoutItemConfig[]) => void;
+ toggleItem: (id: PlayerBarLayoutItemId) => void;
+ reset: () => void;
+}
+
+export const usePlayerBarLayoutStore = create()(
+ persist(
+ (set) => ({
+ items: DEFAULT_PLAYER_BAR_LAYOUT_ITEMS,
+
+ setItems: (items) => set({ items }),
+
+ toggleItem: (id) => set((s) => ({
+ items: s.items.map(it => it.id === id ? { ...it, visible: !it.visible } : it),
+ })),
+
+ reset: () => set({ items: DEFAULT_PLAYER_BAR_LAYOUT_ITEMS }),
+ }),
+ {
+ name: 'psysonic_player_bar_layout',
+ onRehydrateStorage: () => (state) => {
+ if (!state) return;
+ const knownIds = new Set(DEFAULT_PLAYER_BAR_LAYOUT_ITEMS.map(i => i.id));
+ const safe = (state.items ?? [])
+ .filter((i): i is PlayerBarLayoutItemConfig =>
+ i != null && typeof i.id === 'string' && knownIds.has(i.id as PlayerBarLayoutItemId));
+ const seen = new Set(safe.map(i => i.id));
+ const missing = DEFAULT_PLAYER_BAR_LAYOUT_ITEMS.filter(i => !seen.has(i.id));
+ state.items = missing.length > 0 ? [...safe, ...missing] : safe;
+ },
+ }
+ )
+);