Add PDF state persistence
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user