| 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 |