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