- 渲染管线迁移至 MetonaEditor 内置解析器,移除 unified/rehype 全家桶(9 个依赖) - 修复相对路径图片修复的目录前缀碰撞与路径解析 bug - ConfirmDialog/useConfirm/LoadingSpinner/useDocStats 移除,改用 MeToast.confirm/loading/promise - 新增状态栏(字数/行数/阅读时间/光标位置)、Zen 模式、数据备份导出导入 - Sqlark: 版本化迁移(addMigration/migrateTo)、subscribe 表变更、备份 exportAll/importTable - 数据库损坏自愈:异常退出残留残缺 SSTable 导致打开失败时自动重建 - 大纲导航改用 scrollToLine 官方 API - 修复 rollup 平台包互删(postinstall 自动补齐)+ 集成测试(fake-indexeddb)
174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
import React, { useCallback } from 'react'
|
||
import { FolderOpen, Save, Moon, Sun, Info, Download, Upload } from '../Icons'
|
||
import { getMetonaEditor, useEditorStore } from '../../stores/editorStore'
|
||
import { backupRepository } from '../../db/backupRepository'
|
||
import { showToast } from '../../lib/toast'
|
||
import { logError } from '../../lib/errorHandler'
|
||
import type { ThemeMode } from '../../types/settings'
|
||
|
||
interface ToolbarProps {
|
||
onOpen: () => void
|
||
onSave: () => void
|
||
themeMode: ThemeMode
|
||
onCycleTheme: () => void
|
||
onShowAbout: () => void
|
||
isAutoSaving: boolean
|
||
autoSaveEnabled: boolean
|
||
onToggleAutoSave: () => void
|
||
}
|
||
|
||
const THEME_LABELS: Record<ThemeMode, string> = {
|
||
light: '亮色',
|
||
dark: '暗色',
|
||
warm: '暖色',
|
||
}
|
||
|
||
/**
|
||
* 应用顶层工具栏 — 文件操作、自动保存、主题循环、数据备份、关于。
|
||
* 编辑器格式化和模式切换由 MetonaEditor 内置工具栏处理。
|
||
*/
|
||
export const Toolbar = React.memo(function Toolbar({
|
||
onOpen,
|
||
onSave,
|
||
themeMode,
|
||
onCycleTheme,
|
||
onShowAbout,
|
||
isAutoSaving,
|
||
autoSaveEnabled,
|
||
onToggleAutoSave,
|
||
}: ToolbarProps) {
|
||
const nextLabel = THEME_LABELS[themeMode] ?? '主题'
|
||
const zenMode = useEditorStore(s => s.zenMode)
|
||
|
||
// v0.6.0: Zen 专注模式切换(MetonaEditor 内置能力)
|
||
const handleToggleZen = useCallback(() => {
|
||
const editor = getMetonaEditor()
|
||
if (!editor) return
|
||
editor.toggleZen()
|
||
}, [])
|
||
|
||
// v0.6.0: 数据备份导出(sqlark exportAll → JSON 文件)
|
||
const handleExport = useCallback(async () => {
|
||
if (!window.electronAPI) return
|
||
const data = await backupRepository.exportAll()
|
||
if (!data) return
|
||
const result = await window.electronAPI.exportData(JSON.stringify(data, null, 2))
|
||
if (result.success) {
|
||
showToast('备份已导出', 'success')
|
||
} else if (!result.canceled) {
|
||
showToast(`导出失败: ${result.error ?? '未知错误'}`, 'error')
|
||
}
|
||
}, [])
|
||
|
||
// v0.6.0: 数据备份导入(JSON 文件 → sqlark importTable)
|
||
const handleImport = useCallback(async () => {
|
||
if (!window.electronAPI) return
|
||
const result = await window.electronAPI.importData()
|
||
if (!result.success) return
|
||
if (result.canceled || !result.content) return
|
||
try {
|
||
const data = JSON.parse(result.content) as Record<string, Record<string, unknown>[]>
|
||
const ok = await backupRepository.importAll(data)
|
||
if (ok) {
|
||
// 恢复后刷新页面 — 所有 store 从新数据库重新加载(loadFromDB 有 _loaded 守卫,
|
||
// 且 settings/sidebar 也只在初始化时读取,直接重载页面最可靠)
|
||
showToast('备份已恢复', 'success')
|
||
setTimeout(() => window.location.reload(), 800)
|
||
} else {
|
||
showToast('恢复备份失败', 'error')
|
||
}
|
||
} catch (error) {
|
||
logError('解析备份文件失败', error)
|
||
showToast('备份文件格式无效', 'error')
|
||
}
|
||
}, [])
|
||
|
||
return (
|
||
<div id="toolbar" role="toolbar" aria-label="工具栏">
|
||
<div className="toolbar-left" role="group" aria-label="文件操作">
|
||
<button
|
||
className="toolbar-btn"
|
||
onClick={onOpen}
|
||
title="打开文件 (Ctrl+O)"
|
||
aria-label="打开文件"
|
||
>
|
||
<FolderOpen size={18} />
|
||
<span>打开</span>
|
||
</button>
|
||
<button
|
||
className="toolbar-btn"
|
||
onClick={onSave}
|
||
title="保存文件 (Ctrl+S)"
|
||
aria-label="保存文件"
|
||
>
|
||
<Save size={18} />
|
||
<span>保存</span>
|
||
</button>
|
||
<div className="toolbar-divider" role="separator" />
|
||
<button
|
||
className={`toolbar-btn toolbar-autosave${isAutoSaving ? ' saving' : ''}`}
|
||
onClick={onToggleAutoSave}
|
||
title={
|
||
isAutoSaving
|
||
? '正在自动保存...'
|
||
: autoSaveEnabled
|
||
? '自动保存已开启 — 点击关闭'
|
||
: '自动保存已关闭 — 点击开启'
|
||
}
|
||
aria-label={
|
||
isAutoSaving ? '正在自动保存' : autoSaveEnabled ? '关闭自动保存' : '开启自动保存'
|
||
}
|
||
>
|
||
<span>{isAutoSaving ? '保存中...' : autoSaveEnabled ? '自动' : '手动'}</span>
|
||
</button>
|
||
</div>
|
||
<div className="toolbar-right" role="group" aria-label="设置">
|
||
<button
|
||
className={`toolbar-btn${zenMode ? ' active' : ''}`}
|
||
onClick={handleToggleZen}
|
||
title={zenMode ? '退出专注模式' : '专注模式 — 隐藏编辑器工具栏'}
|
||
aria-label={zenMode ? '退出专注模式' : '进入专注模式'}
|
||
aria-pressed={zenMode}
|
||
>
|
||
<span>🧘 {zenMode ? '专注中' : '专注'}</span>
|
||
</button>
|
||
<button
|
||
className="toolbar-btn"
|
||
onClick={handleExport}
|
||
title="导出数据备份"
|
||
aria-label="导出数据备份"
|
||
>
|
||
<Download size={16} />
|
||
</button>
|
||
<button
|
||
className="toolbar-btn"
|
||
onClick={handleImport}
|
||
title="导入数据备份"
|
||
aria-label="导入数据备份"
|
||
>
|
||
<Upload size={16} />
|
||
</button>
|
||
<button
|
||
className="toolbar-btn"
|
||
onClick={onCycleTheme}
|
||
title={`当前 ${nextLabel} — 点击切换`}
|
||
aria-label={`当前${nextLabel}主题,点击切换`}
|
||
>
|
||
{themeMode === 'dark' ? <Moon size={18} /> : <Sun size={18} />}
|
||
<span style={{ fontSize: 12, marginLeft: 2 }}>{nextLabel}</span>
|
||
</button>
|
||
<button
|
||
className="toolbar-btn"
|
||
onClick={onShowAbout}
|
||
title="关于"
|
||
aria-label="关于 MarkLite"
|
||
>
|
||
<Info size={18} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
})
|
||
|
||
Toolbar.displayName = 'Toolbar'
|