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