refactor(settings): co-locate settings feature into features/settings

Theme infra (fixedThemes, theme utils), music-network presetIcon, audio-device
probe, library-index sync, and the auth*SettingsActions slices stay in the
core/global layer (consumed via @/ alias) to avoid inverting core->feature.
This commit is contained in:
Psychotoxical
2026-06-29 23:48:19 +02:00
parent fbc37db64e
commit e945da693a
69 changed files with 361 additions and 345 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ const LuckyMixPage = lazy(() => import('../pages/LuckyMix'));
const Playlists = lazy(() => import('../pages/Playlists'));
const PlaylistDetail = lazy(() => import('../pages/PlaylistDetail'));
const NowPlayingPage = lazy(() => import('@/features/nowPlaying/pages/NowPlaying'));
const Settings = lazy(() => import('../pages/Settings'));
const Settings = lazy(() => import('@/features/settings/pages/Settings'));
const Statistics = lazy(() => import('@/features/stats/pages/Statistics'));
const Help = lazy(() => import('../pages/Help'));
const WhatsNew = lazy(() => import('../pages/WhatsNew'));
+1 -1
View File
@@ -6,7 +6,7 @@ import {
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useHelpModalStore } from '../store/helpModalStore';
import SettingsSubSection from './SettingsSubSection';
import { SettingsSubSection } from '@/features/settings';
/**
* Orbit help modal. Rendered once at the app root; triggered from the
@@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { createPortal } from 'react-dom';
import type { TFunction } from 'i18next';
import { PlaybackRateControls } from '../settings/audio/PlaybackRateBlock';
import { PlaybackRateControls } from '@/features/settings';
import { usePlaybackRateStore } from '../../store/playbackRateStore';
import { useOrbitStore } from '../../store/orbitStore';
import {
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import { AddServerForm } from './AddServerForm';
import { AddServerForm } from '@/features/settings/components/AddServerForm';
import { encodeServerMagicString } from '@/utils/server/serverMagicString';
// resolve_host_addresses Tauri command — hint-only, must not block save.
@@ -1,22 +1,22 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import type { CustomHeaderEntry, CustomHeadersApplyTo, ServerProfile } from '../../store/authStoreTypes';
import { showToast } from '../../utils/ui/toast';
import type { CustomHeaderEntry, CustomHeadersApplyTo, ServerProfile } from '@/store/authStoreTypes';
import { showToast } from '@/utils/ui/toast';
import {
DEFAULT_CUSTOM_HEADERS_APPLY_TO,
serverCustomHeadersFromForm,
validateCustomHeaders,
} from '../../utils/server/serverHttpHeaders';
import { CustomHttpHeadersEditor } from './CustomHttpHeadersEditor';
} from '@/utils/server/serverHttpHeaders';
import { CustomHttpHeadersEditor } from '@/features/settings/components/CustomHttpHeadersEditor';
import {
decodeServerMagicString,
encodeServerMagicString,
DECODED_PASSWORD_VISUAL_MASK,
type ServerMagicPayload,
} from '../../utils/server/serverMagicString';
import { shortHostFromServerUrl } from '../../utils/server/serverDisplayName';
import { isLanUrl } from '../../utils/server/serverEndpoint';
import { resolveHostAddresses } from '../../api/network';
} from '@/utils/server/serverMagicString';
import { shortHostFromServerUrl } from '@/utils/server/serverDisplayName';
import { isLanUrl } from '@/utils/server/serverEndpoint';
import { resolveHostAddresses } from '@/api/network';
type FormState = {
name: string;
@@ -4,11 +4,11 @@ import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { save as saveDialog } from '@tauri-apps/plugin-dialog';
import { writeFile } from '@tauri-apps/plugin-fs';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsSubCard } from './SettingsSubCard';
import { useAnalysisStrategyStore } from '../../store/analysisStrategyStore';
import { useAuthStore } from '../../store/authStore';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsSubCard } from '@/features/settings/components/SettingsSubCard';
import { useAnalysisStrategyStore } from '@/store/analysisStrategyStore';
import { useAuthStore } from '@/store/authStore';
import {
analysisClearFailedTracks,
analysisDeleteAllForServer,
@@ -18,18 +18,18 @@ import {
type AnalysisFailedTrackDto,
libraryAnalysisProgress,
type LibraryAnalysisProgressDto,
} from '../../api/analysis';
import { libraryGetTracksBatch, type LibraryTrackDto, type TrackRefDto } from '../../api/library';
import { buildStreamUrlForServer } from '../../api/subsonicStreamUrl';
import { serverListDisplayLabel } from '../../utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '../../utils/server/serverIndexKey';
import { showToast } from '../../utils/ui/toast';
} from '@/api/analysis';
import { libraryGetTracksBatch, type LibraryTrackDto, type TrackRefDto } from '@/api/library';
import { buildStreamUrlForServer } from '@/api/subsonicStreamUrl';
import { serverListDisplayLabel } from '@/utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { showToast } from '@/utils/ui/toast';
import {
ANALYTICS_STRATEGIES,
ADVANCED_PARALLELISM_MAX,
ADVANCED_PARALLELISM_MIN,
type AnalyticsStrategy,
} from '../../utils/library/analysisStrategy';
} from '@/utils/library/analysisStrategy';
type ClearTarget = {
serverId: string;
@@ -2,22 +2,22 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { invoke } from '@tauri-apps/api/core';
import { LayoutGrid, Maximize2, Palette, Sliders, Type, ZoomIn } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import { useAuthStore } from '@/store/authStore';
import {
LIBRARY_GRID_MAX_COLUMNS_MAX,
LIBRARY_GRID_MAX_COLUMNS_MIN,
} from '../../store/authStoreDefaults';
import type { SeekbarStyle, WindowButtonStyle } from '../../store/authStoreTypes';
import { useFontStore, FontId } from '../../store/fontStore';
import { useThemeStore } from '../../store/themeStore';
import { IS_LINUX, IS_WINDOWS } from '../../utils/platform';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsValue } from './SettingsSubCard';
import { SettingsSegmented, type SegmentedOption } from './SettingsSegmented';
} from '@/store/authStoreDefaults';
import type { SeekbarStyle, WindowButtonStyle } from '@/store/authStoreTypes';
import { useFontStore, FontId } from '@/store/fontStore';
import { useThemeStore } from '@/store/themeStore';
import { IS_LINUX, IS_WINDOWS } from '@/utils/platform';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsValue } from '@/features/settings/components/SettingsSubCard';
import { SettingsSegmented, type SegmentedOption } from '@/features/settings/components/SettingsSegmented';
import { SeekbarPreview } from '@/features/waveform';
import WindowButtonPreview from '../WindowButtonPreview';
import WindowButtonPreview from '@/components/WindowButtonPreview';
export function AppearanceTab() {
const { t } = useTranslation();
@@ -1,9 +1,9 @@
import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useArtistLayoutStore, type ArtistSectionConfig, type ArtistSectionId } from '../../store/artistLayoutStore';
import { useListReorderDnd } from '../../hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '../../utils/componentHelpers/listReorder';
import { ReorderGripHandle } from './ReorderGripHandle';
import { useArtistLayoutStore, type ArtistSectionConfig, type ArtistSectionId } from '@/store/artistLayoutStore';
import { useListReorderDnd } from '@/hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '@/utils/componentHelpers/listReorder';
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
const ARTIST_SECTION_LABEL_KEYS: Record<ArtistSectionId, string> = {
bio: 'settings.artistLayoutBio',
@@ -1,20 +1,20 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Blend, Gauge, Sliders, Volume2, Waves } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import Equalizer from '../Equalizer';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '../../utils/audio/loudnessPreAnalysisSlider';
import { useAudioDevicesProbe } from '../../hooks/useAudioDevicesProbe';
import { IS_MACOS } from '../../utils/platform';
import { AudioOutputDeviceSection } from './audio/AudioOutputDeviceSection';
import { NormalizationBlock } from './audio/NormalizationBlock';
import { PlaybackRateBlock } from './audio/PlaybackRateBlock';
import { TrackTransitionsBlock } from './audio/TrackTransitionsBlock';
import { TrackPreviewsSection } from './audio/TrackPreviewsSection';
import { HiResCrossfadeResampleBlock } from './audio/HiResCrossfadeResampleBlock';
import { useAuthStore } from '@/store/authStore';
import Equalizer from '@/components/Equalizer';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/utils/audio/loudnessPreAnalysisSlider';
import { useAudioDevicesProbe } from '@/hooks/useAudioDevicesProbe';
import { IS_MACOS } from '@/utils/platform';
import { AudioOutputDeviceSection } from '@/features/settings/components/audio/AudioOutputDeviceSection';
import { NormalizationBlock } from '@/features/settings/components/audio/NormalizationBlock';
import { PlaybackRateBlock } from '@/features/settings/components/audio/PlaybackRateBlock';
import { TrackTransitionsBlock } from '@/features/settings/components/audio/TrackTransitionsBlock';
import { TrackPreviewsSection } from '@/features/settings/components/audio/TrackPreviewsSection';
import { HiResCrossfadeResampleBlock } from '@/features/settings/components/audio/HiResCrossfadeResampleBlock';
export function AudioTab() {
const { t } = useTranslation();
@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from 'react';
import { ChevronDown, ChevronUp, GripVertical } from 'lucide-react';
import { useDragDrop, useDragSource } from '../../contexts/DragDropContext';
import type { BackdropSource, BackdropSourcePref } from '../../cover/artistBackdrop';
import type { BackdropSurface } from '../../store/themeStore';
import { moveSourceTo, dropSourceBefore } from './backdropReorder';
import { useDragDrop, useDragSource } from '@/contexts/DragDropContext';
import type { BackdropSource, BackdropSourcePref } from '@/cover/artistBackdrop';
import type { BackdropSurface } from '@/store/themeStore';
import { moveSourceTo, dropSourceBefore } from '@/features/settings/components/backdropReorder';
const DRAG_TYPE = 'backdrop-source';
@@ -2,15 +2,15 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Clock3, Download, Upload } from 'lucide-react';
import { createPortal } from 'react-dom';
import { SettingsGroup } from './SettingsGroup';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
import {
exportBackupToPath,
importAnyBackupFromPath,
pickBackupExportPath,
pickBackupImportPath,
} from '../../utils/export/backup';
import { showToast } from '../../utils/ui/toast';
} from '@/features/settings/utils/backup';
import { showToast } from '@/utils/ui/toast';
type BackupMode = 'full' | 'library' | 'config';
type BackupAction = 'export' | 'import';
@@ -2,28 +2,28 @@ import { Image, Trash2 } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react';
import { useTranslation } from 'react-i18next';
import { listen } from '@tauri-apps/api/event';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsSubCard } from './SettingsSubCard';
import { useCoverStrategyStore } from '../../store/coverStrategyStore';
import { useAuthStore } from '../../store/authStore';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsSubCard } from '@/features/settings/components/SettingsSubCard';
import { useCoverStrategyStore } from '@/store/coverStrategyStore';
import { useAuthStore } from '@/store/authStore';
import {
coverCacheClearServer,
coverCacheStatsServer,
libraryCoverCatalogSize,
libraryCoverProgress,
} from '../../api/coverCache';
import { clearDiskSrcCacheForServer } from '../../cover/diskSrcCache';
import { serverListDisplayLabel } from '../../utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '../../utils/server/serverIndexKey';
import { showToast } from '../../utils/ui/toast';
import { formatBytes } from '../../utils/format/formatBytes';
import { clearImageCache, getImageCacheSize } from '../../utils/imageCache';
import { wakeLibraryCoverBackfill } from '../../utils/library/coverBackfillWake';
} from '@/api/coverCache';
import { clearDiskSrcCacheForServer } from '@/cover/diskSrcCache';
import { serverListDisplayLabel } from '@/utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { showToast } from '@/utils/ui/toast';
import { formatBytes } from '@/utils/format/formatBytes';
import { clearImageCache, getImageCacheSize } from '@/utils/imageCache';
import { wakeLibraryCoverBackfill } from '@/utils/library/coverBackfillWake';
import {
COVER_CACHE_STRATEGIES,
type CoverCacheStrategy,
} from '../../utils/library/coverStrategy';
} from '@/utils/library/coverStrategy';
type ClearTarget =
| { kind: 'image' }
@@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import type { CustomHeaderEntry, CustomHeadersApplyTo } from '../../store/authStoreTypes';
import type { CustomHeaderEntry, CustomHeadersApplyTo } from '@/store/authStoreTypes';
export type CustomHttpHeadersEditorProps = {
headers: CustomHeaderEntry[];
@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next';
import { useHomeStore, HomeSectionId } from '../../store/homeStore';
import { useHomeStore, HomeSectionId } from '@/store/homeStore';
export function HomeCustomizer() {
const { t } = useTranslation();
@@ -1,12 +1,12 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Keyboard, RotateCcw, X } from 'lucide-react';
import { IN_APP_SHORTCUT_ACTIONS, GLOBAL_SHORTCUT_ACTIONS } from '../../config/shortcutActions';
import { useGlobalShortcutsStore, type GlobalAction, buildGlobalShortcut, formatGlobalShortcut } from '../../store/globalShortcutsStore';
import { useKeybindingsStore, type KeyAction, buildInAppBinding, formatBinding } from '../../store/keybindingsStore';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsSubCard } from './SettingsSubCard';
import { IN_APP_SHORTCUT_ACTIONS, GLOBAL_SHORTCUT_ACTIONS } from '@/config/shortcutActions';
import { useGlobalShortcutsStore, type GlobalAction, buildGlobalShortcut, formatGlobalShortcut } from '@/store/globalShortcutsStore';
import { useKeybindingsStore, type KeyAction, buildInAppBinding, formatBinding } from '@/store/keybindingsStore';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsSubCard } from '@/features/settings/components/SettingsSubCard';
export function InputTab() {
const { t } = useTranslation();
@@ -1,15 +1,15 @@
import { useMemo, useState } from 'react';
import { Check, RefreshCw, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useThemeStore } from '../../store/themeStore';
import { useInstalledThemesStore } from '../../store/installedThemesStore';
import { uninstallTheme } from '../../utils/themes/uninstallTheme';
import { installThemeFromRegistry } from '../../utils/themes/installThemeFromRegistry';
import { useThemeUpdates } from '../../hooks/useThemeUpdates';
import { useThemeAnimationRisk } from '../../hooks/useThemeAnimationRisk';
import { showToast } from '../../utils/ui/toast';
import { AnimatedThemeBadge } from './AnimatedThemeBadge';
import { FIXED_THEMES } from './fixedThemes';
import { useThemeStore } from '@/store/themeStore';
import { useInstalledThemesStore } from '@/store/installedThemesStore';
import { uninstallTheme } from '@/utils/themes/uninstallTheme';
import { installThemeFromRegistry } from '@/utils/themes/installThemeFromRegistry';
import { useThemeUpdates } from '@/hooks/useThemeUpdates';
import { useThemeAnimationRisk } from '@/hooks/useThemeAnimationRisk';
import { showToast } from '@/utils/ui/toast';
import { AnimatedThemeBadge } from '@/features/settings/components/AnimatedThemeBadge';
import { FIXED_THEMES } from '@/components/settings/fixedThemes';
/** Pull a 3-band swatch (bg / card / accent) out of an installed theme's CSS. */
function swatch(css: string): { bg: string; card: string; accent: string } {
@@ -1,16 +1,16 @@
import { useTranslation } from 'react-i18next';
import { AlertTriangle, Check, Image as ImageIcon, Info, Sparkles, Wifi } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import { useThemeStore } from '../../store/themeStore';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import { BackdropSourceList } from './BackdropSourceList';
import type { BackdropSurface } from '../../store/themeStore';
import type { BackdropSource } from '../../cover/artistBackdrop';
import { MusicNetworkSection } from './musicNetwork/MusicNetworkSection';
import { purgeExternalArtworkAllServers } from '../../api/coverCache';
import { useAuthStore } from '@/store/authStore';
import { useThemeStore } from '@/store/themeStore';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
import { BackdropSourceList } from '@/features/settings/components/BackdropSourceList';
import type { BackdropSurface } from '@/store/themeStore';
import type { BackdropSource } from '@/cover/artistBackdrop';
import { MusicNetworkSection } from '@/features/settings/components/musicNetwork/MusicNetworkSection';
import { purgeExternalArtworkAllServers } from '@/api/coverCache';
export function IntegrationsTab() {
const { t } = useTranslation();
@@ -1,14 +1,14 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Shuffle, Star } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import { MIX_MIN_RATING_FILTER_MAX_STARS } from '../../store/authStoreDefaults';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import StarRating from '../StarRating';
import AnalyticsStrategySection from './AnalyticsStrategySection';
import { useAuthStore } from '@/store/authStore';
import { MIX_MIN_RATING_FILTER_MAX_STARS } from '@/store/authStoreDefaults';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
import StarRating from '@/components/StarRating';
import AnalyticsStrategySection from '@/features/settings/components/AnalyticsStrategySection';
const AUDIOBOOK_GENRES_DISPLAY = ['Hörbuch', 'Hoerbuch', 'Hörspiel', 'Hoerspiel', 'Audiobook', 'Audio Book', 'Spoken Word', 'Spokenword', 'Podcast', 'Kapitel', 'Thriller', 'Krimi', 'Speech', 'Fantasy', 'Comedy', 'Literature'];
export function LibraryTab() {
@@ -1,4 +1,4 @@
import type { LoudnessLufsPreset } from '../../store/authStoreTypes';
import type { LoudnessLufsPreset } from '@/store/authStoreTypes';
const LOUDNESS_LUFS_BUTTON_ORDER: LoudnessLufsPreset[] = [-10, -12, -14, -16];
@@ -1,12 +1,12 @@
import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useShallow } from 'zustand/react/shallow';
import { useAuthStore } from '../../store/authStore';
import type { LyricsSourceId } from '../../store/authStoreTypes';
import { useListReorderDnd } from '../../hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '../../utils/componentHelpers/listReorder';
import { ReorderGripHandle } from './ReorderGripHandle';
import { SettingsToggle } from './SettingsToggle';
import { useAuthStore } from '@/store/authStore';
import type { LyricsSourceId } from '@/store/authStoreTypes';
import { useListReorderDnd } from '@/hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '@/utils/componentHelpers/listReorder';
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
const LYRICS_SOURCE_LABEL_KEYS: Record<LyricsSourceId, string> = {
server: 'settings.lyricsSourceServer',
@@ -1,11 +1,11 @@
import { useTranslation } from 'react-i18next';
import { AudioLines, Music2 } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { LyricsSourcesCustomizer } from './LyricsSourcesCustomizer';
import { SettingsSegmented, type SegmentedOption } from './SettingsSegmented';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import { useAuthStore } from '@/store/authStore';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { LyricsSourcesCustomizer } from '@/features/settings/components/LyricsSourcesCustomizer';
import { SettingsSegmented, type SegmentedOption } from '@/features/settings/components/SettingsSegmented';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
export function LyricsTab() {
const { t } = useTranslation();
@@ -1,24 +1,24 @@
import { useTranslation } from 'react-i18next';
import { Disc3, LayoutGrid, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
import { useArtistLayoutStore } from '../../store/artistLayoutStore';
import { useAuthStore } from '../../store/authStore';
import type { QueueDisplayMode } from '../../store/authStoreTypes';
import { useHomeStore } from '../../store/homeStore';
import { usePlayerBarLayoutStore } from '../../store/playerBarLayoutStore';
import { usePlaylistLayoutStore } from '../../store/playlistLayoutStore';
import { useQueueToolbarStore } from '../../store/queueToolbarStore';
import { useArtistLayoutStore } from '@/store/artistLayoutStore';
import { useAuthStore } from '@/store/authStore';
import type { QueueDisplayMode } from '@/store/authStoreTypes';
import { useHomeStore } from '@/store/homeStore';
import { usePlayerBarLayoutStore } from '@/store/playerBarLayoutStore';
import { usePlaylistLayoutStore } from '@/store/playlistLayoutStore';
import { useQueueToolbarStore } from '@/store/queueToolbarStore';
import { useSidebarStore } from '@/features/sidebar';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { SettingsSegmented, type SegmentedOption } from './SettingsSegmented';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import { ArtistLayoutCustomizer } from './ArtistLayoutCustomizer';
import { HomeCustomizer } from './HomeCustomizer';
import { PlayerBarLayoutCustomizer } from './PlayerBarLayoutCustomizer';
import { PlaylistLayoutCustomizer } from './PlaylistLayoutCustomizer';
import { QueueToolbarCustomizer } from './QueueToolbarCustomizer';
import { SidebarCustomizer } from './SidebarCustomizer';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSegmented, type SegmentedOption } from '@/features/settings/components/SettingsSegmented';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
import { ArtistLayoutCustomizer } from '@/features/settings/components/ArtistLayoutCustomizer';
import { HomeCustomizer } from '@/features/settings/components/HomeCustomizer';
import { PlayerBarLayoutCustomizer } from '@/features/settings/components/PlayerBarLayoutCustomizer';
import { PlaylistLayoutCustomizer } from '@/features/settings/components/PlaylistLayoutCustomizer';
import { QueueToolbarCustomizer } from '@/features/settings/components/QueueToolbarCustomizer';
import { SidebarCustomizer } from '@/features/settings/components/SidebarCustomizer';
export function PersonalisationTab() {
const { t } = useTranslation();
@@ -1,11 +1,11 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Gauge, Heart, PictureInPicture2, SlidersVertical, Star } from 'lucide-react';
import LastfmIcon from '../LastfmIcon';
import LastfmIcon from '@/components/LastfmIcon';
import {
usePlayerBarLayoutStore,
type PlayerBarLayoutItemId,
} from '../../store/playerBarLayoutStore';
} from '@/store/playerBarLayoutStore';
const PLAYER_BAR_LAYOUT_LABEL_KEYS: Record<PlayerBarLayoutItemId, string> = {
starRating: 'settings.playerBarStarRating',
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { Download, FileUp, HardDrive, Lightbulb, Search } from 'lucide-react';
import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '../../store/playlistLayoutStore';
import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '@/store/playlistLayoutStore';
const PLAYLIST_LAYOUT_ICONS: Record<PlaylistLayoutItemId, typeof Search> = {
addSongs: Search,
@@ -1,10 +1,10 @@
import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Blend, Infinity as InfinityIcon, ListMusic, MoveRight, Share2, Shuffle, Trash2, Waves } from 'lucide-react';
import { useQueueToolbarStore, QueueToolbarButtonId } from '../../store/queueToolbarStore';
import { useListReorderDnd } from '../../hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '../../utils/componentHelpers/listReorder';
import { ReorderGripHandle } from './ReorderGripHandle';
import { useQueueToolbarStore, QueueToolbarButtonId } from '@/store/queueToolbarStore';
import { useListReorderDnd } from '@/hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '@/utils/componentHelpers/listReorder';
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
const QUEUE_TOOLBAR_BUTTON_ICONS: Record<QueueToolbarButtonId, typeof Shuffle | null> = {
shuffle: Shuffle,
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { GripVertical } from 'lucide-react';
import { useDragSource } from '../../contexts/DragDropContext';
import { useDragSource } from '@/contexts/DragDropContext';
/**
* Drag handle shared by the reorder customizers. Emits an id-based payload
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { getCapabilityDefinition } from '../../serverCapabilities/catalog';
import { isFeatureActiveForServer, resolveFeatureForServer } from '../../serverCapabilities/storeView';
import { getCapabilityDefinition } from '@/serverCapabilities/catalog';
import { isFeatureActiveForServer, resolveFeatureForServer } from '@/serverCapabilities/storeView';
/** Inline badge for auto-managed server capabilities (Settings → Servers header row). */
export function ServerCapabilityHeaderBadge({
@@ -1,11 +1,11 @@
import { RefreshCw, ShieldCheck, WifiOff, Zap } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { SyncStateDto } from '../../api/library';
import type { SyncStateDto } from '@/api/library';
import {
libraryStatusDisplayTrackCount,
libraryStatusIsReady,
} from '../../utils/library/libraryReady';
import type { LibraryServerConnection } from '../../hooks/useLibraryIndexSync';
} from '@/utils/library/libraryReady';
import type { LibraryServerConnection } from '@/hooks/useLibraryIndexSync';
interface ServerLibraryIndexControlsProps {
status: SyncStateDto | null;
@@ -3,43 +3,43 @@ import { Trans, useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import { AlertTriangle, CheckCircle2, Info, Lock, LogOut, Pencil, Plus, Power, Server, Sparkles, Wifi, WifiOff } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import { formatServerSoftware, isNavidromeAudiomuseSoftwareEligible, type InstantMixProbeResult, type SubsonicServerIdentity } from '../../utils/server/subsonicServerIdentity';
import { buildCapabilityContext } from '../../serverCapabilities/context';
import { useLibraryIndexStore } from '../../store/libraryIndexStore';
import { libraryDeleteServerData, librarySyncClearSession } from '../../api/library';
import { bootstrapIndexedServer } from '../../utils/library/librarySession';
import { useLibraryIndexSync } from '../../hooks/useLibraryIndexSync';
import ServerLibraryIndexControls from './ServerLibraryIndexControls';
import type { ServerProfile } from '../../store/authStoreTypes';
import { pingWithCredentialsForProfile, scheduleInstantMixProbeForServer } from '../../api/subsonic';
import { useAuthStore } from '@/store/authStore';
import { formatServerSoftware, isNavidromeAudiomuseSoftwareEligible, type InstantMixProbeResult, type SubsonicServerIdentity } from '@/utils/server/subsonicServerIdentity';
import { buildCapabilityContext } from '@/serverCapabilities/context';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { libraryDeleteServerData, librarySyncClearSession } from '@/api/library';
import { bootstrapIndexedServer } from '@/utils/library/librarySession';
import { useLibraryIndexSync } from '@/hooks/useLibraryIndexSync';
import ServerLibraryIndexControls from '@/features/settings/components/ServerLibraryIndexControls';
import type { ServerProfile } from '@/store/authStoreTypes';
import { pingWithCredentialsForProfile, scheduleInstantMixProbeForServer } from '@/api/subsonic';
import {
clearServerHttpContext,
syncServerHttpContextForProfile,
} from '../../utils/server/syncServerHttpContext';
import { type ServerMagicPayload } from '../../utils/server/serverMagicString';
import { ensureConnectUrlResolved, invalidateReachableEndpointCache } from '../../utils/server/serverEndpoint';
} from '@/utils/server/syncServerHttpContext';
import { type ServerMagicPayload } from '@/utils/server/serverMagicString';
import { ensureConnectUrlResolved, invalidateReachableEndpointCache } from '@/utils/server/serverEndpoint';
import {
verifySameServerEndpoints,
type VerifySameServerResult,
} from '../../utils/server/serverFingerprint';
} from '@/utils/server/serverFingerprint';
import {
indexKeyRemapForUrlChange,
runIndexKeyRemigration,
} from '../../utils/server/serverUrlRemigration';
import { useConfirmModalStore } from '../../store/confirmModalStore';
import { showToast } from '../../utils/ui/toast';
import { FEATURE_AUDIOMUSE_SIMILAR_TRACKS } from '../../serverCapabilities/catalog';
import { isFeatureActiveForServer, resolveFeatureForServer } from '../../serverCapabilities/storeView';
import type { ResolvedCapability } from '../../serverCapabilities/types';
import { serverIdentityLabel, serverListDisplayLabel, serverSettingsEntryTitle } from '../../utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '../../utils/server/serverIndexKey';
import { switchActiveServer } from '../../utils/server/switchActiveServer';
import { AddServerForm } from './AddServerForm';
import { ServerCapabilityHeaderBadge } from './ServerCapabilityHeaderBadge';
import { useListReorderDnd } from '../../hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '../../utils/componentHelpers/listReorder';
import { ReorderGripHandle } from './ReorderGripHandle';
} from '@/utils/server/serverUrlRemigration';
import { useConfirmModalStore } from '@/store/confirmModalStore';
import { showToast } from '@/utils/ui/toast';
import { FEATURE_AUDIOMUSE_SIMILAR_TRACKS } from '@/serverCapabilities/catalog';
import { isFeatureActiveForServer, resolveFeatureForServer } from '@/serverCapabilities/storeView';
import type { ResolvedCapability } from '@/serverCapabilities/types';
import { serverIdentityLabel, serverListDisplayLabel, serverSettingsEntryTitle } from '@/utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { switchActiveServer } from '@/utils/server/switchActiveServer';
import { AddServerForm } from '@/features/settings/components/AddServerForm';
import { ServerCapabilityHeaderBadge } from '@/features/settings/components/ServerCapabilityHeaderBadge';
import { useListReorderDnd } from '@/hooks/useListReorderDnd';
import { applyListReorderById, type ListReorderDropTarget } from '@/utils/componentHelpers/listReorder';
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
import { tooltipAttrs } from '@/ui/tooltipAttrs';
const AUDIOMUSE_NV_PLUGIN_URL = 'https://github.com/NeptuneHub/AudioMuse-AI-NV-plugin';
@@ -1,7 +1,7 @@
import React, { useId } from 'react';
import { ChevronDown } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useAuthStore } from '@/store/authStore';
interface SettingsSubSectionProps {
title: string;
@@ -1,15 +1,15 @@
import { useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../../store/authStore';
import { useAuthStore } from '@/store/authStore';
import { useSidebarStore, SidebarItemConfig, CONSERVED_SIDEBAR_NAV_IDS } from '@/features/sidebar';
import { useLuckyMixAvailable } from '../../hooks/useLuckyMixAvailable';
import { ALL_NAV_ITEMS } from '../../config/navItems';
import { useLuckyMixAvailable } from '@/hooks/useLuckyMixAvailable';
import { ALL_NAV_ITEMS } from '@/config/navItems';
import { applySidebarReorderById } from '@/features/sidebar';
import { useListReorderDnd } from '../../hooks/useListReorderDnd';
import type { ListReorderDropTarget } from '../../utils/componentHelpers/listReorder';
import { ReorderGripHandle } from './ReorderGripHandle';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { useListReorderDnd } from '@/hooks/useListReorderDnd';
import type { ListReorderDropTarget } from '@/utils/componentHelpers/listReorder';
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
const REORDER_TYPE = 'sidebar_reorder';
@@ -3,15 +3,15 @@ import { useTranslation } from 'react-i18next';
import { invoke } from '@tauri-apps/api/core';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { Download, FolderOpen, Trash2, X } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import { countHotCacheTracks } from '../../store/hotCacheStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore';
import { formatBytes, snapHotCacheMb } from '../../utils/format/formatBytes';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsValue } from './SettingsSubCard';
import CoverCacheStrategySection from './CoverCacheStrategySection';
import { useAuthStore } from '@/store/authStore';
import { countHotCacheTracks } from '@/store/hotCacheStore';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { formatBytes, snapHotCacheMb } from '@/utils/format/formatBytes';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsValue } from '@/features/settings/components/SettingsSubCard';
import CoverCacheStrategySection from '@/features/settings/components/CoverCacheStrategySection';
export function StorageTab() {
const { t } = useTranslation();
@@ -5,21 +5,21 @@ import { invoke } from '@tauri-apps/api/core';
import { save as saveDialog } from '@tauri-apps/plugin-dialog';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import { AppWindow, ChevronDown, Download, ExternalLink, Globe, HardDrive, Info, Scale, Sliders, Users } from 'lucide-react';
import { version as appVersion } from '../../../package.json';
import i18n from '../../i18n';
import { useAuthStore } from '../../store/authStore';
import type { ClockFormat, LinuxWaylandTextRenderProfile, LoggingMode } from '../../store/authStoreTypes';
import { IS_LINUX } from '../../utils/platform';
import { showToast } from '../../utils/ui/toast';
import { AboutPsysonicBrandHeader } from '../AboutPsysonicLol';
import { version as appVersion } from '@/../package.json';
import i18n from '@/i18n';
import { useAuthStore } from '@/store/authStore';
import type { ClockFormat, LinuxWaylandTextRenderProfile, LoggingMode } from '@/store/authStoreTypes';
import { IS_LINUX } from '@/utils/platform';
import { showToast } from '@/utils/ui/toast';
import { AboutPsysonicBrandHeader } from '@/components/AboutPsysonicLol';
import CustomSelect from '@/ui/CustomSelect';
import LicensesPanel from '../LicensesPanel';
import SettingsSubSection from '../SettingsSubSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsToggle } from './SettingsToggle';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import { BackupSection } from './BackupSection';
import { CONTRIBUTORS, MAINTAINERS } from '../../config/settingsCredits';
import LicensesPanel from '@/components/LicensesPanel';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
import { BackupSection } from '@/features/settings/components/BackupSection';
import { CONTRIBUTORS, MAINTAINERS } from '@/config/settingsCredits';
export function SystemTab() {
const { t } = useTranslation();
@@ -3,10 +3,10 @@ import { Upload, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-dialog';
import { useInstalledThemesStore } from '../../store/installedThemesStore';
import { validateThemePackage, type ValidatedTheme } from '../../utils/themes/validateThemePackage';
import { showToast } from '../../utils/ui/toast';
import ConfirmModal from '../ConfirmModal';
import { useInstalledThemesStore } from '@/store/installedThemesStore';
import { validateThemePackage, type ValidatedTheme } from '@/utils/themes/validateThemePackage';
import { showToast } from '@/utils/ui/toast';
import ConfirmModal from '@/components/ConfirmModal';
/**
* Import a community theme from a local `.zip` (manifest.json + theme.css).
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import { ThemeStoreSection } from './ThemeStoreSection';
import { ThemeStoreSection } from '@/features/settings/components/ThemeStoreSection';
import type { FetchRegistryResult, Registry, RegistryTheme } from '@/utils/themes/themeRegistry';
// Control the registry the store browses so pagination/refresh are deterministic.
@@ -2,21 +2,21 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Check, ChevronLeft, ChevronRight, Download, RefreshCw, Trash2, WifiOff } from 'lucide-react';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import CoverLightbox from '../CoverLightbox';
import { useThemeAnimationRisk } from '../../hooks/useThemeAnimationRisk';
import { AnimatedThemeBadge } from './AnimatedThemeBadge';
import CoverLightbox from '@/components/CoverLightbox';
import { useThemeAnimationRisk } from '@/hooks/useThemeAnimationRisk';
import { AnimatedThemeBadge } from '@/features/settings/components/AnimatedThemeBadge';
import CustomSelect from '@/ui/CustomSelect';
import { formatRelativeTime } from '../../utils/format/relativeTime';
import { useThemeStore } from '../../store/themeStore';
import { useInstalledThemesStore, type InstalledTheme } from '../../store/installedThemesStore';
import { formatRelativeTime } from '@/utils/format/relativeTime';
import { useThemeStore } from '@/store/themeStore';
import { useInstalledThemesStore, type InstalledTheme } from '@/store/installedThemesStore';
import {
assetUrl,
fetchRegistry,
type RegistryTheme,
} from '../../utils/themes/themeRegistry';
import { installThemeFromRegistry } from '../../utils/themes/installThemeFromRegistry';
import { uninstallTheme } from '../../utils/themes/uninstallTheme';
import { isNewer } from '../../utils/componentHelpers/appUpdaterHelpers';
} from '@/utils/themes/themeRegistry';
import { installThemeFromRegistry } from '@/utils/themes/installThemeFromRegistry';
import { uninstallTheme } from '@/utils/themes/uninstallTheme';
import { isNewer } from '@/utils/componentHelpers/appUpdaterHelpers';
type ModeFilter = 'all' | 'dark' | 'light';
type SortMode = 'newest' | 'name';
@@ -1,16 +1,16 @@
import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { Clock, Palette, Store, Upload } from 'lucide-react';
import { useThemeStore } from '../../store/themeStore';
import { useInstalledThemesStore } from '../../store/installedThemesStore';
import { useThemeStore } from '@/store/themeStore';
import { useInstalledThemesStore } from '@/store/installedThemesStore';
import CustomSelect from '@/ui/CustomSelect';
import BackToTopButton from '@/ui/BackToTopButton';
import { FIXED_THEMES } from './fixedThemes';
import { InstalledThemes } from './InstalledThemes';
import { ThemeImportSection } from './ThemeImportSection';
import { ThemeStoreSection } from './ThemeStoreSection';
import { SettingsGroup } from './SettingsGroup';
import { SettingsSubCard, SettingsField } from './SettingsSubCard';
import { FIXED_THEMES } from '@/components/settings/fixedThemes';
import { InstalledThemes } from '@/features/settings/components/InstalledThemes';
import { ThemeImportSection } from '@/features/settings/components/ThemeImportSection';
import { ThemeStoreSection } from '@/features/settings/components/ThemeStoreSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
/**
* A flat, always-visible section. The Themes tab has a single purpose, so its
@@ -1,15 +1,15 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Shield, Wand2 } from 'lucide-react';
import { ndUpdateUser, type NdLibrary, type NdUser } from '../../api/navidromeAdmin';
import { showToast } from '../../utils/ui/toast';
import { ndUpdateUser, type NdLibrary, type NdUser } from '@/api/navidromeAdmin';
import { showToast } from '@/utils/ui/toast';
import {
copyTextToClipboard,
encodeServerMagicString,
magicPayloadAddressFields,
} from '../../utils/server/serverMagicString';
import { shortHostFromServerUrl } from '../../utils/server/serverDisplayName';
import { useAuthStore } from '../../store/authStore';
} from '@/utils/server/serverMagicString';
import { shortHostFromServerUrl } from '@/utils/server/serverDisplayName';
import { useAuthStore } from '@/store/authStore';
export interface UserFormState {
userName: string;
@@ -1,13 +1,13 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RotateCcw, UserPlus, Users } from 'lucide-react';
import type { NdUser } from '../../api/navidromeAdmin';
import ConfirmModal from '../ConfirmModal';
import { useUserMgmtData } from '../../hooks/useUserMgmtData';
import { useUserMgmtActions } from '../../hooks/useUserMgmtActions';
import { UserForm } from './UserForm';
import { UserMgmtRow } from './userMgmt/UserMgmtRow';
import { MagicStringModal } from './userMgmt/MagicStringModal';
import type { NdUser } from '@/api/navidromeAdmin';
import ConfirmModal from '@/components/ConfirmModal';
import { useUserMgmtData } from '@/features/settings/hooks/useUserMgmtData';
import { useUserMgmtActions } from '@/features/settings/hooks/useUserMgmtActions';
import { UserForm } from '@/features/settings/components/UserForm';
import { UserMgmtRow } from '@/features/settings/components/userMgmt/UserMgmtRow';
import { MagicStringModal } from '@/features/settings/components/userMgmt/MagicStringModal';
export function UserManagementSection({
serverUrl,
@@ -3,12 +3,12 @@ import { invoke } from '@tauri-apps/api/core';
import { AudioLines, RotateCcw } from 'lucide-react';
import type { TFunction } from 'i18next';
import CustomSelect from '@/ui/CustomSelect';
import SettingsSubSection from '../../SettingsSubSection';
import { SettingsGroup } from '../SettingsGroup';
import { SettingsToggle } from '../SettingsToggle';
import { useAuthStore } from '../../../store/authStore';
import { useEqStore } from '../../../store/eqStore';
import { buildAudioDeviceSelectOptions } from '../../../utils/audio/audioDeviceLabels';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { useAuthStore } from '@/store/authStore';
import { useEqStore } from '@/store/eqStore';
import { buildAudioDeviceSelectOptions } from '@/utils/audio/audioDeviceLabels';
interface Props {
audioDevices: string[];
@@ -2,9 +2,9 @@ import {
HI_RES_CROSSFADE_RESAMPLE_OPTIONS,
type HiResCrossfadeResampleHz,
sanitizeHiResCrossfadeResampleHz,
} from '../../../utils/audio/hiResCrossfadeResample';
} from '@/utils/audio/hiResCrossfadeResample';
import type { TFunction } from 'i18next';
import { SettingsSubCard, SettingsField } from '../SettingsSubCard';
import { SettingsSubCard, SettingsField } from '@/features/settings/components/SettingsSubCard';
interface Props {
enabled: boolean;
@@ -1,11 +1,11 @@
import React from 'react';
import { RotateCcw } from 'lucide-react';
import type { TFunction } from 'i18next';
import { useAuthStore } from '../../../store/authStore';
import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from '../../../store/authStoreDefaults';
import { LoudnessLufsButtonGroup } from '../LoudnessLufsButtonGroup';
import { SettingsGroup } from '../SettingsGroup';
import { SettingsSubCard, SettingsField, SettingsValue, SettingsCallout } from '../SettingsSubCard';
import { useAuthStore } from '@/store/authStore';
import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from '@/store/authStoreDefaults';
import { LoudnessLufsButtonGroup } from '@/features/settings/components/LoudnessLufsButtonGroup';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsSubCard, SettingsField, SettingsValue, SettingsCallout } from '@/features/settings/components/SettingsSubCard';
interface Props {
preAnalysisEffectiveDb: number;
@@ -17,13 +17,13 @@ import {
playbackSpeedStep,
varispeedSpeedFromSemitones,
type PlaybackStrategy,
} from '../../../utils/audio/playbackRateHelpers';
import { usePlaybackRateStore } from '../../../store/playbackRateStore';
import { useOrbitStore } from '../../../store/orbitStore';
import { useAuthStore } from '../../../store/authStore';
import { isOrbitPlaybackSyncActive } from '../../../utils/orbit';
import { SettingsToggle } from '../SettingsToggle';
import { SettingsSubCard } from '../SettingsSubCard';
} from '@/utils/audio/playbackRateHelpers';
import { usePlaybackRateStore } from '@/store/playbackRateStore';
import { useOrbitStore } from '@/store/orbitStore';
import { useAuthStore } from '@/store/authStore';
import { isOrbitPlaybackSyncActive } from '@/utils/orbit';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard } from '@/features/settings/components/SettingsSubCard';
interface Props {
t: TFunction;
@@ -1,13 +1,13 @@
import React from 'react';
import { Play } from 'lucide-react';
import type { TFunction } from 'i18next';
import { useAuthStore } from '../../../store/authStore';
import { TRACK_PREVIEW_LOCATIONS } from '../../../store/authStoreDefaults';
import type { TrackPreviewLocation } from '../../../store/authStoreTypes';
import SettingsSubSection from '../../SettingsSubSection';
import { SettingsGroup } from '../SettingsGroup';
import { SettingsToggle } from '../SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsValue } from '../SettingsSubCard';
import { useAuthStore } from '@/store/authStore';
import { TRACK_PREVIEW_LOCATIONS } from '@/store/authStoreDefaults';
import type { TrackPreviewLocation } from '@/store/authStoreTypes';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsValue } from '@/features/settings/components/SettingsSubCard';
interface Props {
t: TFunction;
@@ -1,20 +1,20 @@
import React from 'react';
import type { TFunction } from 'i18next';
import { useAuthStore } from '../../../store/authStore';
import { useOrbitStore } from '../../../store/orbitStore';
import { useAuthStore } from '@/store/authStore';
import { useOrbitStore } from '@/store/orbitStore';
import {
AUTODJ_OVERLAP_CAP_MAX_SEC,
AUTODJ_OVERLAP_CAP_MIN_SEC,
} from '../../../utils/playback/autodjOverlapCap';
} from '@/utils/playback/autodjOverlapCap';
import {
getTransitionMode,
setTransitionMode,
type TransitionMode,
} from '../../../utils/playback/playbackTransition';
import { SettingsGroup } from '../SettingsGroup';
import { SettingsToggle } from '../SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsRow, SettingsValue } from '../SettingsSubCard';
import { SettingsSegmented, type SegmentedOption } from '../SettingsSegmented';
} from '@/utils/playback/playbackTransition';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { SettingsSubCard, SettingsField, SettingsRow, SettingsValue } from '@/features/settings/components/SettingsSubCard';
import { SettingsSegmented, type SegmentedOption } from '@/features/settings/components/SettingsSegmented';
interface Props {
t: TFunction;
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { moveSourceTo, dropSourceBefore } from './backdropReorder';
import type { BackdropSource, BackdropSourcePref } from '../../cover/artistBackdrop';
import { moveSourceTo, dropSourceBefore } from '@/features/settings/components/backdropReorder';
import type { BackdropSource, BackdropSourcePref } from '@/cover/artistBackdrop';
const S = (...names: BackdropSource[]): BackdropSourcePref[] =>
names.map((source) => ({ source, enabled: true }));
@@ -1,4 +1,4 @@
import type { BackdropSourcePref } from '../../cover/artistBackdrop';
import type { BackdropSourcePref } from '@/cover/artistBackdrop';
/**
* Move the source at `from` so it lands at exactly index `to` the / buttons,
@@ -6,8 +6,8 @@ import {
listPresets,
type BuiltinPreset,
type PresetId,
} from '../../../music-network';
import { renderPresetIcon } from './presetIcon';
} from '@/music-network';
import { renderPresetIcon } from '@/components/settings/musicNetwork/presetIcon';
/**
* "Add a service" list, driven entirely by the preset registry. Token-poll
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next';
import CustomSelect from '@/ui/CustomSelect';
import { SettingsGroup } from '../SettingsGroup';
import type { Account } from '../../../music-network';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import type { Account } from '@/music-network';
/**
* Picks the single enrichment primary (love / similar / stats source). Only
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { AlertTriangle } from 'lucide-react';
import type { Account } from '../../../music-network';
import type { Account } from '@/music-network';
/**
* Shown when a Maloja account is connected AND Last.fm scrobbling is enabled
@@ -1,22 +1,22 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Share2 } from 'lucide-react';
import SettingsSubSection from '../../SettingsSubSection';
import { SettingsGroup } from '../SettingsGroup';
import { showToast } from '../../../utils/ui/toast';
import { useAuthStore } from '../../../store/authStore';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { showToast } from '@/utils/ui/toast';
import { useAuthStore } from '@/store/authStore';
import {
errorI18nKey,
getMusicNetworkRuntime,
isMusicNetworkError,
type PresetId,
type UserProfile,
} from '../../../music-network';
import { useMusicNetworkState } from './useMusicNetworkState';
import { ScrobbleDestinationCard } from './ScrobbleDestinationCard';
import { EnrichmentPrimarySelect } from './EnrichmentPrimarySelect';
import { ConnectProviderForm } from './ConnectProviderForm';
import { MalojaProxyWarning } from './MalojaProxyWarning';
} from '@/music-network';
import { useMusicNetworkState } from '@/features/settings/components/musicNetwork/useMusicNetworkState';
import { ScrobbleDestinationCard } from '@/features/settings/components/musicNetwork/ScrobbleDestinationCard';
import { EnrichmentPrimarySelect } from '@/features/settings/components/musicNetwork/EnrichmentPrimarySelect';
import { ConnectProviderForm } from '@/features/settings/components/musicNetwork/ConnectProviderForm';
import { MalojaProxyWarning } from '@/features/settings/components/musicNetwork/MalojaProxyWarning';
/**
* Integrations UI for the Music Network framework replaces the old Last.fm
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { getPreset, type Account, type UserProfile } from '../../../music-network';
import { renderPresetIcon } from './presetIcon';
import { getPreset, type Account, type UserProfile } from '@/music-network';
import { renderPresetIcon } from '@/components/settings/musicNetwork/presetIcon';
/**
* One connected account as a single self-contained block: header (icon, label,
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useAuthStore } from '../../../store/authStore';
import { getPreset, type Account } from '../../../music-network';
import { useAuthStore } from '@/store/authStore';
import { getPreset, type Account } from '@/music-network';
/**
* Reactive view of the persisted Music Network state for the Integrations UI.
@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import type { TFunction } from 'i18next';
import { matchScore } from './settingsTabs';
import { searchSettings } from './settingsSearch';
import { matchScore } from '@/features/settings/components/settingsTabs';
import { searchSettings } from '@/features/settings/components/settingsSearch';
const t = vi.fn((key: string) => {
const labels: Record<string, string> = {
@@ -1,6 +1,6 @@
import type { TFunction } from 'i18next';
import { GLOBAL_SHORTCUT_ACTIONS, IN_APP_SHORTCUT_ACTIONS } from '../../config/shortcutActions';
import { SETTINGS_INDEX, matchScore, type Tab } from './settingsTabs';
import { GLOBAL_SHORTCUT_ACTIONS, IN_APP_SHORTCUT_ACTIONS } from '@/config/shortcutActions';
import { SETTINGS_INDEX, matchScore, type Tab } from '@/features/settings/components/settingsTabs';
export type SettingsSearchHit = {
tab: Tab;
@@ -2,15 +2,15 @@ import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { X } from 'lucide-react';
import type { TFunction } from 'i18next';
import { ndUpdateUser, type NdUser } from '../../../api/navidromeAdmin';
import { showToast } from '../../../utils/ui/toast';
import { ndUpdateUser, type NdUser } from '@/api/navidromeAdmin';
import { showToast } from '@/utils/ui/toast';
import {
copyTextToClipboard,
encodeServerMagicString,
magicPayloadAddressFields,
} from '../../../utils/server/serverMagicString';
import { shortHostFromServerUrl } from '../../../utils/server/serverDisplayName';
import { useAuthStore } from '../../../store/authStore';
} from '@/utils/server/serverMagicString';
import { shortHostFromServerUrl } from '@/utils/server/serverDisplayName';
import { useAuthStore } from '@/store/authStore';
interface Props {
user: NdUser;
@@ -1,8 +1,8 @@
import React from 'react';
import { Shield, Trash2, User, Wand2 } from 'lucide-react';
import type { i18n as I18nType, TFunction } from 'i18next';
import type { NdLibrary, NdUser } from '../../../api/navidromeAdmin';
import { formatLastSeen } from '../../../utils/componentHelpers/userMgmtHelpers';
import type { NdLibrary, NdUser } from '@/api/navidromeAdmin';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
interface Props {
user: NdUser;
@@ -7,14 +7,14 @@ import {
ndUpdateUser,
type NdLibrary,
type NdUser,
} from '../api/navidromeAdmin';
import { showToast } from '../utils/ui/toast';
} from '@/api/navidromeAdmin';
import { showToast } from '@/utils/ui/toast';
import {
copyTextToClipboard,
encodeServerMagicString,
} from '../utils/server/serverMagicString';
import { shortHostFromServerUrl } from '../utils/server/serverDisplayName';
import type { UserFormState } from '../components/settings/UserForm';
} from '@/utils/server/serverMagicString';
import { shortHostFromServerUrl } from '@/utils/server/serverDisplayName';
import type { UserFormState } from '@/features/settings/components/UserForm';
interface UseUserMgmtActionsArgs {
serverUrl: string;
@@ -5,7 +5,7 @@ import {
ndListUsers,
type NdLibrary,
type NdUser,
} from '../api/navidromeAdmin';
} from '@/api/navidromeAdmin';
interface UseUserMgmtDataResult {
users: NdUser[];
+16
View File
@@ -0,0 +1,16 @@
/**
* Settings feature — the Settings page (lazy via the deep path `pages/Settings`)
* and all its tabs/sections (servers, audio, input, library, lyrics, storage,
* personalisation, integrations, system, the theme management UI, music-network
* config, user management), plus the settings layout primitives.
*
* Cross-cutting state this feature consumes but does NOT own: `authStore` and its
* `auth*SettingsActions` slices, the theme stores + theme infra utils, the
* library-index sync hook, and `settingsCredits` — all live in the core/global
* layer.
*
* Public surface (consumed by other features / app shell):
*/
export { default as SettingsSubSection } from './components/SettingsSubSection';
export { PlaybackRateControls } from './components/audio/PlaybackRateBlock';
export { CustomHttpHeadersEditor } from './components/CustomHttpHeadersEditor';
@@ -4,24 +4,24 @@ import { useLocation, useNavigate } from 'react-router-dom';
import {
AudioLines, HardDrive, Info, Keyboard, LayoutGrid, Music2, Paintbrush, Palette, Search, Server, Sparkles, Users, X,
} from 'lucide-react';
import { useAuthStore } from '../store/authStore';
import { IS_MACOS } from '../utils/platform';
import { AppearanceTab } from '../components/settings/AppearanceTab';
import { ThemesTab } from '../components/settings/ThemesTab';
import { AudioTab } from '../components/settings/AudioTab';
import { InputTab } from '../components/settings/InputTab';
import { IntegrationsTab } from '../components/settings/IntegrationsTab';
import { LibraryTab } from '../components/settings/LibraryTab';
import { LyricsTab } from '../components/settings/LyricsTab';
import { PersonalisationTab } from '../components/settings/PersonalisationTab';
import { ServersTab } from '../components/settings/ServersTab';
import { StorageTab } from '../components/settings/StorageTab';
import { SystemTab } from '../components/settings/SystemTab';
import { searchSettings, type SettingsSearchHit } from '../components/settings/settingsSearch';
import { type Tab, resolveTab } from '../components/settings/settingsTabs';
import { UserManagementSection } from '../components/settings/UserManagementSection';
import { ndLogin } from '../api/navidromeAdmin';
import { type ServerMagicPayload } from '../utils/server/serverMagicString';
import { useAuthStore } from '@/store/authStore';
import { IS_MACOS } from '@/utils/platform';
import { AppearanceTab } from '@/features/settings/components/AppearanceTab';
import { ThemesTab } from '@/features/settings/components/ThemesTab';
import { AudioTab } from '@/features/settings/components/AudioTab';
import { InputTab } from '@/features/settings/components/InputTab';
import { IntegrationsTab } from '@/features/settings/components/IntegrationsTab';
import { LibraryTab } from '@/features/settings/components/LibraryTab';
import { LyricsTab } from '@/features/settings/components/LyricsTab';
import { PersonalisationTab } from '@/features/settings/components/PersonalisationTab';
import { ServersTab } from '@/features/settings/components/ServersTab';
import { StorageTab } from '@/features/settings/components/StorageTab';
import { SystemTab } from '@/features/settings/components/SystemTab';
import { searchSettings, type SettingsSearchHit } from '@/features/settings/components/settingsSearch';
import { type Tab, resolveTab } from '@/features/settings/components/settingsTabs';
import { UserManagementSection } from '@/features/settings/components/UserManagementSection';
import { ndLogin } from '@/api/navidromeAdmin';
import { type ServerMagicPayload } from '@/utils/server/serverMagicString';
export default function Settings() {
@@ -1,7 +1,7 @@
import { save, open as openDialog } from '@tauri-apps/plugin-dialog';
import { writeFile, readTextFile } from '@tauri-apps/plugin-fs';
import { invoke } from '@tauri-apps/api/core';
import { version as appVersion } from '../../../package.json';
import { version as appVersion } from '@/../package.json';
const BACKUP_VERSION = 1;
export type ImportedBackupKind = 'config' | 'databases' | 'full';
+1 -1
View File
@@ -4,7 +4,7 @@ import { Wifi, WifiOff, Eye, EyeOff, Server, Globe } from 'lucide-react';
import { useAuthStore } from '../store/authStore';
import type { CustomHeaderEntry, CustomHeadersApplyTo, ServerProfile } from '../store/authStoreTypes';
import { pingWithCredentialsForProfile, scheduleInstantMixProbeForServer } from '../api/subsonic';
import { CustomHttpHeadersEditor } from '../components/settings/CustomHttpHeadersEditor';
import { CustomHttpHeadersEditor } from '@/features/settings';
import {
DEFAULT_CUSTOM_HEADERS_APPLY_TO,
serverCustomHeadersFromForm,