mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +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 {
|
||||
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<KeyAction | null>(null);
|
||||
const [listeningForGlobal, setListeningForGlobal] = useState<GlobalAction | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const routeState = location.state;
|
||||
@@ -1156,46 +1143,7 @@ export default function Settings() {
|
||||
)}
|
||||
|
||||
{/* ── Lyrics ───────────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'lyrics' && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'lyrics' && <LyricsTab />}
|
||||
|
||||
{/* ── Integrations ─────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'integrations' && (
|
||||
@@ -1414,85 +1362,7 @@ export default function Settings() {
|
||||
)}
|
||||
|
||||
{/* ── Personalisation ──────────────────────────────────────────────────── */}
|
||||
{activeTab === 'personalisation' && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'personalisation' && <PersonalisationTab />}
|
||||
|
||||
{/* ── Library (legacy 'general' + 'server') ────────────────────────────── */}
|
||||
{activeTab === 'library' && (
|
||||
@@ -2393,174 +2263,8 @@ export default function Settings() {
|
||||
)}
|
||||
|
||||
{/* ── Input ────────────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'input' && (
|
||||
<>
|
||||
<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>
|
||||
{activeTab === 'input' && <InputTab />}
|
||||
|
||||
<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 ───────────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'servers' && (
|
||||
|
||||
Reference in New Issue
Block a user