feat(settings): global Advanced Mode toggle + playlist page layout (#556) (#720)

Adds a per-element visibility toggle for the playlist detail page (Add
Songs, Import CSV, Download ZIP, Cache Offline, Suggestions) and reworks
the way uncommon options are surfaced: instead of a per-tab collapsible
group, a global "Advanced" toggle in the Settings header reveals all
`advanced` sub-sections across every tab and marks each one with a small
badge. Sets the pattern up so any future advanced option lives in its
natural tab, gated by the same switch.

- New `advancedSettingsEnabled` boolean on `authStore`
  (UiAppearance slice, persisted with the rest of the store).
- `SettingsSubSection` gains an `advanced?: boolean` prop. Hidden when
  the toggle is off; renders an "Advanced" pill in the header when on.
- Settings header gets a Toggle-Switch next to the search lupe.
- `PersonalisationTab` flattens — Sidebar + Home stay always visible;
  Artist sections, Queue Toolbar, and the new Playlist layout get
  `advanced` and disappear by default. `PersonalisationAdvancedGroup`
  component + CSS removed.
- New `playlistLayoutStore` (Zustand + persist, items[{id,visible}] +
  rehydrate sanitize) following the queueToolbarStore pattern.
- `PlaylistHero` and `PlaylistSuggestions` gate the four toolbar buttons
  and the suggestions rail on the store directly.
- One-time migration in MainApp on mount: if the user had opened the
  old per-tab Advanced group (`psysonic_personalisation_advanced_open
  === 'true'`) OR already customised any of the three sub-sections,
  Advanced Mode auto-enables on first launch. Idempotent via a
  localStorage flag; legacy key removed afterwards.
- New i18n keys `settings.advancedMode`, `settings.advancedModeTooltip`,
  `settings.advancedBadge`, `settings.playlistLayout*` in all 9 locales.

Reuses kveld9's design from PR #556; not merged because the locale split
landed afterwards. Credited under the existing kveld9 entry in
settingsCredits.ts.

Co-authored-by: Kveld. <kveld912@proton.me>
This commit is contained in:
Frank Stellmacher
2026-05-15 15:55:11 +02:00
committed by GitHub
parent e1283f4068
commit 651a3f276a
24 changed files with 404 additions and 39 deletions
+1
View File
@@ -86,6 +86,7 @@ export const useAuthStore = create<AuthState>()(
fsPortraitDim: 28,
showChangelogOnUpdate: true,
lastSeenChangelogVersion: '',
advancedSettingsEnabled: false,
seekbarStyle: 'truewave',
queueNowPlayingCollapsed: false,
enableHiRes: false,
+3
View File
@@ -130,6 +130,8 @@ export interface AuthState {
fsPortraitDim: number;
showChangelogOnUpdate: boolean;
lastSeenChangelogVersion: string;
/** Reveals sub-sections marked `advanced` across all Settings tabs. */
advancedSettingsEnabled: boolean;
seekbarStyle: SeekbarStyle;
/** Persisted UI toggle: is the Now Playing section in queue panel collapsed */
@@ -284,6 +286,7 @@ export interface AuthState {
setFsPortraitDim: (v: number) => void;
setShowChangelogOnUpdate: (v: boolean) => void;
setLastSeenChangelogVersion: (v: string) => void;
setAdvancedSettingsEnabled: (v: boolean) => void;
setSeekbarStyle: (v: SeekbarStyle) => void;
setQueueNowPlayingCollapsed: (v: boolean) => void;
setEnableHiRes: (v: boolean) => void;
+2
View File
@@ -29,6 +29,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
| 'setFsPortraitDim'
| 'setShowChangelogOnUpdate'
| 'setLastSeenChangelogVersion'
| 'setAdvancedSettingsEnabled'
> {
return {
setShowArtistImages: (v) => set({ showArtistImages: v }),
@@ -48,5 +49,6 @@ export function createUiAppearanceActions(set: SetState): Pick<
setFsPortraitDim: (v) => set({ fsPortraitDim: v }),
setShowChangelogOnUpdate: (v) => set({ showChangelogOnUpdate: v }),
setLastSeenChangelogVersion: (v) => set({ lastSeenChangelogVersion: v }),
setAdvancedSettingsEnabled: (v) => set({ advancedSettingsEnabled: v }),
};
}
+68
View File
@@ -0,0 +1,68 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
export type PlaylistLayoutItemId =
| 'addSongs'
| 'importCsv'
| 'downloadZip'
| 'offlineCache'
| 'suggestions';
export interface PlaylistLayoutItemConfig {
id: PlaylistLayoutItemId;
visible: boolean;
}
export const DEFAULT_PLAYLIST_LAYOUT_ITEMS: PlaylistLayoutItemConfig[] = [
{ id: 'addSongs', visible: true },
{ id: 'importCsv', visible: true },
{ id: 'downloadZip', visible: true },
{ id: 'offlineCache', visible: true },
{ id: 'suggestions', visible: true },
];
interface PlaylistLayoutStore {
items: PlaylistLayoutItemConfig[];
setItems: (items: PlaylistLayoutItemConfig[]) => void;
toggleItem: (id: PlaylistLayoutItemId) => void;
reset: () => void;
}
export const usePlaylistLayoutStore = create<PlaylistLayoutStore>()(
persist(
(set) => ({
items: DEFAULT_PLAYLIST_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_PLAYLIST_LAYOUT_ITEMS }),
}),
{
name: 'psysonic_playlist_layout',
onRehydrateStorage: () => (state) => {
if (!state) return;
const knownIds = new Set(DEFAULT_PLAYLIST_LAYOUT_ITEMS.map(i => i.id));
const safe = (state.items ?? [])
.filter((i): i is PlaylistLayoutItemConfig =>
i != null && typeof i.id === 'string' && knownIds.has(i.id as PlaylistLayoutItemId));
const seen = new Set(safe.map(i => i.id));
const missing = DEFAULT_PLAYLIST_LAYOUT_ITEMS.filter(i => !seen.has(i.id));
state.items = missing.length > 0 ? [...safe, ...missing] : safe;
},
}
)
);
export function isPlaylistLayoutCustomized(items: PlaylistLayoutItemConfig[]): boolean {
if (items.length !== DEFAULT_PLAYLIST_LAYOUT_ITEMS.length) return true;
for (let i = 0; i < items.length; i++) {
const cur = items[i];
const def = DEFAULT_PLAYLIST_LAYOUT_ITEMS[i];
if (cur.id !== def.id || cur.visible !== def.visible) return true;
}
return false;
}