mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
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:
committed by
GitHub
parent
ea6ac49885
commit
2d27428056
@@ -38,10 +38,17 @@ import { PlayerVolume } from './playerBar/PlayerVolume';
|
||||
import { PlayerOverflowMenu } from './playerBar/PlayerOverflowMenu';
|
||||
import { useFloatingPlayerBar } from '../hooks/useFloatingPlayerBar';
|
||||
import { useUtilityOverflowMenu } from '../hooks/useUtilityOverflowMenu';
|
||||
import {
|
||||
usePlayerBarLayoutStore,
|
||||
type PlayerBarLayoutItemId,
|
||||
} from '../store/playerBarLayoutStore';
|
||||
|
||||
export default function PlayerBar() {
|
||||
const { t } = useTranslation();
|
||||
const navigatePlaybackLibrary = usePlaybackLibraryNavigate();
|
||||
const layoutItems = usePlayerBarLayoutStore(s => s.items);
|
||||
const isLayoutVisible = (id: PlayerBarLayoutItemId) =>
|
||||
layoutItems.find(i => i.id === id)?.visible !== false;
|
||||
const [eqOpen, setEqOpen] = useState(false);
|
||||
const [showVolPct, setShowVolPct] = useState(false);
|
||||
const [localShowRemaining, setLocalShowRemaining] = useState(() => useThemeStore.getState().showRemainingTime);
|
||||
@@ -282,23 +289,27 @@ export default function PlayerBar() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`player-btn player-btn-sm player-eq-btn ${eqOpen ? 'active' : ''}`}
|
||||
onClick={() => setEqOpen(v => !v)}
|
||||
aria-label={t('player.equalizer')}
|
||||
data-tooltip={t('player.equalizer')}
|
||||
>
|
||||
<SlidersVertical size={15} />
|
||||
</button>
|
||||
{isLayoutVisible('equalizer') && (
|
||||
<button
|
||||
className={`player-btn player-btn-sm player-eq-btn ${eqOpen ? 'active' : ''}`}
|
||||
onClick={() => setEqOpen(v => !v)}
|
||||
aria-label={t('player.equalizer')}
|
||||
data-tooltip={t('player.equalizer')}
|
||||
>
|
||||
<SlidersVertical size={15} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="player-btn player-btn-sm"
|
||||
onClick={() => invoke('open_mini_player').catch(() => {})}
|
||||
aria-label={t('player.miniPlayer')}
|
||||
data-tooltip={t('player.miniPlayer')}
|
||||
>
|
||||
<PictureInPicture2 size={15} />
|
||||
</button>
|
||||
{isLayoutVisible('miniPlayer') && (
|
||||
<button
|
||||
className="player-btn player-btn-sm"
|
||||
onClick={() => invoke('open_mini_player').catch(() => {})}
|
||||
aria-label={t('player.miniPlayer')}
|
||||
data-tooltip={t('player.miniPlayer')}
|
||||
>
|
||||
<PictureInPicture2 size={15} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<PlayerVolume
|
||||
volume={volume}
|
||||
|
||||
@@ -4,6 +4,10 @@ import { PictureInPicture2, SlidersVertical } from 'lucide-react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { PlayerVolume } from './PlayerVolume';
|
||||
import {
|
||||
usePlayerBarLayoutStore,
|
||||
type PlayerBarLayoutItemId,
|
||||
} from '../../store/playerBarLayoutStore';
|
||||
|
||||
interface Props {
|
||||
utilityMenuRef: React.RefObject<HTMLDivElement | null>;
|
||||
@@ -29,6 +33,11 @@ export function PlayerOverflowMenu({
|
||||
volume, setVolume, premuteVolumeRef, showVolPct, setShowVolPct,
|
||||
handleVolume, handleVolumeWheel, volumeStyle, t,
|
||||
}: Props) {
|
||||
const layoutItems = usePlayerBarLayoutStore(s => s.items);
|
||||
const isLayoutVisible = (id: PlayerBarLayoutItemId) =>
|
||||
layoutItems.find(i => i.id === id)?.visible !== false;
|
||||
const showEqualizer = isLayoutVisible('equalizer');
|
||||
const showMiniPlayer = isLayoutVisible('miniPlayer');
|
||||
return createPortal(
|
||||
<div
|
||||
className={`player-overflow-menu${utilityMenuMode === 'volume' ? ' player-overflow-menu--volume-only' : ''}`}
|
||||
@@ -36,28 +45,32 @@ export function PlayerOverflowMenu({
|
||||
style={utilityMenuStyle}
|
||||
onWheel={handleVolumeWheel}
|
||||
>
|
||||
{utilityMenuMode === 'full' && (
|
||||
{utilityMenuMode === 'full' && (showEqualizer || showMiniPlayer) && (
|
||||
<div className="player-overflow-menu-row">
|
||||
<button
|
||||
className={`player-overflow-menu-btn${eqOpen ? ' active' : ''}`}
|
||||
onClick={() => {
|
||||
setEqOpen(v => !v);
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<SlidersVertical size={14} />
|
||||
{t('player.equalizer')}
|
||||
</button>
|
||||
<button
|
||||
className="player-overflow-menu-btn"
|
||||
onClick={() => {
|
||||
invoke('open_mini_player').catch(() => {});
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<PictureInPicture2 size={14} />
|
||||
{t('player.miniPlayer')}
|
||||
</button>
|
||||
{showEqualizer && (
|
||||
<button
|
||||
className={`player-overflow-menu-btn${eqOpen ? ' active' : ''}`}
|
||||
onClick={() => {
|
||||
setEqOpen(v => !v);
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<SlidersVertical size={14} />
|
||||
{t('player.equalizer')}
|
||||
</button>
|
||||
)}
|
||||
{showMiniPlayer && (
|
||||
<button
|
||||
className="player-overflow-menu-btn"
|
||||
onClick={() => {
|
||||
invoke('open_mini_player').catch(() => {});
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<PictureInPicture2 size={14} />
|
||||
{t('player.miniPlayer')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<PlayerVolume
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user