From 320eb97c03356939f830e74fa52244c5d6364b9e Mon Sep 17 00:00:00 2001 From: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com> Date: Wed, 13 May 2026 01:57:26 +0200 Subject: [PATCH] =?UTF-8?q?refactor(settings):=20G.56=20=E2=80=94=20extrac?= =?UTF-8?q?t=20Lyrics=20+=20Personalisation=20+=20Input=20tabs=20(#621)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tab-section components carved out of the Settings() default- export body. Each owns its store hooks + local state; Settings() now only routes via `{activeTab === 'X' && }`. - `LyricsTab.tsx` (~50 LOC, was ~40 inline) — wraps `LyricsSourcesCustomizer` + sidebar lyrics style toggles. Owns the two `useAuthStore(s => s.sidebarLyricsStyle/...)` selectors. - `PersonalisationTab.tsx` (~95 LOC, was ~80 inline) — wraps the four customizers (sidebar / artist layout / home / queue toolbar) with their per-store reset buttons. - `InputTab.tsx` (~185 LOC, was ~170 inline) — keybindings + global shortcuts. Owns the two `listeningFor` / `listeningForGlobal` state slots that were previously hoisted into Settings(). 13 now-unused imports trimmed (4 customizer-store hooks, 6 keybinding helpers, `Music2/AudioLines` kept since the tab-button array still uses them as icons, `LayoutGrid/Keyboard` kept for the same reason). Pure code-move. Settings.tsx: 3037 → 2741 LOC (−296). Phase-G journey: 5298 → 2741 LOC (~48% reduction). --- src/components/settings/InputTab.tsx | 184 +++++++++++ src/components/settings/LyricsTab.tsx | 52 +++ .../settings/PersonalisationTab.tsx | 94 ++++++ src/pages/Settings.tsx | 312 +----------------- 4 files changed, 338 insertions(+), 304 deletions(-) create mode 100644 src/components/settings/InputTab.tsx create mode 100644 src/components/settings/LyricsTab.tsx create mode 100644 src/components/settings/PersonalisationTab.tsx diff --git a/src/components/settings/InputTab.tsx b/src/components/settings/InputTab.tsx new file mode 100644 index 00000000..21e54cbb --- /dev/null +++ b/src/components/settings/InputTab.tsx @@ -0,0 +1,184 @@ +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'; + +export function InputTab() { + const { t } = useTranslation(); + const kb = useKeybindingsStore(); + const gs = useGlobalShortcutsStore(); + const [listeningFor, setListeningFor] = useState(null); + const [listeningForGlobal, setListeningForGlobal] = useState(null); + + return ( + <> + } + action={ + { kb.resetToDefaults(); setListeningFor(null); }} + data-tooltip={t('settings.shortcutsReset')} + aria-label={t('settings.shortcutsReset')} + > + + + } + > + + + {IN_APP_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => { + const label = getLabel(t); + const bound = kb.bindings[action]; + const isListening = listeningFor === action; + return ( + + {label} + + { + if (isListening) { setListeningFor(null); return; } + setListeningFor(action); + const handler = (e: KeyboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.code === 'Escape') { + setListeningFor(null); + window.removeEventListener('keydown', handler, true); + return; + } + const chord = buildInAppBinding(e); + if (!chord) return; + const existing = (Object.entries(kb.bindings) as [KeyAction, string | null][]) + .find(([, c]) => c === chord)?.[0]; + if (existing && existing !== action) kb.setBinding(existing, null); + kb.setBinding(action, chord); + setListeningFor(null); + window.removeEventListener('keydown', handler, true); + }; + window.addEventListener('keydown', handler, true); + }} + className="keybind-badge" + style={{ + minWidth: 72, padding: '3px 10px', borderRadius: 'var(--radius-sm)', + fontSize: 12, fontWeight: 600, fontFamily: 'monospace', + background: isListening ? 'var(--accent)' : bound ? 'var(--bg-hover)' : 'var(--bg-card)', + color: isListening ? 'var(--ctp-base)' : bound ? 'var(--text-primary)' : 'var(--text-muted)', + border: `1px solid ${isListening ? 'var(--accent)' : 'var(--border-subtle)'}`, + cursor: 'pointer', + }} + > + {isListening ? t('settings.shortcutListening') : bound ? formatBinding(bound) : t('settings.shortcutUnbound')} + + {bound && !isListening && ( + kb.setBinding(action, null)} + style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px', lineHeight: 1 }} + data-tooltip={t('settings.shortcutClear')} + > + + + )} + + + ); + })} + + + + + } + description={t('settings.globalShortcutsNote')} + action={ + { gs.resetAll(); setListeningForGlobal(null); }} + data-tooltip={t('settings.shortcutsReset')} + aria-label={t('settings.shortcutsReset')} + > + + + } + > + + + {GLOBAL_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => { + const label = getLabel(t); + const bound = gs.shortcuts[action] ?? null; + const isListening = listeningForGlobal === action; + return ( + + {label} + + { + if (isListening) { setListeningForGlobal(null); return; } + setListeningForGlobal(action); + const handler = (e: KeyboardEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.code === 'Escape') { + setListeningForGlobal(null); + window.removeEventListener('keydown', handler, true); + return; + } + const shortcut = buildGlobalShortcut(e); + if (shortcut) { + gs.setShortcut(action, shortcut); + setListeningForGlobal(null); + window.removeEventListener('keydown', handler, true); + } + }; + window.addEventListener('keydown', handler, true); + }} + className="keybind-badge" + style={{ + minWidth: 120, padding: '3px 10px', borderRadius: 'var(--radius-sm)', + fontSize: 12, fontWeight: 600, fontFamily: 'monospace', + background: isListening ? 'var(--accent)' : bound ? 'var(--bg-hover)' : 'var(--bg-card)', + color: isListening ? 'var(--ctp-base)' : bound ? 'var(--text-primary)' : 'var(--text-muted)', + border: `1px solid ${isListening ? 'var(--accent)' : 'var(--border-subtle)'}`, + cursor: 'pointer', + }} + > + {isListening ? t('settings.shortcutListening') : bound ? formatGlobalShortcut(bound) : t('settings.shortcutUnbound')} + + {bound && !isListening && ( + gs.setShortcut(action, null)} + style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px', lineHeight: 1 }} + data-tooltip={t('settings.shortcutClear')} + > + + + )} + + + ); + })} + + + + > + ); +} diff --git a/src/components/settings/LyricsTab.tsx b/src/components/settings/LyricsTab.tsx new file mode 100644 index 00000000..9fd8ce73 --- /dev/null +++ b/src/components/settings/LyricsTab.tsx @@ -0,0 +1,52 @@ +import { useTranslation } from 'react-i18next'; +import { AudioLines, Music2 } from 'lucide-react'; +import { useAuthStore } from '../../store/authStore'; +import SettingsSubSection from '../SettingsSubSection'; +import { LyricsSourcesCustomizer } from './LyricsSourcesCustomizer'; + +export function LyricsTab() { + const { t } = useTranslation(); + const sidebarLyricsStyle = useAuthStore(s => s.sidebarLyricsStyle); + const setSidebarLyricsStyle = useAuthStore(s => s.setSidebarLyricsStyle); + + return ( + <> + } + > + + + + } + > + + {(['classic', 'apple'] as const).map(style => { + const key = style === 'classic' ? 'Classic' : 'Apple'; + const other = style === 'classic' ? 'apple' : 'classic'; + return ( + + + + {t(`settings.sidebarLyricsStyle${key}` as any)} + {t(`settings.sidebarLyricsStyle${key}Desc` as any)} + + + setSidebarLyricsStyle(e.target.checked ? style : other)} + /> + + + + + ); + })} + + + > + ); +} diff --git a/src/components/settings/PersonalisationTab.tsx b/src/components/settings/PersonalisationTab.tsx new file mode 100644 index 00000000..ee8dde25 --- /dev/null +++ b/src/components/settings/PersonalisationTab.tsx @@ -0,0 +1,94 @@ +import { useTranslation } from 'react-i18next'; +import { LayoutGrid, ListMusic, PanelLeft, RotateCcw, Users } from 'lucide-react'; +import { useArtistLayoutStore } from '../../store/artistLayoutStore'; +import { useHomeStore } from '../../store/homeStore'; +import { useQueueToolbarStore } from '../../store/queueToolbarStore'; +import { useSidebarStore } from '../../store/sidebarStore'; +import SettingsSubSection from '../SettingsSubSection'; +import { ArtistLayoutCustomizer } from './ArtistLayoutCustomizer'; +import { HomeCustomizer } from './HomeCustomizer'; +import { QueueToolbarCustomizer } from './QueueToolbarCustomizer'; +import { SidebarCustomizer } from './SidebarCustomizer'; + +export function PersonalisationTab() { + const { t } = useTranslation(); + return ( + <> + } + action={ + useSidebarStore.getState().reset()} + data-tooltip={t('settings.sidebarReset')} + aria-label={t('settings.sidebarReset')} + > + + + } + > + + + + } + action={ + useArtistLayoutStore.getState().reset()} + data-tooltip={t('settings.artistLayoutReset')} + aria-label={t('settings.artistLayoutReset')} + > + + + } + > + + + + } + action={ + useHomeStore.getState().reset()} + data-tooltip={t('settings.sidebarReset')} + aria-label={t('settings.sidebarReset')} + > + + + } + > + + + + } + action={ + useQueueToolbarStore.getState().reset()} + data-tooltip={t('settings.queueToolbarReset')} + aria-label={t('settings.queueToolbarReset')} + > + + + } + > + + + > + ); +} diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 163359e4..f5896696 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -6,8 +6,8 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { Wifi, WifiOff, Globe, Music2, Sliders, LogOut, CheckCircle2, FolderOpen, Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, Shuffle, X, Play, Type, Keyboard, ChevronDown, - PanelLeft, RotateCcw, LayoutGrid, AppWindow, HardDrive, Download, Waves, Star, Clock, ZoomIn, Sparkles, AlertTriangle, Maximize2, AudioLines, User, Lock, - Users, Search, Scale, ListMusic + RotateCcw, LayoutGrid, AppWindow, HardDrive, Download, Waves, Star, Clock, ZoomIn, Sparkles, AlertTriangle, Maximize2, AudioLines, User, Lock, + Users, Search, Scale } from 'lucide-react'; import i18n from '../i18n'; import { showToast } from '../utils/toast'; @@ -30,27 +30,18 @@ import { SeekbarPreview } from '../components/WaveformSeek'; import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform'; import { useThemeStore } from '../store/themeStore'; import { useFontStore, FontId } from '../store/fontStore'; -import { useKeybindingsStore, KeyAction, formatBinding, buildInAppBinding } from '../store/keybindingsStore'; -import { useGlobalShortcutsStore, GlobalAction, buildGlobalShortcut, formatGlobalShortcut } from '../store/globalShortcutsStore'; -import { IN_APP_SHORTCUT_ACTIONS, GLOBAL_SHORTCUT_ACTIONS } from '../config/shortcutActions'; -import { useSidebarStore } from '../store/sidebarStore'; -import { useQueueToolbarStore } from '../store/queueToolbarStore'; import { effectiveLoudnessPreAnalysisAttenuationDb, } from '../utils/loudnessPreAnalysisSlider'; -import { useArtistLayoutStore } from '../store/artistLayoutStore'; -import { useHomeStore } from '../store/homeStore'; import { useDragDrop } from '../contexts/DragDropContext'; import { AddServerForm } from '../components/settings/AddServerForm'; -import { ArtistLayoutCustomizer } from '../components/settings/ArtistLayoutCustomizer'; import { BackupSection } from '../components/settings/BackupSection'; -import { HomeCustomizer } from '../components/settings/HomeCustomizer'; +import { InputTab } from '../components/settings/InputTab'; import { LoudnessLufsButtonGroup } from '../components/settings/LoudnessLufsButtonGroup'; -import { LyricsSourcesCustomizer } from '../components/settings/LyricsSourcesCustomizer'; -import { QueueToolbarCustomizer } from '../components/settings/QueueToolbarCustomizer'; +import { LyricsTab } from '../components/settings/LyricsTab'; +import { PersonalisationTab } from '../components/settings/PersonalisationTab'; import { ServerGripHandle } from '../components/settings/ServerGripHandle'; import { SETTINGS_INDEX, type Tab, matchScore, resolveTab } from '../components/settings/settingsTabs'; -import { SidebarCustomizer } from '../components/settings/SidebarCustomizer'; import { UserManagementSection } from '../components/settings/UserManagementSection'; import { CONTRIBUTORS, MAINTAINERS } from '../config/settingsCredits'; import { buildAudioDeviceSelectOptions, formatAudioDeviceLabel, sortAudioDeviceIds } from '../utils/audioDeviceLabels'; @@ -75,8 +66,6 @@ export default function Settings() { const auth = useAuthStore(); const theme = useThemeStore(); const fontStore = useFontStore(); - const kb = useKeybindingsStore(); - const gs = useGlobalShortcutsStore(); const serverId = auth.activeServerId ?? ''; const clearAllOffline = useOfflineStore(s => s.clearAll); const clearHotCacheDisk = useHotCacheStore(s => s.clearAllDisk); @@ -101,8 +90,6 @@ export default function Settings() { ), [auth.loudnessPreAnalysisAttenuationDb, auth.loudnessTargetLufs], ); - const [listeningFor, setListeningFor] = useState(null); - const [listeningForGlobal, setListeningForGlobal] = useState(null); const navigate = useNavigate(); const location = useLocation(); const routeState = location.state; @@ -1156,46 +1143,7 @@ export default function Settings() { )} {/* ── Lyrics ───────────────────────────────────────────────────────────── */} - {activeTab === 'lyrics' && ( - <> - } - > - - - - } - > - - {(['classic', 'apple'] as const).map(style => { - const key = style === 'classic' ? 'Classic' : 'Apple'; - const other = style === 'classic' ? 'apple' : 'classic'; - return ( - - - - {t(`settings.sidebarLyricsStyle${key}` as any)} - {t(`settings.sidebarLyricsStyle${key}Desc` as any)} - - - auth.setSidebarLyricsStyle(e.target.checked ? style : other)} - /> - - - - - ); - })} - - - > - )} + {activeTab === 'lyrics' && } {/* ── Integrations ─────────────────────────────────────────────────────── */} {activeTab === 'integrations' && ( @@ -1414,85 +1362,7 @@ export default function Settings() { )} {/* ── Personalisation ──────────────────────────────────────────────────── */} - {activeTab === 'personalisation' && ( - <> - } - action={ - useSidebarStore.getState().reset()} - data-tooltip={t('settings.sidebarReset')} - aria-label={t('settings.sidebarReset')} - > - - - } - > - - - - } - action={ - useArtistLayoutStore.getState().reset()} - data-tooltip={t('settings.artistLayoutReset')} - aria-label={t('settings.artistLayoutReset')} - > - - - } - > - - - - } - action={ - useHomeStore.getState().reset()} - data-tooltip={t('settings.sidebarReset')} - aria-label={t('settings.sidebarReset')} - > - - - } - > - - - - } - action={ - useQueueToolbarStore.getState().reset()} - data-tooltip={t('settings.queueToolbarReset')} - aria-label={t('settings.queueToolbarReset')} - > - - - } - > - - - > - )} + {activeTab === 'personalisation' && } {/* ── Library (legacy 'general' + 'server') ────────────────────────────── */} {activeTab === 'library' && ( @@ -2393,174 +2263,8 @@ export default function Settings() { )} {/* ── Input ────────────────────────────────────────────────────────────── */} - {activeTab === 'input' && ( - <> - } - action={ - { kb.resetToDefaults(); setListeningFor(null); }} - data-tooltip={t('settings.shortcutsReset')} - aria-label={t('settings.shortcutsReset')} - > - - - } - > - - - {IN_APP_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => { - const label = getLabel(t); - const bound = kb.bindings[action]; - const isListening = listeningFor === action; - return ( - - {label} - - { - if (isListening) { setListeningFor(null); return; } - setListeningFor(action); - const handler = (e: KeyboardEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (e.code === 'Escape') { - setListeningFor(null); - window.removeEventListener('keydown', handler, true); - return; - } - const chord = buildInAppBinding(e); - if (!chord) return; - const existing = (Object.entries(kb.bindings) as [KeyAction, string | null][]) - .find(([, c]) => c === chord)?.[0]; - if (existing && existing !== action) kb.setBinding(existing, null); - kb.setBinding(action, chord); - setListeningFor(null); - window.removeEventListener('keydown', handler, true); - }; - window.addEventListener('keydown', handler, true); - }} - className="keybind-badge" - style={{ - minWidth: 72, padding: '3px 10px', borderRadius: 'var(--radius-sm)', - fontSize: 12, fontWeight: 600, fontFamily: 'monospace', - background: isListening ? 'var(--accent)' : bound ? 'var(--bg-hover)' : 'var(--bg-card)', - color: isListening ? 'var(--ctp-base)' : bound ? 'var(--text-primary)' : 'var(--text-muted)', - border: `1px solid ${isListening ? 'var(--accent)' : 'var(--border-subtle)'}`, - cursor: 'pointer', - }} - > - {isListening ? t('settings.shortcutListening') : bound ? formatBinding(bound) : t('settings.shortcutUnbound')} - - {bound && !isListening && ( - kb.setBinding(action, null)} - style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px', lineHeight: 1 }} - data-tooltip={t('settings.shortcutClear')} - > - - - )} - - - ); - })} - - - + {activeTab === 'input' && } - } - description={t('settings.globalShortcutsNote')} - action={ - { gs.resetAll(); setListeningForGlobal(null); }} - data-tooltip={t('settings.shortcutsReset')} - aria-label={t('settings.shortcutsReset')} - > - - - } - > - - - {GLOBAL_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => { - const label = getLabel(t); - const bound = gs.shortcuts[action] ?? null; - const isListening = listeningForGlobal === action; - return ( - - {label} - - { - if (isListening) { setListeningForGlobal(null); return; } - setListeningForGlobal(action); - const handler = (e: KeyboardEvent) => { - e.preventDefault(); - e.stopPropagation(); - if (e.code === 'Escape') { - setListeningForGlobal(null); - window.removeEventListener('keydown', handler, true); - return; - } - const shortcut = buildGlobalShortcut(e); - if (shortcut) { - gs.setShortcut(action, shortcut); - setListeningForGlobal(null); - window.removeEventListener('keydown', handler, true); - } - }; - window.addEventListener('keydown', handler, true); - }} - className="keybind-badge" - style={{ - minWidth: 120, padding: '3px 10px', borderRadius: 'var(--radius-sm)', - fontSize: 12, fontWeight: 600, fontFamily: 'monospace', - background: isListening ? 'var(--accent)' : bound ? 'var(--bg-hover)' : 'var(--bg-card)', - color: isListening ? 'var(--ctp-base)' : bound ? 'var(--text-primary)' : 'var(--text-muted)', - border: `1px solid ${isListening ? 'var(--accent)' : 'var(--border-subtle)'}`, - cursor: 'pointer', - }} - > - {isListening ? t('settings.shortcutListening') : bound ? formatGlobalShortcut(bound) : t('settings.shortcutUnbound')} - - {bound && !isListening && ( - gs.setShortcut(action, null)} - style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)', padding: '2px 4px', lineHeight: 1 }} - data-tooltip={t('settings.shortcutClear')} - > - - - )} - - - ); - })} - - - - > - )} {/* ── Server ───────────────────────────────────────────────────────────── */} {activeTab === 'servers' && (