131 lines
8.6 KiB
TypeScript
131 lines
8.6 KiB
TypeScript
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>
|
||
</>
|
||
);
|
||
}
|