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
+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;
}