120 lines
7.6 KiB
TypeScript
120 lines
7.6 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useStore } from './store';
|
|
import { invoke } from './ipc-client';
|
|
import { IPC } from '@hunter/shared';
|
|
import { useDialog } from './features/dialog/dialog-store';
|
|
import { useEditorShared } from './features/editor/editor-shared-store';
|
|
import { FileTree } from './features/file-tree/FileTree';
|
|
import { Editor } from './features/editor/Editor';
|
|
import { Toolbar } from './features/toolbar/Toolbar';
|
|
import { TitleBar } from './features/shell/TitleBar';
|
|
import { InputDialog } from './features/dialog/InputDialog';
|
|
import { NotifyLayer } from './features/dialog/NotifyLayer';
|
|
import { TabBar } from './features/tabs/TabBar';
|
|
import { useTabs } from './features/tabs/tab-store';
|
|
import { GraphView } from './features/graph/GraphView';
|
|
import { SettingsPanel } from './features/settings/SettingsPanel';
|
|
import { AppearancePanel } from './features/appearance/AppearancePanel';
|
|
import { useAppearance } from './features/appearance/appearance-store';
|
|
import { CheatSheet } from './features/keymap/CheatSheet';
|
|
import { useGlobalHotkeys } from './features/keymap/use-hotkeys';
|
|
import { useKeymap } from './features/keymap/keymap-service';
|
|
import { useLayout } from './features/shell/layout-store';
|
|
import { Resizer } from './features/shell/Resizer';
|
|
import { EffectLayer } from './features/effects/EffectLayer';
|
|
import { useSettings } from './features/settings/settings-store';
|
|
import { SearchBox } from './features/search/SearchBox';
|
|
import { TagFilter } from './features/search/TagFilter';
|
|
import { OutlinePanel } from './features/outline/OutlinePanel';
|
|
import { DocInfoPanel } from './features/doc-info/DocInfoPanel';
|
|
|
|
export function App() {
|
|
const { loadVault, loadTree, openDoc } = useStore();
|
|
const { restore, tabs, activeId } = useTabs();
|
|
const [rightTab, setRightTab] = useState<'outline' | 'info'>('outline');
|
|
const [view, setView] = useState<'editor' | 'graph'>('editor');
|
|
const [showSettings, setShowSettings] = useState(false);
|
|
const [showCheat, setShowCheat] = useState(false);
|
|
const [showAppearance, setShowAppearance] = useState(false);
|
|
const appearance = useAppearance();
|
|
const settings = useSettings();
|
|
const layout = useLayout();
|
|
|
|
useEffect(() => { loadVault(); loadTree(); restore(); appearance.load(); useKeymap.getState().load(); useSettings.getState().loadGpu(); }, []);
|
|
useEffect(() => { if (activeId && tabs.length > 0) { const t = tabs.find(x => x.id === activeId); if (t) openDoc(t.path).then(() => {
|
|
// FR-T5:恢复标签页保存的滚动位置
|
|
if (t.scrollTop > 0) { setTimeout(() => { const ed = document.querySelector('.ProseMirror'); const c = ed?.closest('[class*="editor"]') as HTMLElement | null; if (c) c.scrollTop = t.scrollTop; }, 100); }
|
|
}).catch(() => {}); } }, [activeId]);
|
|
// FR-S4:关窗前冲刷未保存内容
|
|
useEffect(() => {
|
|
const onBeforeUnload = () => { try { useStore.getState().saveNow(); } catch {} };
|
|
window.addEventListener('beforeunload', onBeforeUnload);
|
|
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
|
}, []);
|
|
useGlobalHotkeys({
|
|
onNewDoc: async () => {
|
|
const { showInput } = useDialog.getState();
|
|
const name = await showInput('新建文档', '输入文档标题…');
|
|
if (!name) return;
|
|
const { path } = await invoke(IPC.fileCreate, { basePath: '', name, isFolder: false });
|
|
await useStore.getState().loadTree();
|
|
await useStore.getState().openDoc(path);
|
|
},
|
|
onSave: () => useStore.getState().saveNow(),
|
|
onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); },
|
|
onToggleOutline: () => layout.toggleOutline(),
|
|
onCheat: () => setShowCheat(true),
|
|
onSettings: () => setShowSettings(true),
|
|
onFind: () => { window.dispatchEvent(new CustomEvent('dsh-toggle-find')); },
|
|
onClearFormat: () => {
|
|
const ed = useEditorShared.getState().editor;
|
|
ed?.chain().focus().clearNodes().unsetAllMarks().run();
|
|
},
|
|
});
|
|
|
|
// 编辑器样式随设置生效(FR-编辑器设置真实生效)
|
|
useEffect(() => { document.documentElement.style.setProperty('--editor-font-size', settings.fontSize + 'px'); document.documentElement.style.setProperty('--editor-font-family', settings.fontFamily); document.documentElement.style.setProperty('--editor-white-space', settings.wordWrap ? 'pre-wrap' : 'pre'); }, [settings.fontSize, settings.fontFamily, settings.wordWrap]);
|
|
return (
|
|
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', height: '100vh', background: 'var(--bg-primary, #1a1a1a)', color: 'var(--text-primary)' }}>
|
|
{appearance.bgImage && <div style={{ position: 'absolute', inset: 0, backgroundImage: 'url(' + appearance.bgImage + ')', backgroundSize: 'cover', backgroundPosition: 'center', filter: settings.perfMode ? 'none' : 'blur(var(--blur-px,0px))', opacity: 0.45, zIndex: 0, pointerEvents: 'none' }} />}
|
|
<div style={{ position: 'relative', zIndex: 1, display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
|
<TitleBar view={view} onToggleView={() => setView(v => v === 'editor' ? 'graph' : 'editor')} onSettings={() => setShowSettings(true)} onAppearance={() => setShowAppearance(true)} onToggleSidebar={() => layout.toggleSidebar()} onToggleMinimal={() => layout.toggleMinimal()} onToggleTabbar={() => layout.toggleTabbar()} minimal={layout.minimal} tabbarVisible={layout.tabbarVisible} />
|
|
<Toolbar />
|
|
{view === 'editor' && layout.tabbarVisible && !layout.minimal && <TabBar />}
|
|
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
|
{layout.minimal ? (<Editor />) : (<>
|
|
{layout.sidebarVisible && (<>
|
|
<div style={{ width: layout.sidebarWidth, borderRight: '1px solid var(--control-border)', display: 'flex', flexDirection: 'column', overflow: 'hidden', flexShrink: 0 }}>
|
|
<SearchBox />
|
|
<div style={{ flex: 1, overflow: 'auto' }}><FileTree /></div>
|
|
<TagFilter />
|
|
</div>
|
|
<Resizer side="left" onDrag={d => layout.setSidebarWidth(layout.sidebarWidth + d)} />
|
|
</>)}
|
|
{view === 'editor' ? <Editor /> : <GraphView onSwitchToEditor={() => setView('editor')} />}
|
|
{view === 'editor' && layout.outlineVisible && (<>
|
|
<Resizer side="right" onDrag={d => layout.setOutlineWidth(layout.outlineWidth + d)} />
|
|
<div style={{ width: layout.outlineWidth, borderLeft: '1px solid var(--control-border)', display: 'flex', flexDirection: 'column', overflow: 'hidden', flexShrink: 0 }}>
|
|
<div style={{ display: 'flex', borderBottom: '1px solid var(--control-border)' }}>
|
|
<button style={tabBtn(rightTab==='outline')} onClick={() => setRightTab('outline')}>大纲</button>
|
|
<button style={tabBtn(rightTab==='info')} onClick={() => setRightTab('info')}>信息</button>
|
|
</div>
|
|
<div style={{ flex: 1, overflow: 'auto' }}>{rightTab === 'outline' ? <OutlinePanel /> : <DocInfoPanel />}</div>
|
|
</div>
|
|
</>)}
|
|
</>)}
|
|
</div>
|
|
<EffectLayer />
|
|
<InputDialog />
|
|
<NotifyLayer />
|
|
{showSettings && <SettingsPanel onClose={() => setShowSettings(false)} />}
|
|
{showAppearance && <AppearancePanel onClose={() => setShowAppearance(false)} />}
|
|
{showCheat && <CheatSheet onClose={() => setShowCheat(false)} />}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function tabBtn(active: boolean): React.CSSProperties {
|
|
return { flex: 1, padding: '6px', background: active ? 'var(--control-border)' : 'transparent', border: 'none', color: active ? 'var(--text-primary)' : 'var(--text-tertiary)', cursor: 'pointer', borderBottom: active ? '2px solid var(--accent)' : '2px solid transparent' };
|
|
} |