返回 oh-my-ppt
htmlEditorStore.ts
根目录 / src / renderer / src / store / htmlEditorStore.ts
1 import { create } from 'zustand'
2 import { ipc, type HtmlEditorImportResult } from '../lib/ipc'
3
4 /**
5 * HTML 编辑器的文档态(与 session-edit 完全独立)。
6 * 持有当前打开文档的身份(docId)、工作文件路径、设计宽度,以及**真相源 HTML 串**。
7 * 不持有编辑选择/草稿/撤销栈——那些在 htmlEditStore / htmlEditHistoryStore。
8 */
9
10 const INITIAL_DESIGN_WIDTH = 1280
11
12 export interface HtmlEditDocumentSummary {
13 id: string
14 title: string
15 sourcePath: string | null
16 htmlPath: string
17 designWidth: number
18 updatedAt: number
19 thumbnailPath: string | null
20 }
21
22 export interface HtmlEditorDocSnapshot {
23 docId: string | null
24 title: string
25 htmlPath: string | null
26 sourcePath: string | null
27 designWidth: number
28 html: string
29 importing: boolean
30 exporting: boolean
31 error: string | null
32 documents: HtmlEditDocumentSummary[]
33 }
34
35 export type HtmlEditorImportOutcome =
36 | { ok: true }
37 | { ok: false; reason: 'user-cancelled' | 'storage-not-configured' | 'error'; message?: string }
38
39 interface HtmlEditorStore extends HtmlEditorDocSnapshot {
40 importFile: () => Promise<HtmlEditorImportOutcome>
41 openDocument: (docId: string) => Promise<HtmlEditorImportOutcome>
42 loadDocuments: () => Promise<void>
43 removeDocument: (docId: string) => Promise<boolean>
44 setDocumentThumbnail: (docId: string, thumbnailPath: string) => void
45 setHtml: (html: string) => void
46 exportAs: () => Promise<string | null>
47 reset: () => void
48 }
49
50 const initial: HtmlEditorDocSnapshot = {
51 docId: null,
52 title: '',
53 htmlPath: null,
54 sourcePath: null,
55 designWidth: INITIAL_DESIGN_WIDTH,
56 html: '',
57 importing: false,
58 exporting: false,
59 error: null,
60 documents: []
61 }
62
63 export const useHtmlEditorStore = create<HtmlEditorStore>((set, get) => ({
64 ...initial,
65
66 importFile: async () => {
67 set({ importing: true, error: null })
68 try {
69 const result = await ipc.importHtmlFile()
70 if (result.cancelled) {
71 set({ importing: false })
72 return { ok: false, reason: result.reason ?? 'user-cancelled' }
73 }
74 applyDocResult(set, result)
75 return { ok: true }
76 } catch (e) {
77 const message = e instanceof Error ? e.message : String(e)
78 set({ importing: false, error: message })
79 return { ok: false, reason: 'error', message }
80 }
81 },
82
83 openDocument: async (docId: string) => {
84 set({
85 docId: null,
86 title: '',
87 htmlPath: null,
88 sourcePath: null,
89 designWidth: INITIAL_DESIGN_WIDTH,
90 html: '',
91 importing: true,
92 error: null
93 })
94 try {
95 const result = await ipc.openHtmlDocument({ docId })
96 if (result.cancelled) {
97 set({ importing: false })
98 return { ok: false, reason: 'user-cancelled' }
99 }
100 applyDocResult(set, result)
101 return { ok: true }
102 } catch (e) {
103 const message = e instanceof Error ? e.message : String(e)
104 set({ importing: false, error: message })
105 return { ok: false, reason: 'error', message }
106 }
107 },
108
109 loadDocuments: async () => {
110 try {
111 const { documents } = await ipc.listHtmlDocuments()
112 set({ documents })
113 } catch {
114 /* 忽略,列表保持空 */
115 }
116 },
117
118 removeDocument: async (docId) => {
119 try {
120 const result = await ipc.cleanupHtmlEditor({ docId })
121 if (!result.ok) return false
122 set((state) => {
123 const documents = state.documents.filter((document) => document.id !== docId)
124 return state.docId === docId ? { ...initial, documents } : { documents }
125 })
126 return true
127 } catch {
128 return false
129 }
130 },
131
132 setDocumentThumbnail: (docId, thumbnailPath) =>
133 set((state) => ({
134 documents: state.documents.map((document) =>
135 document.id === docId ? { ...document, thumbnailPath } : document
136 )
137 })),
138
139 setHtml: (html) => set({ html }),
140
141 exportAs: async () => {
142 const { html, title } = get()
143 if (!html) return null
144 set({ exporting: true })
145 try {
146 const result = await ipc.exportHtml({
147 html,
148 suggestedName: title ? `${title}.html` : 'edited.html'
149 })
150 set({ exporting: false })
151 return result.cancelled ? null : result.path
152 } catch {
153 set({ exporting: false })
154 return null
155 }
156 },
157
158 reset: () => set({ ...initial, documents: get().documents })
159 }))
160
161 function applyDocResult(
162 set: (partial: Partial<HtmlEditorDocSnapshot>) => void,
163 result: HtmlEditorImportResult
164 ): void {
165 set({
166 docId: result.docId,
167 title: result.title,
168 htmlPath: result.htmlPath,
169 sourcePath: result.sourcePath,
170 designWidth: result.designWidth,
171 html: result.html,
172 importing: false,
173 error: null
174 })
175 }
176
176 lines TYPESCRIPT