返回 oh-my-ppt
useWorkspaceRibbonController.ts
根目录 / src / renderer / src / components / session-detail / hooks / useWorkspaceRibbonController.ts
1 import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2 import {
3 useEditHistoryStore,
4 useEditSessionStore,
5 useGenerateStore,
6 useSessionStore,
7 useSessionDetailRuntimeStore,
8 useSessionDetailUiStore,
9 useToastStore,
10 isStyleSwitchPageLocked,
11 type WorkspaceRibbonRegisteredActions
12 } from '@renderer/store'
13 import { useT } from '@renderer/i18n'
14 import type { SessionWorkspaceTab } from '@renderer/types/session-detail'
15 import { isPageGenerationLocked, normalizePagesForSelection } from '../shared'
16 import type { WorkspaceRibbonState } from '../workspace/toolbar/types'
17
18 export function useWorkspaceRibbonActionsRegistration(
19 actions: WorkspaceRibbonRegisteredActions
20 ): void {
21 const actionsRef = useRef(actions)
22 const setWorkspaceRibbonActions = useSessionDetailRuntimeStore(
23 (state) => state.setWorkspaceRibbonActions
24 )
25
26 useEffect(() => {
27 actionsRef.current = actions
28 }, [actions])
29
30 useEffect(() => {
31 const registeredActions: WorkspaceRibbonRegisteredActions = {
32 onUndo: () => actionsRef.current.onUndo(),
33 onRedo: () => actionsRef.current.onRedo(),
34 onSaveCurrentPage: () => actionsRef.current.onSaveCurrentPage(),
35 onDiscardAllEdits: () => actionsRef.current.onDiscardAllEdits(),
36 onApplySelectedToAllPages: () => actionsRef.current.onApplySelectedToAllPages(),
37 onCopySelectedElement: () => actionsRef.current.onCopySelectedElement(),
38 onDeleteSelectedElement: () => actionsRef.current.onDeleteSelectedElement(),
39 onBackToSessions: () => actionsRef.current.onBackToSessions(),
40 onAddFromLibrary: (type) => actionsRef.current.onAddFromLibrary(type),
41 onAddFromLocal: (type) => actionsRef.current.onAddFromLocal(type),
42 onAddText: () => actionsRef.current.onAddText(),
43 onAddArtText: (templateId) => actionsRef.current.onAddArtText(templateId),
44 onAddShape: (type) => actionsRef.current.onAddShape(type),
45 onAddIcon: (iconId) => actionsRef.current.onAddIcon(iconId),
46 onAddChart: (type) => actionsRef.current.onAddChart(type),
47 onAddFormula: () => actionsRef.current.onAddFormula()
48 }
49 setWorkspaceRibbonActions(registeredActions)
50 return () => setWorkspaceRibbonActions(null)
51 }, [setWorkspaceRibbonActions])
52 }
53
54 export function useWorkspaceRibbonController(isSavingEdits: boolean): {
55 selectedPageKey: string | null
56 state: WorkspaceRibbonState
57 activateTab: (tab: SessionWorkspaceTab) => void
58 pendingTab: SessionWorkspaceTab | null
59 pendingTabLabel: string
60 savingBeforeTabSwitch: boolean
61 cancelPendingTab: () => void
62 discardPendingTab: () => void
63 confirmPendingTab: () => Promise<void>
64 } {
65 const t = useT()
66 const [pendingTab, setPendingTab] = useState<SessionWorkspaceTab | null>(null)
67 const [savingBeforeTabSwitch, setSavingBeforeTabSwitch] = useState(false)
68 const isGenerating = useGenerateStore((state) => state.isGenerating)
69 const currentSession = useSessionStore((state) => state.currentSession)
70 const pageEditJob = useGenerateStore((state) =>
71 currentSession ? state.pageEditJobs[currentSession.id] || null : null
72 )
73 const pageBeautifyJob = useGenerateStore((state) =>
74 currentSession ? state.pageBeautifyJobs[currentSession.id] || null : null
75 )
76 const isDeckEditing = useGenerateStore((state) =>
77 currentSession ? Boolean(state.deckEditJobs[currentSession.id]) : false
78 )
79 const styleSwitchJob = useGenerateStore((state) =>
80 currentSession ? state.styleSwitchJobs[currentSession.id] || null : null
81 )
82 const currentPages = useGenerateStore((state) => state.currentPages)
83 const toastInfo = useToastStore((state) => state.info)
84 const selectedPageId = useSessionDetailUiStore((state) => state.selectedPageId)
85 const isAddingPage = useSessionDetailUiStore((state) => state.isAddingPage)
86 const addingPageId = useSessionDetailUiStore((state) => state.addingPageId)
87 const isRetryingSinglePage = useSessionDetailUiStore((state) => state.isRetryingSinglePage)
88 const retryingSinglePageId = useSessionDetailUiStore((state) => state.retryingSinglePageId)
89 const activeTab = useSessionDetailUiStore((state) => state.workspaceTab)
90 const setActiveTab = useSessionDetailUiStore((state) => state.setWorkspaceTab)
91 const bumpPreviewKey = useSessionDetailUiStore((state) => state.bumpPreviewKey)
92 const interactionMode = useSessionDetailUiStore((state) => state.interactionMode)
93 const setInteractionMode = useSessionDetailUiStore((state) => state.setInteractionMode)
94 const clearSelectedElement = useSessionDetailUiStore((state) => state.clearSelectedElement)
95 const setSpeechScriptDialogOpen = useSessionDetailUiStore(
96 (state) => state.setSpeechScriptDialogOpen
97 )
98 const pages = useMemo(() => normalizePagesForSelection(currentPages), [currentPages])
99 const selectedPage = useMemo(
100 () => pages.find((page) => page.id === selectedPageId) ?? pages[0] ?? null,
101 [pages, selectedPageId]
102 )
103
104 const selectedPageKey = selectedPage?.htmlPath
105 ? `${selectedPage.pageId}:${selectedPage.htmlPath}`
106 : null
107 const pageId = selectedPage?.pageId
108 const styleSwitchPageLocked = isStyleSwitchPageLocked(styleSwitchJob, pageId)
109 const isPageEditing = pageEditJob?.pageId === pageId
110 const isPageBeautifying = pageBeautifyJob?.pageId === pageId
111 const isScopedGeneration =
112 isAddingPage ||
113 isRetryingSinglePage ||
114 Boolean(pageEditJob) ||
115 Boolean(pageBeautifyJob) ||
116 Boolean(styleSwitchJob)
117 const isCurrentPageGenerating =
118 isGenerating &&
119 (!isScopedGeneration ||
120 isPageGenerationLocked(selectedPage?.id, {
121 isAddingPage,
122 addingPageId,
123 isRetryingSinglePage,
124 retryingSinglePageId
125 }) ||
126 isPageEditing ||
127 isPageBeautifying ||
128 styleSwitchPageLocked)
129 const canUndo = useEditHistoryStore((state) => state.canUndo(pageId))
130 const canRedo = useEditHistoryStore((state) => state.canRedo(pageId))
131 const hasPendingEdits = useEditHistoryStore((state) => state.hasPendingEdits(pageId))
132 const tabLabels = useMemo<Record<SessionWorkspaceTab, string>>(
133 () => ({
134 preview: t('sessionDetail.previewMode'),
135 edit: t('sessionDetail.editMode'),
136 browse: t('sessionDetail.browseMode'),
137 style: t('sessionDetail.styleMode'),
138 animation: t('sessionDetail.animationTab'),
139 speech: t('sessionDetail.speechScript'),
140 ai: t('sessionDetail.aiMode')
141 }),
142 [t]
143 )
144 const pendingTabLabel = pendingTab ? tabLabels[pendingTab] : ''
145
146 const applyTab = useCallback(
147 (tab: SessionWorkspaceTab): void => {
148 if (activeTab === 'animation' && tab !== 'animation') {
149 clearSelectedElement()
150 bumpPreviewKey()
151 }
152 setActiveTab(tab)
153 if (tab === 'preview') {
154 setInteractionMode('preview')
155 setSpeechScriptDialogOpen(false)
156 return
157 }
158 if (tab === 'browse' || tab === 'style') {
159 setInteractionMode('preview')
160 setSpeechScriptDialogOpen(false)
161 return
162 }
163 if (tab === 'speech') {
164 clearSelectedElement()
165 setInteractionMode('preview')
166 setSpeechScriptDialogOpen(true)
167 return
168 }
169 if (tab === 'animation') {
170 useEditSessionStore.getState().cancelEdit()
171 clearSelectedElement()
172 setInteractionMode('animation-select')
173 setSpeechScriptDialogOpen(false)
174 toastInfo(t('sessionDetail.animationModeToast'))
175 return
176 }
177 if (tab === 'ai') {
178 clearSelectedElement()
179 setInteractionMode('ai-inspect')
180 setSpeechScriptDialogOpen(false)
181 toastInfo(t('sessionDetail.inspectActiveToast'))
182 return
183 }
184 if (interactionMode !== 'edit') {
185 setInteractionMode('edit')
186 }
187 setSpeechScriptDialogOpen(false)
188 toastInfo(t('sessionDetail.editModeToast'))
189 },
190 [
191 clearSelectedElement,
192 activeTab,
193 bumpPreviewKey,
194 interactionMode,
195 setActiveTab,
196 setInteractionMode,
197 setSpeechScriptDialogOpen,
198 t,
199 toastInfo
200 ]
201 )
202
203 const activateTab = useCallback(
204 (tab: SessionWorkspaceTab): void => {
205 if (tab === activeTab) return
206 const canSwitchWithoutSave = tab === 'browse' || tab === 'edit' || tab === 'preview'
207 if (!canSwitchWithoutSave) {
208 useEditSessionStore.getState().commitCurrentDraft()
209 }
210 const hasCurrentPendingEdits = useEditHistoryStore.getState().hasPendingEdits(pageId)
211 if (!canSwitchWithoutSave && (hasPendingEdits || hasCurrentPendingEdits)) {
212 setPendingTab(tab)
213 return
214 }
215 applyTab(tab)
216 },
217 [activeTab, applyTab, hasPendingEdits, pageId]
218 )
219
220 const cancelPendingTab = useCallback((): void => {
221 if (savingBeforeTabSwitch) return
222 setPendingTab(null)
223 }, [savingBeforeTabSwitch])
224
225 const discardPendingTab = useCallback((): void => {
226 const tab = pendingTab
227 if (!tab || savingBeforeTabSwitch) return
228 useEditSessionStore.getState().discardAll()
229 if (pageId && useEditHistoryStore.getState().hasPendingEdits(pageId)) {
230 useEditHistoryStore.getState().clearPage(pageId)
231 }
232 setPendingTab(null)
233 applyTab(tab)
234 }, [applyTab, pageId, pendingTab, savingBeforeTabSwitch])
235
236 const confirmPendingTab = useCallback(async (): Promise<void> => {
237 const tab = pendingTab
238 if (!tab || savingBeforeTabSwitch) return
239 setSavingBeforeTabSwitch(true)
240 try {
241 const result = await useEditSessionStore.getState().save()
242 const stillPending = useEditHistoryStore.getState().hasPendingEdits(pageId)
243 if (result.saved || !stillPending) {
244 setPendingTab(null)
245 applyTab(tab)
246 }
247 } finally {
248 setSavingBeforeTabSwitch(false)
249 }
250 }, [applyTab, pageId, pendingTab, savingBeforeTabSwitch])
251
252 const state: WorkspaceRibbonState = useMemo(
253 () => ({
254 isGenerating: isCurrentPageGenerating,
255 isPageEditing,
256 isPageBeautifying,
257 isDeckEditing,
258 isStyleSwitchPageLocked: styleSwitchPageLocked,
259 isSavingEdits,
260 canUndo,
261 canRedo,
262 hasPendingEdits,
263 activeTab
264 }),
265 [
266 activeTab,
267 canRedo,
268 canUndo,
269 hasPendingEdits,
270 isDeckEditing,
271 styleSwitchPageLocked,
272 isCurrentPageGenerating,
273 isPageEditing,
274 isPageBeautifying,
275 isSavingEdits
276 ]
277 )
278
279 return {
280 selectedPageKey,
281 state,
282 activateTab,
283 pendingTab,
284 pendingTabLabel,
285 savingBeforeTabSwitch,
286 cancelPendingTab,
287 discardPendingTab,
288 confirmPendingTab
289 }
290 }
291
291 lines TYPESCRIPT