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
+11 -3
View File
@@ -10,6 +10,10 @@ import LastfmIcon from '../LastfmIcon';
import MarqueeText from '../MarqueeText';
import { OpenArtistRefInline } from '../OpenArtistRefInline';
import StarRating from '../StarRating';
import {
usePlayerBarLayoutStore,
type PlayerBarLayoutItemId,
} from '../../store/playerBarLayoutStore';
interface Props {
currentTrack: Track | null;
@@ -48,6 +52,10 @@ export function PlayerTrackInfo({
userRatingOverrides, setUserRatingOverride, toggleFullscreen,
navigate, openContextMenu, t,
}: Props) {
const layoutItems = usePlayerBarLayoutStore(s => s.items);
const isLayoutVisible = (id: PlayerBarLayoutItemId) =>
layoutItems.find(i => i.id === id)?.visible !== false;
return (
<div className="player-track-info">
<div
@@ -142,7 +150,7 @@ export function PlayerTrackInfo({
onClick={() => !isRadio && !showPreviewMeta && currentTrack?.artistId && navigate(`/artist/${currentTrack.artistId}`)}
/>
)}
{currentTrack && !isRadio && !showPreviewMeta && (
{currentTrack && !isRadio && !showPreviewMeta && isLayoutVisible('starRating') && (
<StarRating
value={userRatingOverrides[currentTrack.id] ?? currentTrack.userRating ?? 0}
onChange={r => { setUserRatingOverride(currentTrack.id, r); setRating(currentTrack.id, r).catch(() => {}); }}
@@ -156,7 +164,7 @@ export function PlayerTrackInfo({
</span>
)}
</div>
{currentTrack && !isRadio && (
{currentTrack && !isRadio && isLayoutVisible('favorite') && (
<button
className={`player-btn player-btn-sm player-star-btn${isStarred ? ' is-starred' : ''}`}
onClick={toggleStar}
@@ -167,7 +175,7 @@ export function PlayerTrackInfo({
<Heart size={15} fill={isStarred ? 'currentColor' : 'none'} />
</button>
)}
{currentTrack && !isRadio && lastfmSessionKey && (
{currentTrack && !isRadio && lastfmSessionKey && isLayoutVisible('lastfmLove') && (
<button
className="player-btn player-btn-sm player-love-btn"
onClick={toggleLastfmLove}