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