diff --git a/assets/webui/index.html b/assets/webui/index.html index a0ca415..9bf8225 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -353,8 +353,8 @@

版本管理

- -
+ +
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index cd4ba34..a7e268e 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -1562,6 +1562,10 @@ body[data-hide-meta] .meta-panel { padding-inline: 6px; } +.document-actions.version-readonly { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + .document-list { max-height: 45dvh; min-height: 80px; @@ -2015,6 +2019,7 @@ button.outline-link:hover { .form-row[hidden], .form-pair[hidden], .form-actions > [hidden], +.document-actions > [hidden], .annotation-detail[hidden], .tool-group[hidden], .page-list[hidden], diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 12418ac..906115f 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -259,6 +259,9 @@ const el = { documentUpButton: document.querySelector("#documentUpButton"), documentDownButton: document.querySelector("#documentDownButton"), versionManageButton: document.querySelector("#versionManageButton"), + versionTitle: document.querySelector("#versionTitle"), + versionNameRow: document.querySelector("#versionNameRow"), + versionActions: document.querySelector("#versionActions"), versionPanel: document.querySelector("#versionPanel"), versionForm: document.querySelector("#versionForm"), versionNotice: document.querySelector("#versionNotice"), @@ -2078,7 +2081,7 @@ function setEditorInfo(doc) { } async function openVersionManagement() { - if (!state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return; + if (!state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return; state.versionManagement = { id: state.doc.documentVersion || "", entries: null, loading: false, failed: false }; el.versionList.replaceChildren(); el.versionName.value = ""; @@ -2148,9 +2151,16 @@ function selectManagedVersion(id, focus = false) { async function changeVersion(action) { const manager = state.versionManagement; - if (!state.editing || !manager?.entries || manager.loading || manager.failed || document.body.hasAttribute("aria-busy")) return; + if (!state.ready || state.exporting || !state.editing && action !== "select" || !manager?.entries || manager.loading || manager.failed || document.body.hasAttribute("aria-busy")) return; const entry = manager.entries.find(entry => entry.id === manager.id); if (!entry) return; + if (!state.editing) { + if (await switchReadingVersion(entry.id) && state.versionManagement === manager && el.versionPanel.open) { + manager.id = state.doc.documentVersion || ""; + await refreshVersionManagement(true); + } + return; + } if (action === "delete" && (!entry.id || !window.confirm(`删除版本“${entry.name || entry.index}”?`))) return; el.versionStatus.textContent = "正在处理版本"; if (await changeDocument("ofdgoChangeVersion", null, action, entry.id, el.versionName.value.trim())) { @@ -2160,20 +2170,63 @@ async function changeVersion(action) { } } +async function switchReadingVersion(id) { + if (!state.doc || state.editing || !state.ready || state.exporting || document.body.hasAttribute("aria-busy")) return; + if (id === (state.doc.documentVersion || "")) return true; + const dirty = state.dirty; + const before = state.editorInfo ? editorLocation() : null, revision = state.editorInfo?.revision; + const openSeq = ++state.openSeq; + setBusy(true, "正在切换版本"); + try { + const doc = await callWASM("ofdgoSelectVersion", id); + if (openSeq !== state.openSeq) return; + state.objectClipboard = state.styleClipboard = null; + resetCompositeScope(); + canvasEditor.clear(); + canvasEditor.setTool(""); + delete state.textDefaults.fontChoice; + updateTextFonts(null, true); + if (state.editorInfo) { + if (!dirty) state.savedRevision = doc.revision; + setEditorInfo(doc); + } + await openDocument({ doc, openSeq, skipAutoFonts: true, resetScroll: true, clearSelection: true, + fitMode: state.fitMode, scale: state.scale, activateActions: true }); + if (openSeq !== state.openSeq) return; + if (before) { + rememberEditorView(before, revision); + const view = state.editorViews.get(doc.revision); + if (view) view.reading = true; + } + return true; + } catch (error) { + if (openSeq === state.openSeq) el.versionStatus.textContent = error.message; + } finally { + if (openSeq === state.openSeq) setBusy(false); + } +} + function updateVersionControls() { const manager = state.versionManagement, busy = document.body.hasAttribute("aria-busy"); - const disabled = !state.editing || !state.ready || state.exporting || busy; - el.versionManageButton.hidden = !state.editing; + const disabled = !state.doc || !state.ready || state.exporting || busy; + const title = state.editing ? "版本管理" : "版本浏览"; + el.versionManageButton.hidden = !state.editing && !(state.doc?.versionCount > 0); + el.versionManageButton.title = title; + el.versionManageButton.setAttribute("aria-label", title); + el.versionTitle.textContent = title; el.versionManageButton.disabled = disabled; - el.versionNotice.textContent = state.editorInfo?.editWarnings?.join(";") || ""; + el.versionNotice.textContent = state.editing ? state.editorInfo?.editWarnings?.join(";") || "" : ""; el.versionNotice.hidden = !el.versionNotice.textContent; + el.versionNameRow.hidden = !state.editing; + el.versionActions.classList.toggle("version-readonly", !state.editing); + for (const button of [el.versionAdd, el.versionRename, el.versionDelete, el.versionDefault]) button.hidden = !state.editing; const entry = manager?.entries?.find(entry => entry.id === manager.id); const unavailable = disabled || !entry || Boolean(manager?.loading || manager?.failed); el.versionList.inert = busy || Boolean(manager?.loading); - el.versionName.disabled = el.versionAdd.disabled = unavailable; - el.versionRename.disabled = unavailable || !entry?.id || el.versionName.value.trim() === entry.name; - el.versionDelete.disabled = unavailable || !entry?.id; - el.versionDefault.disabled = unavailable || Boolean(entry?.default); + el.versionName.disabled = el.versionAdd.disabled = unavailable || !state.editing; + el.versionRename.disabled = unavailable || !state.editing || !entry?.id || el.versionName.value.trim() === entry.name; + el.versionDelete.disabled = unavailable || !state.editing || !entry?.id; + el.versionDefault.disabled = unavailable || !state.editing || Boolean(entry?.default); el.versionSelect.disabled = unavailable || entry?.id === (state.doc?.documentVersion || ""); } @@ -3235,6 +3288,8 @@ async function changeDocument(name, item, ...args) { if (doc.revision === state.editorInfo?.revision) { return true; } + const restoredView = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null; + if (restoredView?.reading && state.savedRevision === revision) state.savedRevision = doc.revision; refreshManagement = el.documentPanel.open && (name === "ofdgoChangeDocument" || name === "ofdgoUpdateInfo" || restoring); const previousDocument = state.doc.documents?.[state.doc.documentIndex || 0]; const nextDocument = doc.documents?.[doc.documentIndex || 0]; @@ -3272,7 +3327,7 @@ async function changeDocument(name, item, ...args) { } return; } - const view = name === "ofdgoUndo" ? state.editorViews.get(revision) : name === "ofdgoRedo" ? state.editorViews.get(doc.revision) : null; + const view = restoredView; const reindex = !!scope && (scoped && ["ofdgoDeleteObject", "ofdgoDeleteObjects", "ofdgoCopyObjects", "ofdgoOrderObjects", "ofdgoEraseObjects", "ofdgoEraseObjectsPath", "ofdgoGroupObjects"].includes(name) || inserting || name === "ofdgoPasteObjects" && !!args[4]); const reindexed = restoring ? view?.reindex && view.before : reindex && before; @@ -3953,6 +4008,7 @@ async function recoverWASM() { try { await openDocument({ pageIndex: state.pageIndex, + versionID: state.doc?.documentVersion, fitMode: state.fitMode || "width", scale: state.scale, skipAutoFonts: true, @@ -4676,12 +4732,16 @@ async function openDocument(options = {}) { if (openSeq !== state.openSeq) { return; } - const doc = options.doc || (options.reuseSession + let doc = options.doc || (options.reuseSession ? await callWASM("ofdgoConfigure", fonts, state.renderAnnotations) : await openWithCredentials(fonts, openSeq)); if (openSeq !== state.openSeq) { return; } + if (options.versionID !== undefined && options.versionID !== (doc.documentVersion || "")) { + doc = await callWASM("ofdgoSelectVersion", options.versionID); + if (openSeq !== state.openSeq) return; + } const pageCount = doc.pageCount || 0; const pageIndex = Math.min(Math.max(options.pageIndex || 0, 0), Math.max(pageCount - 1, 0)); state.doc = doc; diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index 3d56b84..5abe833 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -187,6 +187,7 @@ func RunWASM() { registerCallback("ofdgoOpen", openDocument) registerCallback("ofdgoSelectDocument", selectDocument) registerCallback("ofdgoDocumentVersions", documentVersions) + registerCallback("ofdgoSelectVersion", selectDocumentVersion) registerCallback("ofdgoChangeVersion", changeDocumentVersion) registerCallback("ofdgoChangeDocument", changePackageDocument) registerCallback("ofdgoPackageDocuments", packageDocuments) @@ -3445,14 +3446,20 @@ func packageDocuments(args []js.Value) (any, error) { return entries, nil } -// documentVersions 读取当前编辑文档的版本列表,不切换版本或解析页面 +// documentVersions 读取当前文档的版本列表,包含尚未保存的编辑内容 // 入参: args 保留参数 // 返回: any 版本列表, error 读取错误 func documentVersions(args []js.Value) (any, error) { - if currentEditor == nil { - return nil, fmt.Errorf("no document is being edited") + if currentSession == nil { + return nil, fmt.Errorf("ofd document is not opened") + } + var versions []ofdgo.DocumentVersionInfo + var err error + if currentEditor != nil { + versions, err = currentEditor.DocumentVersions() + } else { + versions, err = currentSession.Reader.DocumentVersions() } - versions, err := currentEditor.DocumentVersions() if err != nil { return nil, err } @@ -3467,6 +3474,28 @@ func documentVersions(args []js.Value) (any, error) { return entries, nil } +// selectDocumentVersion 切换阅读版本,已有编辑器时保留修改及历史,否则直接创建阅读视图 +// 入参: args 版本标识 +// 返回: any 文档信息, error 切换错误 +func selectDocumentVersion(args []js.Value) (any, error) { + if currentSession == nil || len(args) != 1 || args[0].Type() != js.TypeString { + return nil, fmt.Errorf("invalid version selection") + } + id := args[0].String() + if currentEditor != nil { + return changeAtomicObjects(func() error { return currentEditor.SelectVersion(id) }) + } + session, err := currentSession.DocumentVersion(id) + if err != nil { + return nil, err + } + _ = currentSession.Close() + currentSession = session + clearImport() + copiedObjects, copiedStyle = nil, nil + return session.Summary(), nil +} + // changeDocumentVersion 调用库层版本操作,沿用编辑历史和预览更新流程 // 入参: args 操作、版本标识及可选名称 // 返回: any 文档信息, error 操作错误 diff --git a/internal/webui/webui.go b/internal/webui/webui.go index acd04c8..6af1f02 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -74,6 +74,7 @@ type pageBoxInfo struct { type DocumentInfo struct { DocumentIndex int `json:"documentIndex"` DocumentVersion string `json:"documentVersion"` + VersionCount int `json:"versionCount"` Documents []DocumentEntry `json:"documents,omitempty"` Encryption EncryptionInfo `json:"encryption"` Version string `json:"version"` @@ -349,6 +350,34 @@ func (s *Session) Document(index int) (*Session, error) { return next, nil } +// DocumentVersion 创建版本阅读会话,保留未保存内容和字体配置,不修改默认版本 +// 入参: id 版本标识,空字符串选择主入口 +// 返回: *Session 阅读会话, error 版本读取错误 +func (s *Session) DocumentVersion(id string) (*Session, error) { + reader := s.Reader + if s.editor != nil { + var err error + reader, err = s.editor.Reader() + if err != nil { + return nil, err + } + } + reader, err := reader.DocumentVersion(id) + if err != nil { + return nil, err + } + next, err := newSession(reader, OpenOptions{RenderAnnotations: s.Renderer.RenderAnnotations}) + if err != nil { + return nil, err + } + ofdgo.WithRenderBackends(s.Renderer.Backends())(next.Renderer) + next.fontFS = s.fontFS + if next.fontFS != nil { + next.Renderer.SetFontFS(next.fontFS) + } + return next, nil +} + // SetFonts 更新字体配置并保留文档、页面和验签结果 // 入参: fonts 字体文件列表 // 返回: error 错误信息 @@ -451,6 +480,7 @@ func (s *Session) Summary() DocumentInfo { DetailsPending: true, } info.DocumentVersion, _ = s.Reader.DocumentVersionID() + info.VersionCount, _ = s.Reader.DocumentVersionCount() for index, body := range s.Reader.OFD.DocBody { info.Documents = append(info.Documents, DocumentEntry{Index: index, Title: body.DocInfo.Title, Root: body.DocRoot, ID: body.DocInfo.DocID}) } diff --git a/ofdgo_version.go b/ofdgo_version.go index 472ceaf..f174e69 100644 --- a/ofdgo_version.go +++ b/ofdgo_version.go @@ -40,6 +40,16 @@ type VersionFile struct { Location string } +// DocumentVersionCount 获取当前文档的版本数量,不读取版本描述文件 +// 返回: int 版本数量, error 入口读取或结构错误 +func (r *Reader) DocumentVersionCount() (int, error) { + if r.documentIndex >= 0 && r.documentIndex < r.DocumentCount() && !r.OFD.DocBody[r.documentIndex].versioned { + return 0, nil + } + entries, err := r.documentVersionEntries() + return len(entries), err +} + // DocumentVersions 按入口顺序读取当前文档的版本信息,不切换文档或读取文件清单中的内容 // 返回独立副本;没有版本列表时返回nil,不以版本号推测默认版本 // 返回: []DocumentVersionInfo 版本信息, error 读取或描述错误