feat(settings): player bar layout — per-control visibility toggles (#627) (#721)

Adds a new sub-section under Settings → Personalisation (Advanced) that
hides individual controls in the player bar: Star rating, Favorite
(heart), Last.fm love, Equalizer, Mini player. Last.fm love still only
renders when a Last.fm session exists; the overflow row in the player
collapses when both Equalizer and Mini player are hidden.

- New `playerBarLayoutStore` (Zustand + persist, items[{id, visible}] +
  rehydrate sanitize) following the queueToolbar / playlistLayout
  pattern; defaults to all visible.
- New `PlayerBarLayoutCustomizer` reuses the same row + toggle pattern
  as the other personalisation customisers.
- Gates threaded through `PlayerTrackInfo` (3 controls), `PlayerBar`
  (EQ + Mini buttons), and `PlayerOverflowMenu` (EQ + Mini in the
  overflow row, with row-level conditional).
- `PersonalisationTab`: added as the last advanced sub-section so it
  only appears when the global Advanced Mode toggle is on.
- Settings search index gets entries for both Playlist page layout and
  Player bar (playlist row was missing).
- New i18n keys `settings.playerBar*` in all 9 locales.

Reuses kveld9's design from PR #627; not merged because the locale
split and the Advanced Mode refactor landed afterwards. Credited via
Co-Authored-By trailer + a new line in settingsCredits.ts under the
existing kveld9 entry.

Co-authored-by: Kveld. <kveld912@proton.me>
This commit is contained in:
Frank Stellmacher
2026-05-15 17:48:32 +02:00
committed by GitHub
parent ea6ac49885
commit 2d27428056
19 changed files with 314 additions and 41 deletions
+23 -1
View File
@@ -1,13 +1,15 @@
import { useTranslation } from 'react-i18next';
import { LayoutGrid, ListMusic, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
import { Disc3, LayoutGrid, ListMusic, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
import { useArtistLayoutStore } from '../../store/artistLayoutStore';
import { useHomeStore } from '../../store/homeStore';
import { usePlayerBarLayoutStore } from '../../store/playerBarLayoutStore';
import { usePlaylistLayoutStore } from '../../store/playlistLayoutStore';
import { useQueueToolbarStore } from '../../store/queueToolbarStore';
import { useSidebarStore } from '../../store/sidebarStore';
import SettingsSubSection from '../SettingsSubSection';
import { ArtistLayoutCustomizer } from './ArtistLayoutCustomizer';
import { HomeCustomizer } from './HomeCustomizer';
import { PlayerBarLayoutCustomizer } from './PlayerBarLayoutCustomizer';
import { PlaylistLayoutCustomizer } from './PlaylistLayoutCustomizer';
import { QueueToolbarCustomizer } from './QueueToolbarCustomizer';
import { SidebarCustomizer } from './SidebarCustomizer';
@@ -113,6 +115,26 @@ export function PersonalisationTab() {
>
<PlaylistLayoutCustomizer />
</SettingsSubSection>
<SettingsSubSection
title={t('settings.playerBarTitle')}
icon={<Disc3 size={16} />}
advanced
action={
<button
type="button"
className="btn btn-ghost"
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
onClick={() => usePlayerBarLayoutStore.getState().reset()}
data-tooltip={t('settings.playerBarReset')}
aria-label={t('settings.playerBarReset')}
>
<RotateCcw size={14} />
</button>
}
>
<PlayerBarLayoutCustomizer />
</SettingsSubSection>
</>
);
}
@@ -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<PlayerBarLayoutItemId, string> = {
starRating: 'settings.playerBarStarRating',
favorite: 'settings.playerBarFavorite',
lastfmLove: 'settings.playerBarLastfmLove',
equalizer: 'settings.playerBarEqualizer',
miniPlayer: 'settings.playerBarMiniPlayer',
};
const PLAYER_BAR_LAYOUT_ICONS: Record<PlayerBarLayoutItemId, React.ReactNode> = {
starRating: <Star size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
favorite: <Heart size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
lastfmLove: (
<span style={{ color: 'var(--text-muted)', display: 'inline-flex', flexShrink: 0 }} aria-hidden>
<LastfmIcon size={16} />
</span>
),
equalizer: <SlidersVertical size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
miniPlayer: <PictureInPicture2 size={16} style={{ color: 'var(--text-muted)', flexShrink: 0 }} />,
};
export function PlayerBarLayoutCustomizer() {
const { t } = useTranslation();
const items = usePlayerBarLayoutStore(s => s.items);
const toggleItem = usePlayerBarLayoutStore(s => s.toggleItem);
return (
<div className="settings-card" style={{ padding: '4px 0' }}>
{items.map((it) => {
const label = t(PLAYER_BAR_LAYOUT_LABEL_KEYS[it.id]);
return (
<div key={it.id} className="sidebar-customizer-row">
{PLAYER_BAR_LAYOUT_ICONS[it.id]}
<span style={{ flex: 1, fontSize: 14, opacity: it.visible ? 1 : 0.45 }}>{label}</span>
<label className="toggle-switch" aria-label={label}>
<input type="checkbox" checked={it.visible} onChange={() => toggleItem(it.id)} />
<span className="toggle-track" />
</label>
</div>
);
})}
</div>
);
}
+2
View File
@@ -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' },