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>
111 of 122 top-level src/utils/ files move into 16 topic folders (audio,
cache, cover, share, server, playback, playlist, deviceSync, waveform,
mix, format, export, changelog, ui, perf, componentHelpers). True
singletons with no cluster stay at the utils/ root.
Pure file-move: a path-aware codemod rewrote 539 relative-import
specifiers across 275 files; no logic touched. The hot-path coverage
gate list (.github/frontend-hot-path-files.txt) is updated to the new
paths for the 11 gated utils files — a mechanical consequence of the
move, not a CI change. tsc is green.
* refactor(user-mgmt): extract formatLastSeen helper
Move the relative-time formatter (with the Navidrome
'0001-01-01T00:00:00Z' epoch guard) into utils/userMgmtHelpers.ts.
UserManagementSection.tsx: 515 → 499 LOC.
* refactor(user-mgmt): extract useUserMgmtData hook
Pull users + libraries state, sequential admin-API fetch, and the
nginx-friendly error normalisation into hooks/useUserMgmtData.ts.
UserManagementSection.tsx: 499 → 464 LOC.
* refactor(user-mgmt): extract useUserMgmtActions hook
Bundle handleSave (covers create + edit + library assignment),
handleSaveAndGetMagic (new non-admin user → encoded magic string on
clipboard), and performDelete into hooks/useUserMgmtActions.ts. The
delete confirmation modal now closes inline in the parent before
delegating to performDelete so the hook stays agnostic of UI state.
UserManagementSection.tsx: 464 → 343 LOC.
* refactor(user-mgmt): extract UserMgmtRow subcomponent
Move the per-user list row (user/admin badges, lib-names blob, magic-
string + delete actions, keyboard activation) into
components/settings/userMgmt/UserMgmtRow.tsx.
UserManagementSection.tsx: 343 → 272 LOC.
* refactor(user-mgmt): extract MagicStringModal subcomponent
Move the per-user magic-string portal modal (password re-set + clipboard
copy of the encoded server-magic-string) into
components/settings/userMgmt/MagicStringModal.tsx. Internal password and
submitting state move into the modal; the parent only owns which user is
targeted.
UserManagementSection.tsx: 272 → 153 LOC.
* feat(i18n): Add Romanian translation
* feat(i18n): Update Romanian translation to lang file changes
* feat(i18n): Add new Romanian translation entries
* fix(i18n): add settings.languageRo to remaining locale bundles
Romanian was missing from the language picker labels when UI was not en/ro;
add endonym-style names per locale (de/fr/nl/nb/ru/es/zh) for consistency.
* fix(i18n): use Romanian autonym for settings.languageRo everywhere
Match existing language picker convention (e.g. languageDe is Deutsch in
every locale bundle). Replaces UI-language translations of Romanian.
Three-tab cluster cut. Settings.tsx 1393 → 345 LOC (−1048); the page
now keeps only the tab header, search UI, route-state handling,
ndAdminAuth probe, and tab dispatch — every section body lives in its
own file.
StorageTab — owns offline dir + cache size readouts + cache-clear flow
+ waveform-cache clear + buffering toggles (preload mode / hot cache
incl. dir picker, sliders, clear) + ZIP downloads dir. The hot-cache
state (imageCacheBytes / offlineCacheBytes / hotCacheBytes /
showClearConfirm / clearing), the hotCacheTrackCount memo, all three
hot-cache useEffects, handleClearCache / handleClearWaveformCache, and
pickOfflineDir / pickHotCacheDir / pickDownloadFolder move with it.
Side effect: the two live hotCacheBytes-refresh useEffects were gated
on `activeTab === 'audio'` (a stale leftover from when hot cache lived
on the audio tab); they now run while StorageTab is mounted, which is
the only place hotCacheBytes is actually displayed.
ServersTab — owns the server list, DnD reorder (psy-drop listener +
drop-target hover state + serverContainerEl + handleServerDragMove),
connStatus map, AddServerForm flow (showAddForm + pastedServerInvite +
addServerInviteAnchorRef + the scroll-into-view useLayoutEffect),
testConnection / switchToServer / deleteServer / handleAddServer /
closeAddServerForm / handleLogout. Settings still owns the route-state
useEffect that catches `openAddServerInvite` and flips to the servers
tab; it passes the invite as `initialInvite` and ServersTab consumes
it on mount + on later prop changes.
SystemTab — owns Language picker, behavior toggles (tray, minimize to
tray, Linux kinetic scroll), Backup section, logging mode + export
runtime logs, About card (maintainers, release notes link,
show-changelog-on-update), Contributors grid, Licenses panel. The
exportRuntimeLogs handler moves with it.
UsersTab stays inline in Settings.tsx — it's an 8-line wrapper around
UserManagementSection gated on ndAdminAuth, which Settings already
owns for the tab-bar visibility check.
The Settings.tsx import list drops 30+ names that only the extracted
tabs used: many lucide icons, openDialog/saveDialog, openUrl, Trans,
showToast, invoke, getImageCacheSize/clearImageCache, usePlayerStore,
useOfflineStore, useHotCacheStore, useDragDrop, pingWithCredentials,
scheduleInstantMixProbeForServer, switchActiveServer, formatBytes,
snapHotCacheMb, MAINTAINERS, CONTRIBUTORS, LicensesPanel,
AboutPsysonicBrandHeader, BackupSection, AddServerForm, ServerGripHandle,
serverListDisplayLabel, showAudiomuseNavidromeServerSetting,
shortHostFromServerUrl, ServerProfile, LoggingMode, LoudnessLufsPreset,
appVersion, i18n, IS_LINUX/IS_WINDOWS, CustomSelect, SettingsSubSection,
plus useMemo/useCallback/useLayoutEffect.
Pure code move otherwise — no behaviour change.
Move the Appearance tab body into AppearanceTab: ThemePicker, theme
scheduler (day/night themes + start times, 24h/12h locale-aware),
visual options card (cover art bg, playlist cover photo, bitrate badge,
floating player bar, artist images, Orbit trigger, preloadMiniPlayer,
custom titlebar on Linux non-tiling), UI scale presets, font picker,
fullscreen player portrait + dim slider, seekbar style picker.
The `isTilingWm` state + `is_tiling_wm_cmd` invoke effect, plus the
`useThemeStore` and `useFontStore` hooks, move into AppearanceTab —
no other tab needs them.
Settings.tsx 1761 → 1404 LOC (−357). Drops 9 imports that only the
appearance tab used (ThemePicker, THEME_GROUPS, SeekbarPreview,
useThemeStore, useFontStore, FontId, SeekbarStyle, lucide Clock /
Maximize2 / Type / ZoomIn).
Pure code move — no behaviour change.
Move the Audio tab body into AudioTab: output-device picker (with the
canonicalize + list-refresh dance and the audio:device-changed / -reset
listener), Hi-Res toggle, embedded Equalizer, the normalization block
(off / replaygain / loudness with pre-analysis attenuation slider and
LoudnessLufsButtonGroup), Crossfade + Gapless mutual-exclusion toggles,
preserve-play-next-order, and Track Previews (locations + start ratio +
duration). The audio-devices state (audioDevices, osDefaultAudioDeviceId,
deviceSwitching, devicesLoading) and refreshAudioDevices useCallback move
into AudioTab; preAnalysisEffectiveDb useMemo moves with them.
Settings.tsx 2266 → 1761 LOC (−505). Drops 8 imports that only the audio
tab used (lucide Play/Waves; listen; effectiveLoudnessPreAnalysisAttenuationDb;
LoudnessLufsButtonGroup; Equalizer; audio-device label helpers; the
TRACK_PREVIEW_LOCATIONS / DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB
constants; TrackPreviewLocation type).
Pure code move — no behaviour change.
Move the Integrations tab (Last.fm connect/disconnect, scrobbling toggles,
ListenBrainz, Discord RPC) into IntegrationsTab. The Last.fm token+session
poll flow is now owned by IntegrationsTab as a useCallback closing over
useAuthStore directly. Move the Library tab (Random Mix blacklist, Lucky
Mix menu toggle, hard-coded audiobook genre badges, ratings sliders and
mix min-rating thresholds) into LibraryTab; AUDIOBOOK_GENRES_DISPLAY and
the per-row star sliders move with it.
Settings.tsx 2741 → 2266 LOC (−475). Unused imports removed: lastfm api
helpers, LastfmIcon, Shuffle, Star, StarRating, MIX_MIN_RATING_FILTER_MAX_STARS.
Pure code move — no behaviour change.
Three tab-section components carved out of the Settings() default-
export body. Each owns its store hooks + local state; Settings()
now only routes via `{activeTab === 'X' && <XTab />}`.
- `LyricsTab.tsx` (~50 LOC, was ~40 inline) — wraps
`LyricsSourcesCustomizer` + sidebar lyrics style toggles. Owns
the two `useAuthStore(s => s.sidebarLyricsStyle/...)` selectors.
- `PersonalisationTab.tsx` (~95 LOC, was ~80 inline) — wraps the
four customizers (sidebar / artist layout / home / queue toolbar)
with their per-store reset buttons.
- `InputTab.tsx` (~185 LOC, was ~170 inline) — keybindings + global
shortcuts. Owns the two `listeningFor` / `listeningForGlobal`
state slots that were previously hoisted into Settings().
13 now-unused imports trimmed (4 customizer-store hooks, 6
keybinding helpers, `Music2/AudioLines` kept since the tab-button
array still uses them as icons, `LayoutGrid/Keyboard` kept for the
same reason).
Pure code-move. Settings.tsx: 3037 → 2741 LOC (−296). Phase-G
journey: 5298 → 2741 LOC (~48% reduction).
Pull pure helpers, the contributors/maintainers data list and the
tab type/search index out of `Settings.tsx` so the main component
only has tab-section render logic + the orchestrating state left:
- `utils/audioDeviceLabels.ts` — five ALSA-device label helpers
(formatAudioDeviceLabel + duplicate-disambiguation + sort +
select-option builder).
- `utils/formatBytes.ts` — formatBytes + snapHotCacheMb.
- `components/settings/LoudnessLufsButtonGroup.tsx` — small chip
group used by the audio tab.
- `components/settings/settingsTabs.ts` — `Tab` type + legacy alias
map + `resolveTab` + `SearchIndexEntry` + `SETTINGS_INDEX` + the
`matchScore` substring-with-fuzzy-fallback scorer.
- `config/settingsCredits.ts` — `CONTRIBUTORS` (~270 LOC of static
contributor history) + `MAINTAINERS`.
Pure code-move. Settings.tsx: 3552 → 3037 LOC (−515). Settings/G
journey now 5298 → 3037 LOC (~43% reduction).
Three self-contained server/user-management components peel ~1000 LOC
out of `Settings.tsx`:
- `AddServerForm.tsx` (~163 LOC) — server URL + credentials + magic-
string paste form. Used by the Servers tab.
- `UserForm.tsx` (~340 LOC, with `initialUserFormState` + `UserFormState`
type) — full Navidrome user create/edit form including the
"save + copy magic string" admin flow.
- `UserManagementSection.tsx` (~485 LOC, with `formatLastSeen` helper) —
list + CRUD + Trash/Edit row + per-row magic-string-with-password
modal. Used by the Users tab. Imports `UserForm` directly.
Each component owns its own state, helpers, and modal-portal logic.
Settings.tsx now imports them and threads in props (server URL, admin
token, current username).
Pure code-move. Settings.tsx: 4568 → 3552 LOC (−1016). 18 unused
imports trimmed (navidromeAdmin types/functions, serverMagicString
helpers, ConfirmModal, lucide icons, createPortal).
Pull the 11 self-contained components at the bottom of Settings.tsx
out into `src/components/settings/`:
- `HomeCustomizer.tsx` — home-page section visibility toggles.
- `QueueToolbarCustomizer.tsx` — drag-to-reorder queue toolbar
buttons + per-button visibility. Includes the GripHandle + button
icons/labels tables.
- `SidebarCustomizer.tsx` — sidebar nav drag-reorder for library +
system blocks. Includes the GripHandle and the random-nav-mode
toggle.
- `LyricsSourcesCustomizer.tsx` — lyrics fetch pipeline UI (mode
switch + drag-reorder source list + static-only toggle).
- `ArtistLayoutCustomizer.tsx` — artist page section drag-reorder.
- `BackupSection.tsx` — export/import buttons with toast feedback.
- `ServerGripHandle.tsx` — single-purpose grip handle used by the
servers tab in the main Settings body.
Each component owns its own DnD plumbing, label tables and drop
target types. Settings.tsx now only imports the components; one
local `ServerDropTarget` type kept inside `Settings()` because the
main component still owns the server-list DnD state.
Pure code-move. Settings.tsx: 5298 → 4568 LOC (−730). 13 unused
imports trimmed (lucide icons, store types, shallow, layout helpers).