返回 oh-my-ppt
HtmlEditorToolbar.tsx
根目录 / src / renderer / src / components / html-editor / HtmlEditorToolbar.tsx
1 import { useCallback, type ReactElement } from 'react'
2 import { useNavigate } from 'react-router-dom'
3 import {
4 Download,
5 ExternalLink,
6 FileSearch,
7 History,
8 Home,
9 Redo2,
10 RotateCcw,
11 Save,
12 Undo2
13 } from 'lucide-react'
14 import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip'
15 import { useT } from '../../i18n'
16 import { ipc } from '../../lib/ipc'
17 import { useHtmlEditorStore } from '../../store/htmlEditorStore'
18 import { useHtmlEditStore } from '../../store/htmlEditStore'
19 import { useHtmlEditHistoryStore } from '../../store/htmlEditHistoryStore'
20 import { useHtmlEditorAiStore } from '../../store/htmlEditorAiStore'
21 import { useHtmlEditorUiStore } from '../../store/htmlEditorUiStore'
22 import { useToastStore } from '../../store/toastStore'
23
24 const iconBtnClass =
25 'app-no-drag rounded-md p-1.5 text-[#5d6b4d] transition-colors hover:bg-[#ece5d6] disabled:pointer-events-none disabled:opacity-40'
26
27 /** HTML 编辑器顶部工具条(全图标,tooltip 标注)。 */
28 export function HtmlEditorToolbar({ onOpenHistory }: { onOpenHistory: () => void }): ReactElement {
29 const t = useT()
30 const navigate = useNavigate()
31 const isMac = window.electron?.process?.platform === 'darwin'
32
33 const docId = useHtmlEditorStore((s) => s.docId)
34 const title = useHtmlEditorStore((s) => s.title)
35 const sourcePath = useHtmlEditorStore((s) => s.sourcePath)
36 const exporting = useHtmlEditorStore((s) => s.exporting)
37 const isSavingEdits = useHtmlEditStore((s) => s.isSavingEdits)
38 const canUndo = useHtmlEditHistoryStore((s) => (docId ? s.canUndo(docId) : false))
39 const canRedo = useHtmlEditHistoryStore((s) => (docId ? s.canRedo(docId) : false))
40 const hasPending = useHtmlEditHistoryStore((s) => (docId ? s.hasPendingEdits(docId) : false))
41 const aiModeEnabled = useHtmlEditorAiStore((s) => s.enabled)
42
43 const displayName =
44 title || (sourcePath ? sourcePath.split(/[\\/]/).pop() : '') || t('htmlEditor.untitled')
45
46 const handleSave = useCallback(async (): Promise<void> => {
47 await useHtmlEditStore.getState().save()
48 }, [])
49
50 const handleExport = useCallback(async (): Promise<void> => {
51 const saved = await useHtmlEditStore.getState().save()
52 if (saved.error) return
53 const path = await useHtmlEditorStore.getState().exportAs()
54 if (path) useToastStore.getState().success(t('htmlEditor.exported'))
55 }, [t])
56
57 const handlePreview = useCallback(async (): Promise<void> => {
58 const saved = await useHtmlEditStore.getState().save()
59 if (saved.error || !docId) return
60 await ipc.openHtmlInBrowser({ docId })
61 }, [docId])
62
63 const handleRevealFile = useCallback(async (): Promise<void> => {
64 if (!docId) return
65 const result = await ipc.revealHtmlFile({ docId })
66 if (!result.ok) useToastStore.getState().error(t('htmlEditor.revealFileFailed'))
67 }, [docId, t])
68
69 const handleUndo = useCallback((): void => {
70 useHtmlEditStore.getState().undo()
71 }, [])
72 const handleRedo = useCallback((): void => {
73 useHtmlEditStore.getState().redo()
74 }, [])
75 const handleDiscardAll = useCallback((): void => {
76 useHtmlEditStore.getState().discardAll()
77 }, [])
78
79 const handleToggleAiMode = useCallback(async (): Promise<void> => {
80 if (isSavingEdits) return
81 const nextEnabled = !useHtmlEditorAiStore.getState().enabled
82 if (nextEnabled && docId) {
83 const editStore = useHtmlEditStore.getState()
84 editStore.commitCurrentDraft()
85 if (useHtmlEditHistoryStore.getState().hasPendingEdits(docId)) {
86 const saved = await editStore.save()
87 if (!saved.saved) return
88 }
89 }
90 useHtmlEditorAiStore.getState().setEnabled(nextEnabled)
91 useHtmlEditorUiStore.getState().clearSelectedElement()
92 }, [docId, isSavingEdits])
93
94 const tipBtn = (
95 Icon: typeof Home,
96 label: string,
97 onClick: () => void,
98 disabled?: boolean,
99 danger?: boolean
100 ) => (
101 <Tooltip>
102 <TooltipTrigger asChild>
103 <button
104 type="button"
105 onClick={onClick}
106 disabled={disabled}
107 aria-label={label}
108 className={`${iconBtnClass} ${danger ? 'text-[#8e5a53] hover:bg-[#f3e6e2]' : ''}`}
109 >
110 <Icon className="h-4 w-4" />
111 </button>
112 </TooltipTrigger>
113 <TooltipContent side="bottom">{label}</TooltipContent>
114 </Tooltip>
115 )
116
117 return (
118 <header className="app-drag-region app-titlebar relative shrink-0 bg-[#f5f1e8]/95 shadow-[0_10px_26px_rgba(93,107,77,0.055)] backdrop-blur-xl">
119 <div
120 className={`relative flex h-full items-center gap-1.5 ${
121 isMac ? 'pl-[85px]' : 'pl-4'
122 } ${isMac ? '' : 'pr-[calc(var(--app-titlebar-control-safe-area)+16px)]'}`}
123 >
124 {/* 返回 */}
125 <Tooltip>
126 <TooltipTrigger asChild>
127 <button
128 type="button"
129 onClick={() => navigate('/edit-html')}
130 className="app-no-drag inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-[8px] bg-[#e8e0d0]/72 text-[#3e4a32] shadow-[0_4px_10px_rgba(86,72,53,0.08)] transition-colors hover:bg-[#d4e4c1]/78"
131 >
132 <Home className="h-3.5 w-3.5" />
133 </button>
134 </TooltipTrigger>
135 <TooltipContent side="bottom">{t('htmlEditor.backToList')}</TooltipContent>
136 </Tooltip>
137
138 {/* 标题 */}
139 <Tooltip>
140 <TooltipTrigger asChild>
141 <div className="flex w-[150px] shrink-0 items-center gap-2 rounded-[10px] bg-[#e8e0d0]/60 px-3 py-1">
142 <div className="min-w-0 flex-1 truncate text-[12px] font-medium text-[#3e4a32]">
143 {displayName}
144 </div>
145 </div>
146 </TooltipTrigger>
147 <TooltipContent side="bottom" align="start">
148 {displayName}
149 </TooltipContent>
150 </Tooltip>
151
152 <span className="mx-0.5 h-4 w-px bg-[#e2dccf]" />
153
154 {/* 撤销 / 重做 / 撤销所有 / 保存(一组) */}
155 {tipBtn(Undo2, t('htmlEditor.undo'), handleUndo, !canUndo)}
156 {tipBtn(Redo2, t('htmlEditor.redo'), handleRedo, !canRedo)}
157 {tipBtn(RotateCcw, t('htmlEditor.discardAll'), handleDiscardAll, !hasPending, true)}
158 {tipBtn(Save, t('htmlEditor.save'), () => void handleSave(), !hasPending || isSavingEdits)}
159
160 <span className="mx-0.5 h-4 w-px bg-[#e2dccf]" />
161
162 {/* 导出 / 查看文件 / 预览 / 版本历史(一组) */}
163 {tipBtn(
164 Download,
165 t('htmlEditor.export'),
166 () => void handleExport(),
167 exporting || isSavingEdits
168 )}
169 {tipBtn(
170 FileSearch,
171 t('htmlEditor.revealFile'),
172 () => void handleRevealFile(),
173 !docId || isSavingEdits
174 )}
175 {tipBtn(
176 ExternalLink,
177 t('htmlEditor.preview'),
178 () => void handlePreview(),
179 !docId || isSavingEdits
180 )}
181 {tipBtn(History, t('htmlEditor.history'), onOpenHistory, !docId)}
182 <button
183 type="button"
184 onClick={() => void handleToggleAiMode()}
185 disabled={!docId || isSavingEdits}
186 className={`app-no-drag ml-1 rounded-md px-2 py-1 text-[12px] font-medium transition-colors disabled:pointer-events-none disabled:opacity-40 ${
187 aiModeEnabled ? 'bg-[#dbe7ca] text-[#2f3b28]' : 'text-[#5d6b4d] hover:bg-[#ece5d6]'
188 }`}
189 title={t('htmlEditor.aiModeButton')}
190 >
191 {t('htmlEditor.aiModeButton')}
192 </button>
193 </div>
194 </header>
195 )
196 }
197
197 lines Plain Text