返回 oh-my-ppt
SessionToolbar.tsx
根目录 / src / renderer / src / components / session-detail / toolbar / SessionToolbar.tsx
1 import {
2 Archive,
3 ChevronDown,
4 CopyPlus,
5 FileDown,
6 FileSearch,
7 Globe,
8 History,
9 Home,
10 Image as ImageIcon,
11 LayoutTemplate,
12 Loader2,
13 Monitor,
14 MoreHorizontal,
15 Package,
16 Presentation,
17 Rows3,
18 Video
19 } from 'lucide-react'
20 import { cn } from '@renderer/lib/utils'
21 import { useSessionDetailRuntimeStore, useSessionDetailUiStore } from '@renderer/store'
22 import { Button } from '../../ui/Button'
23 import {
24 DropdownMenu,
25 DropdownMenuContent,
26 DropdownMenuItem,
27 DropdownMenuSeparator,
28 DropdownMenuTrigger
29 } from '../../ui/DropdownMenu'
30 import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/Tooltip'
31 import { useT } from '@renderer/i18n'
32 import { ipc } from '@renderer/lib/ipc'
33 import { SaveTemplateDialog } from '../../templates/SaveTemplateDialog'
34 import { SaveAsNewSessionDialog } from './SaveAsNewSessionDialog'
35 import { useSessionToolbarController } from './useSessionToolbarController'
36
37 const btnClass =
38 'app-no-drag h-7 rounded-[8px] border-transparent bg-[#e8e0d0]/72 px-2.5 text-[11px] text-[#3e4a32] shadow-[0_4px_10px_rgba(86,72,53,0.08)] hover:bg-[#d4e4c1]/78'
39 const iconClass = 'mr-1.5 h-3.5 w-3.5'
40 const dropIconClass = 'mr-2 h-3.5 w-3.5 text-[#6b7280]'
41
42 const isMac = ipc.getPlatform() === 'darwin'
43
44 export function SessionToolbar({
45 sessionId,
46 isSavingEdits
47 }: {
48 sessionId: string
49 isSavingEdits?: boolean
50 }): React.JSX.Element {
51 const t = useT()
52 const {
53 hasPages,
54 isGenerating,
55 historyDisabled,
56 selectedPageHasPendingEdits,
57 canPreview,
58 canRevealFile,
59 sessionTitle,
60 saveTemplateOpen,
61 savingTemplate,
62 saveAsNewSessionOpen,
63 savingAsNewSession,
64 saveAsNewSessionDisabled,
65 defaultTemplateName,
66 defaultSaveAsNewSessionName,
67 setSaveTemplateOpen,
68 setSaveAsNewSessionOpen,
69 handleSaveTemplate,
70 handleSaveAsNewSession,
71 exportActions,
72 openHistory
73 } = useSessionToolbarController(sessionId)
74
75 const ribbonActions = useSessionDetailRuntimeStore((state) => state.workspaceRibbonActions)
76
77 const isExportingPdf = useSessionDetailUiStore((state) => state.isExportingPdf)
78 const isExportingPng = useSessionDetailUiStore((state) => state.isExportingPng)
79 const isExportingLongImage = useSessionDetailUiStore((state) => state.isExportingLongImage)
80 const isExportingPptx = useSessionDetailUiStore((state) => state.isExportingPptx)
81 const isExportingVideo = useSessionDetailUiStore((state) => state.isExportingVideo)
82 const isExportingSlidePack = useSessionDetailUiStore((state) => state.isExportingSlidePack)
83 const isExportingSessionZip = useSessionDetailUiStore((state) => state.isExportingSessionZip)
84 const isExporting =
85 isExportingPdf ||
86 isExportingPng ||
87 isExportingLongImage ||
88 isExportingPptx ||
89 isExportingVideo ||
90 isExportingSlidePack ||
91 isExportingSessionZip
92
93 const exportingAny = isExporting
94 const editLocked = selectedPageHasPendingEdits || !!isSavingEdits
95 const toolbarActionsDisabled = exportingAny || isGenerating || editLocked
96 const homeDisabled = toolbarActionsDisabled
97
98 return (
99 <>
100 {/* Mac left padding for traffic lights */}
101 <div className={cn('flex h-full items-center gap-2', isMac ? 'pl-[85px]' : 'pl-4')}>
102 {/* Home / Back */}
103 <Tooltip>
104 <TooltipTrigger asChild>
105 <button
106 type="button"
107 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 disabled:pointer-events-none disabled:opacity-45 disabled:grayscale"
108 onClick={() => ribbonActions?.onBackToSessions()}
109 disabled={homeDisabled}
110 >
111 <Home className="h-3.5 w-3.5" />
112 </button>
113 </TooltipTrigger>
114 <TooltipContent side="bottom">{t('sessionDetail.backToSessions')}</TooltipContent>
115 </Tooltip>
116
117 {/* Title */}
118 <Tooltip>
119 <TooltipTrigger asChild>
120 <div className="flex w-[150px] shrink-0 items-center gap-2 rounded-[10px] bg-[#e8e0d0]/60 px-3 py-1">
121 <div className="min-w-0 flex-1 truncate text-[12px] font-medium text-[#3e4a32]">
122 {sessionTitle}
123 </div>
124 </div>
125 </TooltipTrigger>
126 <TooltipContent side="bottom" align="start">
127 {sessionTitle}
128 </TooltipContent>
129 </Tooltip>
130
131 {/* Export dropdown */}
132 {hasPages && (
133 <DropdownMenu>
134 <DropdownMenuTrigger asChild>
135 <Button
136 type="button"
137 variant="outline"
138 size="sm"
139 className={cn(btnClass, 'gap-1')}
140 disabled={toolbarActionsDisabled}
141 >
142 {exportingAny ? (
143 <Loader2 className={cn(iconClass, 'animate-spin')} />
144 ) : (
145 <FileDown className={iconClass} />
146 )}
147 {t('sessionDetail.toolbarExport')}
148 {!exportingAny && <ChevronDown className="h-3 w-3" />}
149 </Button>
150 </DropdownMenuTrigger>
151 <DropdownMenuContent align="start" className="w-80">
152 <DropdownMenuItem
153 disabled={!exportActions.canExportPptx}
154 onClick={() => void exportActions.exportPptx()}
155 >
156 <Presentation className={dropIconClass} />
157 {t('sessionDetail.toolbarExportPptxEditable')}
158 </DropdownMenuItem>
159 <DropdownMenuItem
160 className="items-start"
161 disabled={!exportActions.canExportPptx}
162 onClick={() => void exportActions.exportPptx({ imageOnly: true })}
163 >
164 <ImageIcon className={cn(dropIconClass, 'mt-0.5')} />
165 <span className="flex min-w-0 flex-1 flex-col gap-0.5 whitespace-normal">
166 <span>{t('sessionDetail.toolbarExportPptxImageOnly')}</span>
167 </span>
168 </DropdownMenuItem>
169 <DropdownMenuSeparator />
170 <DropdownMenuItem onClick={() => void exportActions.exportPng()}>
171 <ImageIcon className={dropIconClass} />
172 {t('sessionDetail.toolbarExportPng')}
173 </DropdownMenuItem>
174 <DropdownMenuItem onClick={() => void exportActions.exportLongImage()}>
175 <Rows3 className={dropIconClass} />
176 {t('sessionDetail.toolbarExportLongImage')}
177 </DropdownMenuItem>
178 <DropdownMenuItem onClick={() => void exportActions.exportVideo()}>
179 <Video className={dropIconClass} />
180 {t('sessionDetail.toolbarExportVideo')}
181 </DropdownMenuItem>
182 <DropdownMenuItem onClick={() => void exportActions.exportPdf()}>
183 <FileDown className={dropIconClass} />
184 {t('sessionDetail.toolbarExportPdf')}
185 </DropdownMenuItem>
186 <DropdownMenuSeparator />
187 <DropdownMenuItem
188 className="items-start"
189 onClick={() => void exportActions.exportSlidePack()}
190 >
191 <Package className={cn(dropIconClass, 'mt-0.5')} />
192 <span className="flex min-w-0 flex-1 flex-col gap-0.5 whitespace-normal">
193 <span>{t('sessionDetail.toolbarExportSlidePack')}</span>
194 <span className="text-[11px] leading-snug text-[#9a8f80]">
195 {t('sessionDetail.toolbarExportSlidePackDesc')}
196 </span>
197 </span>
198 </DropdownMenuItem>
199 <DropdownMenuItem
200 className="items-start"
201 onClick={() => void exportActions.exportSessionZip()}
202 >
203 <Archive className={cn(dropIconClass, 'mt-0.5')} />
204 <span className="flex min-w-0 flex-1 flex-col gap-0.5 whitespace-normal">
205 <span>{t('sessionDetail.toolbarExportSessionZip')}</span>
206 <span className="text-[11px] leading-snug text-[#9a8f80]">
207 {t('sessionDetail.toolbarExportSessionZipDesc')}
208 </span>
209 </span>
210 </DropdownMenuItem>
211 </DropdownMenuContent>
212 </DropdownMenu>
213 )}
214
215 {/* Browse */}
216 {canPreview && (
217 <Tooltip>
218 <TooltipTrigger asChild>
219 <Button
220 type="button"
221 variant="outline"
222 size="sm"
223 className={btnClass}
224 onClick={() => void exportActions.openProjectPreview()}
225 disabled={toolbarActionsDisabled}
226 >
227 <Globe className={iconClass} />
228 {t('sessionDetail.preview')}
229 </Button>
230 </TooltipTrigger>
231 <TooltipContent side="bottom" align="start">
232 {t('sessionDetail.previewTooltip')}
233 </TooltipContent>
234 </Tooltip>
235 )}
236
237 {/* Present */}
238 {hasPages && (
239 <Tooltip>
240 <TooltipTrigger asChild>
241 <Button
242 type="button"
243 variant="outline"
244 size="sm"
245 className={btnClass}
246 onClick={() => void exportActions.openPresentation()}
247 disabled={toolbarActionsDisabled}
248 >
249 <Monitor className={iconClass} />
250 {t('sessionDetail.present')}
251 </Button>
252 </TooltipTrigger>
253 <TooltipContent side="bottom" align="start">
254 {t('sessionDetail.presentTooltip')}
255 </TooltipContent>
256 </Tooltip>
257 )}
258
259 {/* Version History */}
260 {hasPages && (
261 <Tooltip>
262 <TooltipTrigger asChild>
263 <Button
264 type="button"
265 variant="outline"
266 size="sm"
267 className={btnClass}
268 onClick={openHistory}
269 disabled={historyDisabled || exportingAny}
270 >
271 <History className={iconClass} />
272 {t('sessionDetail.history')}
273 </Button>
274 </TooltipTrigger>
275 <TooltipContent side="bottom" align="start">
276 {t('sessionDetail.historyTooltip')}
277 </TooltipContent>
278 </Tooltip>
279 )}
280
281 {/* More dropdown */}
282 {hasPages && (
283 <DropdownMenu>
284 <DropdownMenuTrigger asChild>
285 <Button
286 type="button"
287 variant="outline"
288 size="sm"
289 className={cn(btnClass, 'px-2')}
290 disabled={toolbarActionsDisabled}
291 >
292 <MoreHorizontal className="h-3.5 w-3.5" />
293 </Button>
294 </DropdownMenuTrigger>
295 <DropdownMenuContent align="start" className="min-w-[12rem]">
296 <DropdownMenuItem
297 disabled={saveAsNewSessionDisabled || editLocked}
298 onClick={() => setSaveAsNewSessionOpen(true)}
299 >
300 {savingAsNewSession ? (
301 <Loader2 className={cn(dropIconClass, 'animate-spin')} />
302 ) : (
303 <CopyPlus className={dropIconClass} />
304 )}
305 {t('sessionDetail.saveAsNewSession')}
306 </DropdownMenuItem>
307 <DropdownMenuSeparator />
308 <DropdownMenuItem
309 disabled={editLocked || exportingAny || isGenerating}
310 onClick={() => setSaveTemplateOpen(true)}
311 >
312 <LayoutTemplate className={dropIconClass} />
313 {t('sessionDetail.saveTemplate')}
314 </DropdownMenuItem>
315 {canRevealFile && (
316 <DropdownMenuItem
317 disabled={editLocked || exportingAny || isGenerating}
318 onClick={() => void exportActions.revealSelectedPageFile()}
319 >
320 <FileSearch className={dropIconClass} />
321 {t('sessionDetail.revealFile')}
322 </DropdownMenuItem>
323 )}
324 </DropdownMenuContent>
325 </DropdownMenu>
326 )}
327 </div>
328
329 <SaveTemplateDialog
330 open={saveTemplateOpen}
331 defaultName={defaultTemplateName}
332 saving={savingTemplate}
333 onOpenChange={setSaveTemplateOpen}
334 onSubmit={(payload) => void handleSaveTemplate(payload)}
335 />
336 <SaveAsNewSessionDialog
337 open={saveAsNewSessionOpen}
338 defaultName={defaultSaveAsNewSessionName}
339 saving={savingAsNewSession}
340 onOpenChange={setSaveAsNewSessionOpen}
341 onSubmit={(payload) => void handleSaveAsNewSession(payload)}
342 />
343 </>
344 )
345 }
346
346 lines Plain Text