mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
refactor(settings): G.56 — extract Lyrics + Personalisation + Input tabs (#621)
Three tab-section components carved out of the Settings() default-
export body. Each owns its store hooks + local state; Settings()
now only routes via `{activeTab === 'X' && <XTab />}`.
- `LyricsTab.tsx` (~50 LOC, was ~40 inline) — wraps
`LyricsSourcesCustomizer` + sidebar lyrics style toggles. Owns
the two `useAuthStore(s => s.sidebarLyricsStyle/...)` selectors.
- `PersonalisationTab.tsx` (~95 LOC, was ~80 inline) — wraps the
four customizers (sidebar / artist layout / home / queue toolbar)
with their per-store reset buttons.
- `InputTab.tsx` (~185 LOC, was ~170 inline) — keybindings + global
shortcuts. Owns the two `listeningFor` / `listeningForGlobal`
state slots that were previously hoisted into Settings().
13 now-unused imports trimmed (4 customizer-store hooks, 6
keybinding helpers, `Music2/AudioLines` kept since the tab-button
array still uses them as icons, `LayoutGrid/Keyboard` kept for the
same reason).
Pure code-move. Settings.tsx: 3037 → 2741 LOC (−296). Phase-G
journey: 5298 → 2741 LOC (~48% reduction).
This commit is contained in:
committed by
GitHub
parent
306e56dc2b
commit
320eb97c03
@@ -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<KeyAction | null>(null);
|
||||||
|
const [listeningForGlobal, setListeningForGlobal] = useState<GlobalAction | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.inputKeybindingsTitle')}
|
||||||
|
icon={<Keyboard size={16} />}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
||||||
|
onClick={() => { kb.resetToDefaults(); setListeningFor(null); }}
|
||||||
|
data-tooltip={t('settings.shortcutsReset')}
|
||||||
|
aria-label={t('settings.shortcutsReset')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="settings-card">
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
|
{IN_APP_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => {
|
||||||
|
const label = getLabel(t);
|
||||||
|
const bound = kb.bindings[action];
|
||||||
|
const isListening = listeningFor === action;
|
||||||
|
return (
|
||||||
|
<div key={action} style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
padding: '8px 10px', borderRadius: 'var(--radius-sm)',
|
||||||
|
background: isListening ? 'var(--accent-dim)' : 'transparent',
|
||||||
|
transition: 'background 0.15s',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 13, color: 'var(--text-primary)' }}>{label}</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
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')}
|
||||||
|
</button>
|
||||||
|
{bound && !isListening && (
|
||||||
|
<button
|
||||||
|
onClick={() => 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')}
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsSubSection>
|
||||||
|
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.globalShortcutsTitle')}
|
||||||
|
icon={<Keyboard size={16} />}
|
||||||
|
description={t('settings.globalShortcutsNote')}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
||||||
|
onClick={() => { gs.resetAll(); setListeningForGlobal(null); }}
|
||||||
|
data-tooltip={t('settings.shortcutsReset')}
|
||||||
|
aria-label={t('settings.shortcutsReset')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="settings-card">
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
|
{GLOBAL_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => {
|
||||||
|
const label = getLabel(t);
|
||||||
|
const bound = gs.shortcuts[action] ?? null;
|
||||||
|
const isListening = listeningForGlobal === action;
|
||||||
|
return (
|
||||||
|
<div key={action} style={{
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||||
|
padding: '8px 10px', borderRadius: 'var(--radius-sm)',
|
||||||
|
background: isListening ? 'var(--accent-dim)' : 'transparent',
|
||||||
|
transition: 'background 0.15s',
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 13, color: 'var(--text-primary)' }}>{label}</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
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')}
|
||||||
|
</button>
|
||||||
|
{bound && !isListening && (
|
||||||
|
<button
|
||||||
|
onClick={() => 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')}
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SettingsSubSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.lyricsSourcesTitle')}
|
||||||
|
icon={<Music2 size={16} />}
|
||||||
|
>
|
||||||
|
<LyricsSourcesCustomizer />
|
||||||
|
</SettingsSubSection>
|
||||||
|
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.sidebarLyricsStyle')}
|
||||||
|
icon={<AudioLines size={16} />}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||||
|
{(['classic', 'apple'] as const).map(style => {
|
||||||
|
const key = style === 'classic' ? 'Classic' : 'Apple';
|
||||||
|
const other = style === 'classic' ? 'apple' : 'classic';
|
||||||
|
return (
|
||||||
|
<div key={style} className="settings-card">
|
||||||
|
<div className="settings-toggle-row">
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 500 }}>{t(`settings.sidebarLyricsStyle${key}` as any)}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t(`settings.sidebarLyricsStyle${key}Desc` as any)}</div>
|
||||||
|
</div>
|
||||||
|
<label className="toggle-switch" aria-label={t(`settings.sidebarLyricsStyle${key}` as any)}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={sidebarLyricsStyle === style}
|
||||||
|
onChange={e => setSidebarLyricsStyle(e.target.checked ? style : other)}
|
||||||
|
/>
|
||||||
|
<span className="toggle-track" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</SettingsSubSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.sidebarTitle')}
|
||||||
|
icon={<PanelLeft size={16} />}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
||||||
|
onClick={() => useSidebarStore.getState().reset()}
|
||||||
|
data-tooltip={t('settings.sidebarReset')}
|
||||||
|
aria-label={t('settings.sidebarReset')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SidebarCustomizer />
|
||||||
|
</SettingsSubSection>
|
||||||
|
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.artistLayoutTitle')}
|
||||||
|
icon={<Users size={16} />}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
||||||
|
onClick={() => useArtistLayoutStore.getState().reset()}
|
||||||
|
data-tooltip={t('settings.artistLayoutReset')}
|
||||||
|
aria-label={t('settings.artistLayoutReset')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ArtistLayoutCustomizer />
|
||||||
|
</SettingsSubSection>
|
||||||
|
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.homeCustomizerTitle')}
|
||||||
|
icon={<LayoutGrid size={16} />}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
||||||
|
onClick={() => useHomeStore.getState().reset()}
|
||||||
|
data-tooltip={t('settings.sidebarReset')}
|
||||||
|
aria-label={t('settings.sidebarReset')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<HomeCustomizer />
|
||||||
|
</SettingsSubSection>
|
||||||
|
|
||||||
|
<SettingsSubSection
|
||||||
|
title={t('settings.queueToolbarTitle')}
|
||||||
|
icon={<ListMusic size={16} />}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
||||||
|
onClick={() => useQueueToolbarStore.getState().reset()}
|
||||||
|
data-tooltip={t('settings.queueToolbarReset')}
|
||||||
|
aria-label={t('settings.queueToolbarReset')}
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<QueueToolbarCustomizer />
|
||||||
|
</SettingsSubSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+8
-304
@@ -6,8 +6,8 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import {
|
import {
|
||||||
Wifi, WifiOff, Globe, Music2, Sliders, LogOut, CheckCircle2, FolderOpen,
|
Wifi, WifiOff, Globe, Music2, Sliders, LogOut, CheckCircle2, FolderOpen,
|
||||||
Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, Shuffle, X, Play, Type, Keyboard, ChevronDown,
|
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,
|
RotateCcw, LayoutGrid, AppWindow, HardDrive, Download, Waves, Star, Clock, ZoomIn, Sparkles, AlertTriangle, Maximize2, AudioLines, User, Lock,
|
||||||
Users, Search, Scale, ListMusic
|
Users, Search, Scale
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { showToast } from '../utils/toast';
|
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 { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
|
||||||
import { useThemeStore } from '../store/themeStore';
|
import { useThemeStore } from '../store/themeStore';
|
||||||
import { useFontStore, FontId } from '../store/fontStore';
|
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 {
|
import {
|
||||||
effectiveLoudnessPreAnalysisAttenuationDb,
|
effectiveLoudnessPreAnalysisAttenuationDb,
|
||||||
} from '../utils/loudnessPreAnalysisSlider';
|
} from '../utils/loudnessPreAnalysisSlider';
|
||||||
import { useArtistLayoutStore } from '../store/artistLayoutStore';
|
|
||||||
import { useHomeStore } from '../store/homeStore';
|
|
||||||
import { useDragDrop } from '../contexts/DragDropContext';
|
import { useDragDrop } from '../contexts/DragDropContext';
|
||||||
import { AddServerForm } from '../components/settings/AddServerForm';
|
import { AddServerForm } from '../components/settings/AddServerForm';
|
||||||
import { ArtistLayoutCustomizer } from '../components/settings/ArtistLayoutCustomizer';
|
|
||||||
import { BackupSection } from '../components/settings/BackupSection';
|
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 { LoudnessLufsButtonGroup } from '../components/settings/LoudnessLufsButtonGroup';
|
||||||
import { LyricsSourcesCustomizer } from '../components/settings/LyricsSourcesCustomizer';
|
import { LyricsTab } from '../components/settings/LyricsTab';
|
||||||
import { QueueToolbarCustomizer } from '../components/settings/QueueToolbarCustomizer';
|
import { PersonalisationTab } from '../components/settings/PersonalisationTab';
|
||||||
import { ServerGripHandle } from '../components/settings/ServerGripHandle';
|
import { ServerGripHandle } from '../components/settings/ServerGripHandle';
|
||||||
import { SETTINGS_INDEX, type Tab, matchScore, resolveTab } from '../components/settings/settingsTabs';
|
import { SETTINGS_INDEX, type Tab, matchScore, resolveTab } from '../components/settings/settingsTabs';
|
||||||
import { SidebarCustomizer } from '../components/settings/SidebarCustomizer';
|
|
||||||
import { UserManagementSection } from '../components/settings/UserManagementSection';
|
import { UserManagementSection } from '../components/settings/UserManagementSection';
|
||||||
import { CONTRIBUTORS, MAINTAINERS } from '../config/settingsCredits';
|
import { CONTRIBUTORS, MAINTAINERS } from '../config/settingsCredits';
|
||||||
import { buildAudioDeviceSelectOptions, formatAudioDeviceLabel, sortAudioDeviceIds } from '../utils/audioDeviceLabels';
|
import { buildAudioDeviceSelectOptions, formatAudioDeviceLabel, sortAudioDeviceIds } from '../utils/audioDeviceLabels';
|
||||||
@@ -75,8 +66,6 @@ export default function Settings() {
|
|||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const theme = useThemeStore();
|
const theme = useThemeStore();
|
||||||
const fontStore = useFontStore();
|
const fontStore = useFontStore();
|
||||||
const kb = useKeybindingsStore();
|
|
||||||
const gs = useGlobalShortcutsStore();
|
|
||||||
const serverId = auth.activeServerId ?? '';
|
const serverId = auth.activeServerId ?? '';
|
||||||
const clearAllOffline = useOfflineStore(s => s.clearAll);
|
const clearAllOffline = useOfflineStore(s => s.clearAll);
|
||||||
const clearHotCacheDisk = useHotCacheStore(s => s.clearAllDisk);
|
const clearHotCacheDisk = useHotCacheStore(s => s.clearAllDisk);
|
||||||
@@ -101,8 +90,6 @@ export default function Settings() {
|
|||||||
),
|
),
|
||||||
[auth.loudnessPreAnalysisAttenuationDb, auth.loudnessTargetLufs],
|
[auth.loudnessPreAnalysisAttenuationDb, auth.loudnessTargetLufs],
|
||||||
);
|
);
|
||||||
const [listeningFor, setListeningFor] = useState<KeyAction | null>(null);
|
|
||||||
const [listeningForGlobal, setListeningForGlobal] = useState<GlobalAction | null>(null);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const routeState = location.state;
|
const routeState = location.state;
|
||||||
@@ -1156,46 +1143,7 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Lyrics ───────────────────────────────────────────────────────────── */}
|
{/* ── Lyrics ───────────────────────────────────────────────────────────── */}
|
||||||
{activeTab === 'lyrics' && (
|
{activeTab === 'lyrics' && <LyricsTab />}
|
||||||
<>
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.lyricsSourcesTitle')}
|
|
||||||
icon={<Music2 size={16} />}
|
|
||||||
>
|
|
||||||
<LyricsSourcesCustomizer />
|
|
||||||
</SettingsSubSection>
|
|
||||||
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.sidebarLyricsStyle')}
|
|
||||||
icon={<AudioLines size={16} />}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
|
||||||
{(['classic', 'apple'] as const).map(style => {
|
|
||||||
const key = style === 'classic' ? 'Classic' : 'Apple';
|
|
||||||
const other = style === 'classic' ? 'apple' : 'classic';
|
|
||||||
return (
|
|
||||||
<div key={style} className="settings-card">
|
|
||||||
<div className="settings-toggle-row">
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: 500 }}>{t(`settings.sidebarLyricsStyle${key}` as any)}</div>
|
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t(`settings.sidebarLyricsStyle${key}Desc` as any)}</div>
|
|
||||||
</div>
|
|
||||||
<label className="toggle-switch" aria-label={t(`settings.sidebarLyricsStyle${key}` as any)}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={auth.sidebarLyricsStyle === style}
|
|
||||||
onChange={e => auth.setSidebarLyricsStyle(e.target.checked ? style : other)}
|
|
||||||
/>
|
|
||||||
<span className="toggle-track" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</SettingsSubSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Integrations ─────────────────────────────────────────────────────── */}
|
{/* ── Integrations ─────────────────────────────────────────────────────── */}
|
||||||
{activeTab === 'integrations' && (
|
{activeTab === 'integrations' && (
|
||||||
@@ -1414,85 +1362,7 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Personalisation ──────────────────────────────────────────────────── */}
|
{/* ── Personalisation ──────────────────────────────────────────────────── */}
|
||||||
{activeTab === 'personalisation' && (
|
{activeTab === 'personalisation' && <PersonalisationTab />}
|
||||||
<>
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.sidebarTitle')}
|
|
||||||
icon={<PanelLeft size={16} />}
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
|
||||||
onClick={() => useSidebarStore.getState().reset()}
|
|
||||||
data-tooltip={t('settings.sidebarReset')}
|
|
||||||
aria-label={t('settings.sidebarReset')}
|
|
||||||
>
|
|
||||||
<RotateCcw size={14} />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SidebarCustomizer />
|
|
||||||
</SettingsSubSection>
|
|
||||||
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.artistLayoutTitle')}
|
|
||||||
icon={<Users size={16} />}
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
|
||||||
onClick={() => useArtistLayoutStore.getState().reset()}
|
|
||||||
data-tooltip={t('settings.artistLayoutReset')}
|
|
||||||
aria-label={t('settings.artistLayoutReset')}
|
|
||||||
>
|
|
||||||
<RotateCcw size={14} />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ArtistLayoutCustomizer />
|
|
||||||
</SettingsSubSection>
|
|
||||||
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.homeCustomizerTitle')}
|
|
||||||
icon={<LayoutGrid size={16} />}
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
|
||||||
onClick={() => useHomeStore.getState().reset()}
|
|
||||||
data-tooltip={t('settings.sidebarReset')}
|
|
||||||
aria-label={t('settings.sidebarReset')}
|
|
||||||
>
|
|
||||||
<RotateCcw size={14} />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<HomeCustomizer />
|
|
||||||
</SettingsSubSection>
|
|
||||||
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.queueToolbarTitle')}
|
|
||||||
icon={<ListMusic size={16} />}
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
|
||||||
onClick={() => useQueueToolbarStore.getState().reset()}
|
|
||||||
data-tooltip={t('settings.queueToolbarReset')}
|
|
||||||
aria-label={t('settings.queueToolbarReset')}
|
|
||||||
>
|
|
||||||
<RotateCcw size={14} />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<QueueToolbarCustomizer />
|
|
||||||
</SettingsSubSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Library (legacy 'general' + 'server') ────────────────────────────── */}
|
{/* ── Library (legacy 'general' + 'server') ────────────────────────────── */}
|
||||||
{activeTab === 'library' && (
|
{activeTab === 'library' && (
|
||||||
@@ -2393,174 +2263,8 @@ export default function Settings() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Input ────────────────────────────────────────────────────────────── */}
|
{/* ── Input ────────────────────────────────────────────────────────────── */}
|
||||||
{activeTab === 'input' && (
|
{activeTab === 'input' && <InputTab />}
|
||||||
<>
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.inputKeybindingsTitle')}
|
|
||||||
icon={<Keyboard size={16} />}
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
|
||||||
onClick={() => { kb.resetToDefaults(); setListeningFor(null); }}
|
|
||||||
data-tooltip={t('settings.shortcutsReset')}
|
|
||||||
aria-label={t('settings.shortcutsReset')}
|
|
||||||
>
|
|
||||||
<RotateCcw size={14} />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="settings-card">
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
||||||
{IN_APP_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => {
|
|
||||||
const label = getLabel(t);
|
|
||||||
const bound = kb.bindings[action];
|
|
||||||
const isListening = listeningFor === action;
|
|
||||||
return (
|
|
||||||
<div key={action} style={{
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
||||||
padding: '8px 10px', borderRadius: 'var(--radius-sm)',
|
|
||||||
background: isListening ? 'var(--accent-dim)' : 'transparent',
|
|
||||||
transition: 'background 0.15s',
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-primary)' }}>{label}</span>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
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')}
|
|
||||||
</button>
|
|
||||||
{bound && !isListening && (
|
|
||||||
<button
|
|
||||||
onClick={() => 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')}
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsSubSection>
|
|
||||||
|
|
||||||
<SettingsSubSection
|
|
||||||
title={t('settings.globalShortcutsTitle')}
|
|
||||||
icon={<Keyboard size={16} />}
|
|
||||||
description={t('settings.globalShortcutsNote')}
|
|
||||||
action={
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
style={{ fontSize: 12, color: 'var(--text-muted)', padding: '2px 6px' }}
|
|
||||||
onClick={() => { gs.resetAll(); setListeningForGlobal(null); }}
|
|
||||||
data-tooltip={t('settings.shortcutsReset')}
|
|
||||||
aria-label={t('settings.shortcutsReset')}
|
|
||||||
>
|
|
||||||
<RotateCcw size={14} />
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="settings-card">
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
|
||||||
{GLOBAL_SHORTCUT_ACTIONS.map(({ id: action, getLabel }) => {
|
|
||||||
const label = getLabel(t);
|
|
||||||
const bound = gs.shortcuts[action] ?? null;
|
|
||||||
const isListening = listeningForGlobal === action;
|
|
||||||
return (
|
|
||||||
<div key={action} style={{
|
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
||||||
padding: '8px 10px', borderRadius: 'var(--radius-sm)',
|
|
||||||
background: isListening ? 'var(--accent-dim)' : 'transparent',
|
|
||||||
transition: 'background 0.15s',
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-primary)' }}>{label}</span>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
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')}
|
|
||||||
</button>
|
|
||||||
{bound && !isListening && (
|
|
||||||
<button
|
|
||||||
onClick={() => 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')}
|
|
||||||
>
|
|
||||||
<X size={12} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsSubSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Server ───────────────────────────────────────────────────────────── */}
|
{/* ── Server ───────────────────────────────────────────────────────────── */}
|
||||||
{activeTab === 'servers' && (
|
{activeTab === 'servers' && (
|
||||||
|
|||||||
Reference in New Issue
Block a user