Add PDF state persistence

This commit is contained in:
2026-03-30 09:29:19 +02:00
parent d1bb79570d
commit 4613b8e5dd
15 changed files with 380 additions and 55 deletions

View File

@@ -33,8 +33,11 @@ const zoomLabel = document.getElementById("zoom-label");
const pageIndicator = document.getElementById("page-indicator");
// ── Global state ─────────────────────────────────────────────────────────────
const refId = new URLSearchParams(location.search).get("ref_id") || "";
const DPR = window.devicePixelRatio || 1;
const params = new URLSearchParams(location.search);
const refId = params.get("ref_id") || "";
const savedZoom = parseFloat(params.get("zoom")); // NaN if absent
const savedScrollTop = parseFloat(params.get("scroll_top")); // NaN if absent
const DPR = window.devicePixelRatio || 1;
let pdfDoc = null;
let pageManager = null;
@@ -61,12 +64,13 @@ function refreshPageIndicator() {
pageIndicator.textContent = cur + " / " + pageManager.numPages;
}
async function fitToWidth() {
async function fitToPage() {
if (!pdfDoc) return 1.0;
const page = await pdfDoc.getPage(1);
const vp = page.getViewport({ scale: 1.0 });
const avail = container.clientWidth - 40;
return Math.max(0.1, Math.min(5.0, avail / vp.width));
const page = await pdfDoc.getPage(1);
const vp = page.getViewport({ scale: 1.0 });
const scaleW = (container.clientWidth - 40) / vp.width;
const scaleH = (container.clientHeight - 40) / vp.height;
return Math.max(0.1, Math.min(5.0, Math.min(scaleW, scaleH)));
}
function scrollToPage(pageNum) {
@@ -103,7 +107,7 @@ async function renderPage(pageNum, scale, gen) {
const bitmap = offscreen.transferToImageBitmap();
pageManager?.onRendered(pageNum, gen, bitmap);
refreshPageIndicator();
setStatus("Ready");
if (pageManager?.allRendered) setStatus("Ready");
} catch (e) {
if (e?.name !== "RenderingCancelledException") {
console.warn("[viewer] render error page", pageNum, e);
@@ -113,6 +117,11 @@ async function renderPage(pageNum, scale, gen) {
}
}
function sendViewerState() {
if (!bridge || !zoomController) return;
bridge.postViewerState(refId, zoomController.scale, container.scrollTop);
}
// ── Visibility change callback (called by ViewportTracker) ───────────────────
function onVisibilityChange(bufferSet, visibleSet) {
currentBufferSet = bufferSet;
@@ -145,9 +154,12 @@ async function load() {
viewports.push({ width: vp.width, height: vp.height });
}
// 3. Compute initial fit-to-width scale
const avail = container.clientWidth - 40;
const initialScale = Math.max(0.1, Math.min(5.0, avail / viewports[0].width));
// 3. Compute initial scale: use saved zoom if available, else fit full page
const fittedScale = Math.max(0.1, Math.min(5.0, Math.min(
(container.clientWidth - 40) / viewports[0].width,
(container.clientHeight - 40) / viewports[0].height,
)));
const initialScale = (savedZoom > 0) ? Math.max(0.1, Math.min(5.0, savedZoom)) : fittedScale;
// 4. PageManager — creates placeholder divs
pageManager = new PageManager(
@@ -160,11 +172,14 @@ async function load() {
container, pageManager.pageWrappers, onVisibilityChange,
);
// 6. ZoomController
// 6. ZoomController — send state after each debounced re-render
zoomController = new ZoomController(
container,
pageManager,
(newScale, bufferSet, visibleSet) => pageManager.onScaleChange(newScale, bufferSet, visibleSet),
(newScale, bufferSet, visibleSet) => {
pageManager.onScaleChange(newScale, bufferSet, visibleSet);
sendViewerState();
},
() => ({ bufferSet: currentBufferSet, visibleSet: currentVisibleSet }),
zoomLabel,
initialScale,
@@ -182,19 +197,29 @@ async function load() {
document.getElementById("btn-zoom-in").addEventListener("click",
() => zoomController.applyScale(zoomController.scale * 1.25));
document.getElementById("btn-zoom-fit").addEventListener("click",
async () => zoomController.applyScale(await fitToWidth()));
async () => zoomController.applyScale(await fitToPage()));
// Keyboard shortcuts + keydown forwarding to parent
document.addEventListener("keydown", ev => {
if (ev.target.tagName === "INPUT") return;
if (ev.key === "+" || ev.key === "=") { ev.preventDefault(); zoomController.applyScale(zoomController.scale * 1.25); }
if (ev.key === "-") { ev.preventDefault(); zoomController.applyScale(zoomController.scale / 1.25); }
if (ev.key === "0") { ev.preventDefault(); fitToWidth().then(s => zoomController.applyScale(s)); }
if (ev.key === "0") { ev.preventDefault(); fitToPage().then(s => zoomController.applyScale(s)); }
bridge.forwardKeydown(ev);
});
// Scroll → update page indicator
container.addEventListener("scroll", refreshPageIndicator, { passive: true });
// Scroll → update page indicator + debounced state save
let _scrollSaveTimer = null;
container.addEventListener("scroll", () => {
refreshPageIndicator();
if (_scrollSaveTimer) clearTimeout(_scrollSaveTimer);
_scrollSaveTimer = setTimeout(sendViewerState, 500);
}, { passive: true });
// Restore saved scroll position (rAF ensures layout is ready)
if (savedScrollTop > 0) {
requestAnimationFrame(() => { container.scrollTop = savedScrollTop; });
}
// Lifecycle: tab hidden → free caches; tab visible → re-reconcile
document.addEventListener("visibilitychange", () => {