From a9bd789bc4380be1e5be46ac0f3b6ba62b09580a Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Mon, 14 Sep 2026 22:53:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=98=E5=8C=96=E4=BD=93=E9=AA=8C):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9B=BE=E7=89=87=E4=BC=A0=E8=BE=93=EF=BC=8C?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E4=BE=A7=E6=A0=8F=E6=BB=9A=E5=8A=A8=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E5=B9=B6=E6=94=AF=E6=8C=81=E9=A1=B5=E9=9D=A2=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.css | 6 ++++-- assets/webui/ofdgo.js | 37 ++++++++++++++++++++++++++++++------- assets/webui/ofdgo.wasm.js | 3 ++- internal/webui/wasm.go | 3 +-- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index 3c88828..7f2879c 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -424,8 +424,10 @@ body[data-hide-meta] .meta-panel { overflow: auto; } -.page-list-panel { - scroll-padding-top: 51px; +.page-list-item, +.outline-list summary, +.outline-link { + scroll-margin-top: 51px; } .meta-panel { diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index fb1f1c1..8dfe070 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -40,6 +40,7 @@ const state = { searchQuery: "", searchMatches: [], searchIndex: -1, + navigationScroll: new Map(), localFonts: [], userFonts: [], fontSyncPending: false, @@ -183,7 +184,7 @@ el.navigationTabs.addEventListener("keydown", (event) => { const index = event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (current + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length; showNavigation(tabs[index]); - tabs[index].focus(); + tabs[index].focus({ preventScroll: true }); } }); el.searchForm.addEventListener("submit", (event) => { @@ -1713,6 +1714,9 @@ function resetPageFlow() { document.fonts.delete(font); } state.svgFonts.clear(); + for (const image of state.svgImages.values()) { + URL.revokeObjectURL(image.url); + } state.svgImages.clear(); state.pageCache.clear(); state.selectedPages.clear(); @@ -1873,7 +1877,8 @@ async function processPageRenderQueue() { if (task.openSeq === state.openSeq) { for (const image of page.images) { if (!state.svgImages.has(image.name)) { - state.svgImages.set(image.name, image.url); + const blob = new Blob([image.bytes], { type: image.mime }); + state.svgImages.set(image.name, { url: URL.createObjectURL(blob), size: blob.size }); } } page.imageNames = page.images.map((image) => image.name); @@ -1907,8 +1912,8 @@ function trimPageCache() { references.set(name, (references.get(name) || 0) + 1); } } - for (const url of state.svgImages.values()) { - bytes += url.length * 2; + for (const image of state.svgImages.values()) { + bytes += image.size; } for (const [index, page] of state.pageCache) { if (state.pageCache.size <= PAGE_CACHE_LIMIT && bytes <= PAGE_CACHE_BYTES) { @@ -1923,7 +1928,9 @@ function trimPageCache() { const count = references.get(name) - 1; references.set(name, count); if (count === 0) { - bytes -= state.svgImages.get(name).length * 2; + const image = state.svgImages.get(name); + bytes -= image.size; + URL.revokeObjectURL(image.url); state.svgImages.delete(name); } } @@ -2239,6 +2246,16 @@ function updatePageListCurrent() { const next = el.pageList.querySelector(`.page-list-item[data-page-index="${state.pageIndex}"]`); if (next) { setPageItemCurrent(next, true); + if (state.showPages && !el.pageList.hidden) { + const panel = el.pageListPanel.getBoundingClientRect(); + const item = next.getBoundingClientRect(); + const top = panel.top + Number.parseFloat(getComputedStyle(next).scrollMarginTop); + if (item.top < top) { + el.pageListPanel.scrollTop += item.top - top; + } else if (item.bottom > panel.bottom) { + el.pageListPanel.scrollTop += Math.min(item.top - top, item.bottom - panel.bottom); + } + } } } @@ -2283,7 +2300,7 @@ function layoutPageShell(shell, page) { function parseSVG(svgText, prefix = "") { const template = document.createElement("template"); - template.innerHTML = svgText.replace(/xlink:href="(ofdgo-image-[a-f0-9]{64})"/g, (_, name) => `xlink:href="${state.svgImages.get(name)}"`); + template.innerHTML = svgText.replace(/xlink:href="(ofdgo-image-[a-f0-9]{64})"/g, (_, name) => `xlink:href="${state.svgImages.get(name).url}"`); const svg = document.adoptNode(template.content.firstElementChild); prefixSVGIds(svg, prefix); return svg; @@ -2344,16 +2361,22 @@ function focusSearch() { } function showNavigation(selected) { + const scrollTop = el.pageListPanel.scrollTop; for (const [tab, panel] of [[el.pagesTab, el.pageList], [el.outlinesTab, el.outlineList], [el.searchTab, el.searchPanel]]) { + if (tab.getAttribute("aria-selected") === "true") { + state.navigationScroll.set(tab, scrollTop); + } const active = tab === selected; panel.hidden = !active; tab.setAttribute("aria-selected", String(active)); tab.tabIndex = active ? 0 : -1; } - el.pageListPanel.scrollTop = 0; + el.pageListPanel.scrollTop = state.navigationScroll.get(selected) || 0; } function renderOutlines() { + state.navigationScroll.clear(); + el.pageListPanel.scrollTop = 0; const outlines = state.doc.outlines || []; el.pageListTitle.hidden = true; el.navigationTabs.hidden = false; diff --git a/assets/webui/ofdgo.wasm.js b/assets/webui/ofdgo.wasm.js index 73818c5..a4f2447 100644 --- a/assets/webui/ofdgo.wasm.js +++ b/assets/webui/ofdgo.wasm.js @@ -67,7 +67,8 @@ async function handleMessage({ id, name, args }) { await output.abort(); } output = null; - self.postMessage({ id, ...result }, result.data?.bytes ? [result.data.bytes.buffer] : []); + const transfer = result.data?.bytes ? [result.data.bytes.buffer] : (result.data?.images || []).map((image) => image.bytes.buffer); + self.postMessage({ id, ...result }, transfer); } catch (err) { await output?.abort().catch(() => {}); const result = { id, ok: false, error: err.message }; diff --git a/internal/webui/wasm.go b/internal/webui/wasm.go index f27dbc9..3e5fe27 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -19,7 +19,6 @@ package webui import ( "bufio" "context" - "encoding/base64" "encoding/json" "fmt" "io" @@ -285,7 +284,7 @@ func renderPage(args []js.Value) (any, error) { } images := make([]any, len(page.Images)) for i, image := range page.Images { - images[i] = map[string]any{"name": image.Name, "url": "data:" + image.MIME + ";base64," + base64.StdEncoding.EncodeToString(image.Data)} + images[i] = map[string]any{"name": image.Name, "mime": image.MIME, "bytes": bytesToJS(image.Data)} } return successResult(map[string]any{ "index": page.Index,