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

94 lines
6.4 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useStore } from './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).catch(() => {}); } }, []);
// FR-S4:关窗前冲刷未保存内容
useEffect(() => {
const onBeforeUnload = () => { try { useStore.getState().saveNow(); } catch {} };
window.addEventListener('beforeunload', onBeforeUnload);
return () => window.removeEventListener('beforeunload', onBeforeUnload);
}, []);
useGlobalHotkeys({ onNewDoc: () => {}, 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')); } });
// 编辑器样式随设置生效(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: '#ddd' }}>
{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()} minimal={layout.minimal} />
<Toolbar />
{view === 'editor' && <TabBar />}
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
{layout.minimal ? (<Editor />) : (<>
{layout.sidebarVisible && (<>
<div style={{ width: layout.sidebarWidth, borderRight: '1px solid #2a2a3a', 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 />}
{view === 'editor' && layout.outlineVisible && (<>
<Resizer side="right" onDrag={d => layout.setOutlineWidth(layout.outlineWidth + d)} />
<div style={{ width: layout.outlineWidth, borderLeft: '1px solid #2a2a3a', display: 'flex', flexDirection: 'column', overflow: 'hidden', flexShrink: 0 }}>
<div style={{ display: 'flex', borderBottom: '1px solid #2a2a3a' }}>
<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 ? '#2a2a3a' : 'transparent', border: 'none', color: active ? '#fff' : '#888', cursor: 'pointer', borderBottom: active ? '2px solid #4a9eff' : '2px solid transparent' };
}