返回 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 isDeckEditing = useGenerateStore((state) =>
74 currentSession ? Boolean(state.deckEditJobs[currentSession.id]) : false
75 )
76 const styleSwitchJob = useGenerateStore((state) =>
77 currentSession ? state.styleSwitchJobs[currentSession.id] || null : null
78 )
79 const currentPages = useGenerateStore((state) => state.currentPages)
80 const toastInfo = useToastStore((state) => state.info)
81 const selectedPageId = useSessionDetailUiStore((state) => state.selectedPageId)
82 const isAddingPage = useSessionDetailUiStore((state) => state.isAddingPage)
83 const addingPageId = useSessionDetailUiStore((state) => state.addingPageId)
84 const isRetryingSinglePage = useSessionDetailUiStore((state) => state.isRetryingSinglePage)
85 const retryingSinglePageId = useSessionDetailUiStore((state) => state.retryingSinglePageId)
86 const activeTab = useSessionDetailUiStore((state) => state.workspaceTab)
87 const setActiveTab = useSessionDetailUiStore((state) => state.setWorkspaceTab)
88 const bumpPreviewKey = useSessionDetailUiStore((state) => state.bumpPreviewKey)
89 const interactionMode = useSessionDetailUiStore((state) => state.interactionMode)
90 const setInteractionMode = useSessionDetailUiStore((state) => state.setInteractionMode)
91 const clearSelectedElement = useSessionDetailUiStore((state) => state.clearSelectedElement)
92 const setSpeechScriptDialogOpen = useSessionDetailUiStore(
93 (state) => state.setSpeechScriptDialogOpen
94 )
95 const pages = useMemo(() => normalizePagesForSelection(currentPages), [currentPages])
96 const selectedPage = useMemo(
97 () => pages.find((page) => page.id === selectedPageId) ?? pages[0] ?? null,
98 [pages, selectedPageId]
99 )
100
101 const selectedPageKey = selectedPage?.htmlPath
102 ? `${selectedPage.pageId}:${selectedPage.htmlPath}`
103 : null
104 const pageId = selectedPage?.pageId
105 const styleSwitchPageLocked = isStyleSwitchPageLocked(styleSwitchJob, pageId)
106 const isPageEditing = pageEditJob?.pageId === pageId
107 const isScopedGeneration =
108 isAddingPage ||
109 isRetryingSinglePage ||
110 Boolean(pageEditJob) ||
111 Boolean(styleSwitchJob)
112 const isCurrentPageGenerating =
113 isGenerating &&
114 (!isScopedGeneration ||
115 isPageGenerationLocked(selectedPage?.id, {
116 isAddingPage,
117 addingPageId,
118 isRetryingSinglePage,
119 retryingSinglePageId
120 }) ||
121 isPageEditing ||
122 styleSwitchPageLocked)
123 const canUndo = useEditHistoryStore((state) => state.canUndo(pageId))
124 const canRedo = useEditHistoryStore((state) => state.canRedo(pageId))
125 const hasPendingEdits = useEditHistoryStore((state) => state.hasPendingEdits(pageId))
126 const tabLabels = useMemo<Record<SessionWorkspaceTab, string>>(
127 () => ({
128 preview: t('sessionDetail.previewMode'),
129 edit: t('sessionDetail.editMode'),
130 browse: t('sessionDetail.browseMode'),
131 style: t('sessionDetail.styleMode'),
132 animation: t('sessionDetail.animationTab'),
133 speech: t('sessionDetail.speechScript'),
134 ai: t('sessionDetail.aiMode')
135 }),
136 [t]
137 )
138 const pendingTabLabel = pendingTab ? tabLabels[pendingTab] : ''
139
140 const applyTab = useCallback(
141 (tab: SessionWorkspaceTab): void => {
142 if (activeTab === 'animation' && tab !== 'animation') {
143 clearSelectedElement()
144 bumpPreviewKey()
145 }
146 setActiveTab(tab)
147 if (tab === 'preview') {
148 setInteractionMode('preview')
149 setSpeechScriptDialogOpen(false)
150 return
151 }
152 if (tab === 'browse' || tab === 'style') {
153 setInteractionMode('preview')
154 setSpeechScriptDialogOpen(false)
155 return
156 }
157 if (tab === 'speech') {
158 clearSelectedElement()
159 setInteractionMode('preview')
160 setSpeechScriptDialogOpen(true)
161 return
162 }
163 if (tab === 'animation') {
164 useEditSessionStore.getState().cancelEdit()
165 clearSelectedElement()
166 setInteractionMode('animation-select')
167 setSpeechScriptDialogOpen(false)
168 toastInfo(t('sessionDetail.animationModeToast'))
169 return
170 }
171 if (tab === 'ai') {
172 clearSelectedElement()
173 setInteractionMode('ai-inspect')
174 setSpeechScriptDialogOpen(false)
175 toastInfo(t('sessionDetail.inspectActiveToast'))
176 return
177 }
178 if (interactionMode !== 'edit') {
179 setInteractionMode('edit')
180 }
181 setSpeechScriptDialogOpen(false)
182 toastInfo(t('sessionDetail.editModeToast'))
183 },
184 [
185 clearSelectedElement,
186 activeTab,
187 bumpPreviewKey,
188 interactionMode,
189 setActiveTab,
190 setInteractionMode,
191 setSpeechScriptDialogOpen,
192 t,
193 toastInfo
194 ]
195 )
196
197 const activateTab = useCallback(
198 (tab: SessionWorkspaceTab): void => {
199 if (tab === activeTab) return
200 const canSwitchWithoutSave = tab === 'browse' || tab === 'edit' || tab === 'preview'
201 if (!canSwitchWithoutSave) {
202 useEditSessionStore.getState().commitCurrentDraft()
203 }
204 const hasCurrentPendingEdits = useEditHistoryStore.getState().hasPendingEdits(pageId)
205 if (!canSwitchWithoutSave && (hasPendingEdits || hasCurrentPendingEdits)) {
206 setPendingTab(tab)
207 return
208 }
209 applyTab(tab)
210 },
211 [activeTab, applyTab, hasPendingEdits, pageId]
212 )
213
214 const cancelPendingTab = useCallback((): void => {
215 if (savingBeforeTabSwitch) return
216 setPendingTab(null)
217 }, [savingBeforeTabSwitch])
218
219 const discardPendingTab = useCallback((): void => {
220 const tab = pendingTab
221 if (!tab || savingBeforeTabSwitch) return
222 useEditSessionStore.getState().discardAll()
223 if (pageId && useEditHistoryStore.getState().hasPendingEdits(pageId)) {
224 useEditHistoryStore.getState().clearPage(pageId)
225 }
226 setPendingTab(null)
227 applyTab(tab)
228 }, [applyTab, pageId, pendingTab, savingBeforeTabSwitch])
229
230 const confirmPendingTab = useCallback(async (): Promise<void> => {
231 const tab = pendingTab
232 if (!tab || savingBeforeTabSwitch) return
233 setSavingBeforeTabSwitch(true)
234 try {
235 const result = await useEditSessionStore.getState().save()
236 const stillPending = useEditHistoryStore.getState().hasPendingEdits(pageId)
237 if (result.saved || !stillPending) {
238 setPendingTab(null)
239 applyTab(tab)
240 }
241 } finally {
242 setSavingBeforeTabSwitch(false)
243 }
244 }, [applyTab, pageId, pendingTab, savingBeforeTabSwitch])
245
246 const state: WorkspaceRibbonState = useMemo(
247 () => ({
248 isGenerating: isCurrentPageGenerating,
249 isPageEditing,
250 isDeckEditing,
251 isStyleSwitchPageLocked: styleSwitchPageLocked,
252 isSavingEdits,
253 canUndo,
254 canRedo,
255 hasPendingEdits,
256 activeTab
257 }),
258 [
259 activeTab,
260 canRedo,
261 canUndo,
262 hasPendingEdits,
263 isDeckEditing,
264 styleSwitchPageLocked,
265 isCurrentPageGenerating,
266 isPageEditing,
267 isSavingEdits
268 ]
269 )
270
271 return {
272 selectedPageKey,
273 state,
274 activateTab,
275 pendingTab,
276 pendingTabLabel,
277 savingBeforeTabSwitch,
278 cancelPendingTab,
279 discardPendingTab,
280 confirmPendingTab
281 }
282 }
283
283 lines TYPESCRIPT