Files
MarkLite/src/renderer/components/Toolbar/Toolbar.tsx
T
thzxx ee5f35177e release: v0.6.0 — 内置解析器迁移 / Toast 全面接入 / Sqlark 深度集成
- 渲染管线迁移至 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)
2026-08-09 16:50:06 +08:00

174 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'