Files
MyNote/apps/desktop/src/renderer/App.tsx
T

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' };
}