Files
MyNote/apps/desktop/src/renderer/features/settings/SettingsPanel.tsx
T

131 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { useSettings } from './settings-store';
import { useKeymap } from '../keymap/keymap-service';
import { useStore } from '../../store';
import { useTabs } from '../tabs/tab-store';
import { invoke } from '../../ipc-client';
import { IPC } from '@hunter/shared';
const pageBtn: React.CSSProperties = { padding: '8px 14px', border: 'none', background: 'transparent', color: '#888', cursor: 'pointer', borderBottom: '2px solid transparent', fontSize: 13 };
const row: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 0', borderBottom: '1px solid #2a2a3a' };
const lbl: React.CSSProperties = { fontSize: 13, color: '#ccc' };
const toggle = (on: boolean): React.CSSProperties => ({ width: 36, height: 20, borderRadius: 10, background: on ? '#4a9eff' : '#444', position: 'relative', cursor: 'pointer', border: 'none', transition: 'background 0.2s' });
export function SettingsPanel({ onClose }: { onClose: () => void }) {
const s = useSettings();
const [page, setPage] = useState<'keymap' | 'editor' | 'path' | 'other'>('editor');
const { bindings, setCombo, resetAll, save: saveKeymap, conflicts } = useKeymap();
const [recId, setRecId] = useState<string | null>(null);
useEffect(() => {
if (!recId) return;
const onKey = (e: KeyboardEvent) => {
e.preventDefault();
if (e.key === 'Escape') { setRecId(null); return; }
if (!e.ctrlKey && !e.metaKey && !e.altKey) return;
const parts: string[] = [];
if (e.ctrlKey) parts.push('ctrl');
if (e.shiftKey) parts.push('shift');
if (e.altKey) parts.push('alt');
if (e.metaKey) parts.push('meta');
const k = e.key.toLowerCase();
if (k.length > 1) return;
parts.push(k);
const combo = parts.join('+');
setCombo(recId, combo);
void saveKeymap();
setRecId(null);
};
window.addEventListener('keydown', onKey, true);
return () => window.removeEventListener('keydown', onKey, true);
}, [recId, setCombo, saveKeymap]);
return (
<div onClick={e => { if (e.target === e.currentTarget) onClose(); }} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1500 }}>
<div style={{ width: 520, maxHeight: '80vh', overflow: 'auto', background: '#1e1e2a', border: '1px solid #3a3a4a', borderRadius: 10, padding: 20, boxShadow: '0 8px 32px rgba(0,0,0,0.5)' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<strong style={{ fontSize: 15 }}>设置</strong>
<button onClick={onClose} style={{ background: 'none', border: 'none', color: '#888', cursor: 'pointer', fontSize: 16 }}>✕</button>
</div>
<div style={{ display: 'flex', borderBottom: '1px solid #2a2a3a', marginBottom: 12 }}>
{(['editor','keymap','path','other'] as const).map(p => (
<button key={p} onClick={() => setPage(p)} style={{ ...pageBtn, color: page===p ? '#fff' : '#888', borderBottom: page===p ? '2px solid #4a9eff' : '2px solid transparent' }}>{({editor:'编辑器',keymap:'快捷键',path:'文件路径',other:'其他'} as any)[p]}</button>
))}
</div>
{page === 'editor' && <>
<div style={row}><span style={lbl}>字号</span><input type="range" min={12} max={20} value={s.fontSize} onChange={e => s.set('fontSize', +e.target.value)} /> <span style={{ width: 30, textAlign: 'right' }}>{s.fontSize}px</span></div>
<div style={row}><span style={lbl}>字体</span><select value={s.fontFamily} onChange={e => s.set('fontFamily', e.target.value)} style={sel}>{['Consolas, monospace','微软雅黑','宋体','楷体','仿宋','Source Code Pro'].map(f=><option key={f} value={f}>{f.split(',')[0]}</option>)}</select></div>
<div style={row}><span style={lbl}>自动保存间隔</span><select value={s.autoSaveInterval} onChange={e => s.set('autoSaveInterval', +e.target.value)} style={sel}>{[1,3,5,10,15,30].map(n=><option key={n} value={n}>{n} 秒</option>)}</select></div>
<div style={row}><span style={lbl}>代码块显示行号</span><button onClick={() => s.set('showLineNumbers', !s.showLineNumbers)} style={toggle(s.showLineNumbers)}><span style={dot(s.showLineNumbers)} /></button></div>
<div style={row}><span style={lbl}>自动换行</span><button onClick={() => s.set('wordWrap', !s.wordWrap)} style={toggle(s.wordWrap)}><span style={dot(s.wordWrap)} /></button></div>
</>}
{page === 'other' && <div style={row}><span style={lbl}>删除前确认</span><button onClick={() => s.set('confirmDelete', !s.confirmDelete)} style={toggle(s.confirmDelete)}><span style={dot(s.confirmDelete)} /></button></div>}
{page === 'path' && <PathPage />}
{page === 'keymap' && <>
<div style={{ ...lbl, opacity: 0.6, marginBottom: 8 }}>点击「录入」后按下新组合键(需含 Ctrl/Meta/Alt);Esc 取消</div>
{bindings.map(b => {
const cf = conflicts();
const inConflict = Object.values(cf).some(ids => ids.includes(b.id));
return (
<div key={b.id} style={row}>
<span style={lbl}>{b.label}</span>
<span style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{inConflict && <span style={{ color: '#e88', fontSize: 11 }}>冲突</span>}
<code style={{ background: '#15151f', padding: '1px 6px', borderRadius: 3, color: recId === b.id ? '#fc4' : '#9cf', minWidth: 90, textAlign: 'center' }}>{recId === b.id ? '按下组合键…' : b.combo}</code>
<button onClick={() => setRecId(b.id)} style={{ ...sel, cursor: 'pointer' }}>录入</button>
</span>
</div>
);
})}
<button onClick={async () => { resetAll(); await saveKeymap(); }} style={{ marginTop: 12, ...sel, cursor: 'pointer' }}>重置默认</button>
</>}
<div style={{ marginTop: 16, fontSize: 12, opacity: 0.5 }}>设置已自动保存</div>
</div>
</div>
);
}
const sel: React.CSSProperties = { background: '#15151f', border: '1px solid #3a3a4a', color: '#ddd', borderRadius: 4, padding: '4px 8px', fontSize: 13 };
function dot(on: boolean): React.CSSProperties { return { position: 'absolute', top: 2, left: on ? 18 : 2, width: 16, height: 16, borderRadius: '50%', background: '#fff', transition: 'left 0.2s' }; }
// FR-V5: 默认库路径设置页——查看当前库、浏览选择、立即切换
function PathPage() {
const { vault, loadVault, loadTree } = useStore();
const [val, setVal] = useState(vault?.currentRoot ?? '');
const [msg, setMsg] = useState('');
const browse = async () => { const p = await invoke(IPC.vaultBrowse, undefined as never); if (p) setVal(p); };
const apply = async () => {
if (!val.trim()) { setMsg('请填写或浏览选择库路径'); return; }
try {
setMsg('切换中…');
await useStore.getState().saveNow(); // 先冲刷未保存到旧库
await invoke(IPC.vaultSwitch, { root: val.trim() });
useTabs.getState().closeAll(); // 清空标签页(FR-V6)
useStore.getState().resetEditor(); // 重置编辑区
await loadVault();
await loadTree();
setMsg('已切换到:' + val.trim());
} catch (e: any) { setMsg('切换失败:' + (e?.message ?? String(e))); }
};
return (
<>
<div style={{ ...row, alignItems: 'flex-start' }}>
<span style={{ ...lbl, paddingTop: 6 }}>当前库路径</span>
<span style={{ fontSize: 12, color: '#9cf', maxWidth: 320, wordBreak: 'break-all', textAlign: 'right' }}>{vault?.currentRoot ?? '—'}</span>
</div>
<div style={{ ...row, alignItems: 'flex-start' }}>
<span style={{ ...lbl, paddingTop: 6 }}>新库路径</span>
<span style={{ display: 'flex', gap: 6, flex: 1, justifyContent: 'flex-end' }}>
<input value={val} onChange={e => setVal(e.target.value)} placeholder="填写或浏览选择目录…" style={{ ...sel, flex: 1, maxWidth: 220 }} />
<button onClick={browse} style={{ ...sel, cursor: 'pointer' }}>浏览…</button>
</span>
</div>
<div style={{ ...row }}>
<span style={lbl} />
<span style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
{msg && <span style={{ fontSize: 12, color: msg.startsWith('切换失败') ? '#f88' : (msg.startsWith('已切换') ? '#8c8' : '#9cf') }}>{msg}</span>}
<button onClick={apply} style={{ ...sel, cursor: 'pointer', background: '#2a4a6a', color: '#9cf' }}>立即切换</button>
</span>
</div>
</>
);
}