Spaces:
Sleeping
Sleeping
t commited on
Commit ·
7d3748e
1
Parent(s): 6fb2610
fix: switch to bullseye base to resolve wkhtmltox dependencies
Browse files- templates/pdfjs_viewer.html +179 -43
templates/pdfjs_viewer.html
CHANGED
|
@@ -2,11 +2,11 @@
|
|
| 2 |
<html lang="en" dir="ltr">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
| 6 |
<title>{{ pdf_title }}</title>
|
| 7 |
<!-- Bootstrap Icons -->
|
| 8 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
| 9 |
-
<!--
|
| 10 |
<style>
|
| 11 |
/* Mozilla PDF.js Viewer Visual Replication */
|
| 12 |
:root {
|
|
@@ -27,6 +27,7 @@
|
|
| 27 |
* {
|
| 28 |
box-sizing: border-box;
|
| 29 |
user-select: none;
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
body {
|
|
@@ -40,6 +41,8 @@
|
|
| 40 |
display: flex;
|
| 41 |
flex-direction: column;
|
| 42 |
color: var(--text-color);
|
|
|
|
|
|
|
| 43 |
}
|
| 44 |
|
| 45 |
#outerContainer {
|
|
@@ -67,7 +70,7 @@
|
|
| 67 |
flex: 1;
|
| 68 |
overflow-y: auto;
|
| 69 |
padding: 10px;
|
| 70 |
-
position: relative;
|
| 71 |
}
|
| 72 |
|
| 73 |
#sidebarContent::-webkit-scrollbar { width: 10px; }
|
|
@@ -97,7 +100,7 @@
|
|
| 97 |
|
| 98 |
.thumbnail-canvas {
|
| 99 |
display: block;
|
| 100 |
-
width: 100px;
|
| 101 |
height: auto;
|
| 102 |
}
|
| 103 |
|
|
@@ -195,6 +198,9 @@
|
|
| 195 |
right: 0; bottom: 0; left: 0;
|
| 196 |
background-color: var(--body-bg);
|
| 197 |
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=');
|
|
|
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
|
| 200 |
#viewerContainer::-webkit-scrollbar { width: 14px; height: 14px; }
|
|
@@ -208,6 +214,9 @@
|
|
| 208 |
align-items: center;
|
| 209 |
padding: 20px 0;
|
| 210 |
position: relative;
|
|
|
|
|
|
|
|
|
|
| 211 |
}
|
| 212 |
|
| 213 |
.page {
|
|
@@ -246,7 +255,7 @@
|
|
| 246 |
}
|
| 247 |
|
| 248 |
.toast {
|
| 249 |
-
background-color: #2b3035;
|
| 250 |
color: #fff;
|
| 251 |
border-radius: 0.375rem;
|
| 252 |
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
@@ -466,6 +475,124 @@
|
|
| 466 |
toast: document.getElementById('cacheToast')
|
| 467 |
};
|
| 468 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
// ==========================================
|
| 470 |
// INDEXED DB CACHE
|
| 471 |
// ==========================================
|
|
@@ -500,10 +627,7 @@
|
|
| 500 |
|
| 501 |
if (!record) return null;
|
| 502 |
|
| 503 |
-
// Unpack if wrapped (new format) or use raw (legacy)
|
| 504 |
const data = record.data || record;
|
| 505 |
-
|
| 506 |
-
// If it's the new format, update timestamp (async fire-and-forget)
|
| 507 |
if (record.data) {
|
| 508 |
this.touch(url, record);
|
| 509 |
}
|
|
@@ -530,7 +654,6 @@
|
|
| 530 |
const tx = db.transaction(this.storeName, 'readwrite');
|
| 531 |
const store = tx.objectStore(this.storeName);
|
| 532 |
|
| 533 |
-
// Calculate usage and collect entries for eviction
|
| 534 |
let currentSize = 0;
|
| 535 |
const entries = [];
|
| 536 |
|
|
@@ -539,11 +662,10 @@
|
|
| 539 |
req.onsuccess = (e) => {
|
| 540 |
const cursor = e.target.result;
|
| 541 |
if (cursor) {
|
| 542 |
-
// Skip if it's the current file (we are overwriting anyway)
|
| 543 |
if (cursor.key !== url) {
|
| 544 |
const val = cursor.value;
|
| 545 |
const itemSize = (val.data || val).byteLength || 0;
|
| 546 |
-
const itemTime = val.timestamp || 0;
|
| 547 |
currentSize += itemSize;
|
| 548 |
entries.push({ key: cursor.key, size: itemSize, timestamp: itemTime });
|
| 549 |
}
|
|
@@ -555,11 +677,8 @@
|
|
| 555 |
req.onerror = () => resolve();
|
| 556 |
});
|
| 557 |
|
| 558 |
-
// Evict if needed
|
| 559 |
if (currentSize + newSize > this.MAX_TOTAL_SIZE) {
|
| 560 |
-
// Sort oldest first
|
| 561 |
entries.sort((a, b) => a.timestamp - b.timestamp);
|
| 562 |
-
|
| 563 |
while (entries.length > 0 && (currentSize + newSize > this.MAX_TOTAL_SIZE)) {
|
| 564 |
const entry = entries.shift();
|
| 565 |
store.delete(entry.key);
|
|
@@ -568,7 +687,6 @@
|
|
| 568 |
}
|
| 569 |
}
|
| 570 |
|
| 571 |
-
// Save new record
|
| 572 |
const record = { data: data, timestamp: Date.now() };
|
| 573 |
store.put(record, url);
|
| 574 |
|
|
@@ -592,7 +710,7 @@
|
|
| 592 |
elements.toast.classList.remove('show');
|
| 593 |
}
|
| 594 |
|
| 595 |
-
window.hideToast = hideToast;
|
| 596 |
|
| 597 |
// ==========================================
|
| 598 |
// LOADING
|
|
@@ -609,10 +727,9 @@
|
|
| 609 |
let isCached = !!data;
|
| 610 |
|
| 611 |
if (isCached) {
|
| 612 |
-
showToast();
|
| 613 |
}
|
| 614 |
|
| 615 |
-
// Config for fonts to prevent warnings
|
| 616 |
const params = {
|
| 617 |
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdf.js@3.11.174/dist/cmaps/',
|
| 618 |
cMapPacked: true,
|
|
@@ -635,9 +752,7 @@
|
|
| 635 |
state.pdfDoc = await loadingTask.promise;
|
| 636 |
|
| 637 |
if (!isCached) {
|
| 638 |
-
// Save to cache for next time
|
| 639 |
state.pdfDoc.getData().then(pdfData => {
|
| 640 |
-
// Limit cache to 100MB per file
|
| 641 |
if (pdfData.byteLength <= 100 * 1024 * 1024) {
|
| 642 |
PDFCache.put(config.url, pdfData);
|
| 643 |
} else {
|
|
@@ -664,8 +779,9 @@
|
|
| 664 |
elements.viewer.innerHTML = '';
|
| 665 |
state.pages = [];
|
| 666 |
|
| 667 |
-
// Get last viewed page for this PDF
|
| 668 |
-
|
|
|
|
| 669 |
|
| 670 |
for (let i = 1; i <= state.pdfDoc.numPages; i++) {
|
| 671 |
const pageDiv = document.createElement('div');
|
|
@@ -673,6 +789,7 @@
|
|
| 673 |
pageDiv.setAttribute('data-page-number', i);
|
| 674 |
pageDiv.id = `pageContainer${i}`;
|
| 675 |
|
|
|
|
| 676 |
pageDiv.style.width = '612px';
|
| 677 |
pageDiv.style.height = '792px';
|
| 678 |
|
|
@@ -681,9 +798,13 @@
|
|
| 681 |
}
|
| 682 |
|
| 683 |
setupIntersectionObserver();
|
| 684 |
-
generateThumbnails();
|
| 685 |
|
| 686 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
if (lastPage > 1) {
|
| 688 |
setTimeout(() => scrollToPage(lastPage), 100);
|
| 689 |
}
|
|
@@ -716,7 +837,6 @@
|
|
| 716 |
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
|
| 717 |
const pageNum = parseInt(entry.target.getAttribute('data-page-number'));
|
| 718 |
updateUIState(pageNum);
|
| 719 |
-
// Save current page to localStorage
|
| 720 |
PageMemory.saveLastPage(config.pdfIdentifier, pageNum);
|
| 721 |
}
|
| 722 |
});
|
|
@@ -783,7 +903,6 @@
|
|
| 783 |
imgContainer.className = 'thumbnail-image-container';
|
| 784 |
imgContainer.id = `thumb-img-${i}`;
|
| 785 |
|
| 786 |
-
// Placeholder dimensions until render
|
| 787 |
imgContainer.style.width = '100px';
|
| 788 |
imgContainer.style.height = '130px';
|
| 789 |
|
|
@@ -796,14 +915,11 @@
|
|
| 796 |
elements.sidebarContent.appendChild(thumbContainer);
|
| 797 |
}
|
| 798 |
|
| 799 |
-
// Render thumbnails lazily when sidebar is open or idle
|
| 800 |
-
// For now, render them sequentially to avoid UI freeze
|
| 801 |
renderThumbnailsQueue();
|
| 802 |
}
|
| 803 |
|
| 804 |
async function renderThumbnailsQueue() {
|
| 805 |
for (let i = 1; i <= state.pdfDoc.numPages; i++) {
|
| 806 |
-
// Check if we should render (could add visibility check here)
|
| 807 |
await renderSingleThumbnail(i);
|
| 808 |
}
|
| 809 |
}
|
|
@@ -811,7 +927,7 @@
|
|
| 811 |
async function renderSingleThumbnail(i) {
|
| 812 |
try {
|
| 813 |
const page = await state.pdfDoc.getPage(i);
|
| 814 |
-
const viewport = page.getViewport({ scale: 0.2 });
|
| 815 |
const canvas = document.createElement('canvas');
|
| 816 |
canvas.className = 'thumbnail-canvas';
|
| 817 |
canvas.height = viewport.height;
|
|
@@ -837,8 +953,6 @@
|
|
| 837 |
if (num < 1 || num > state.pdfDoc.numPages) return;
|
| 838 |
const pageDiv = document.getElementById(`pageContainer${num}`);
|
| 839 |
if (pageDiv) {
|
| 840 |
-
// FIXED: Use scrollTop instead of scrollIntoView to prevent layout breaking
|
| 841 |
-
// Calculate offset relative to the container
|
| 842 |
elements.container.scrollTop = pageDiv.offsetTop;
|
| 843 |
}
|
| 844 |
}
|
|
@@ -853,17 +967,14 @@
|
|
| 853 |
if (thumb) {
|
| 854 |
thumb.classList.add('selected');
|
| 855 |
|
| 856 |
-
// Manual Scroll Logic for Sidebar (Replaces scrollIntoView)
|
| 857 |
const thumbTop = thumb.offsetTop;
|
| 858 |
const thumbHeight = thumb.clientHeight;
|
| 859 |
const containerHeight = elements.sidebarContent.clientHeight;
|
| 860 |
const containerScroll = elements.sidebarContent.scrollTop;
|
| 861 |
|
| 862 |
-
// If above visible area
|
| 863 |
if (thumbTop < containerScroll) {
|
| 864 |
elements.sidebarContent.scrollTop = thumbTop - 10;
|
| 865 |
}
|
| 866 |
-
// If below visible area
|
| 867 |
else if (thumbTop + thumbHeight > containerScroll + containerHeight) {
|
| 868 |
elements.sidebarContent.scrollTop = thumbTop + thumbHeight - containerHeight + 10;
|
| 869 |
}
|
|
@@ -895,18 +1006,41 @@
|
|
| 895 |
});
|
| 896 |
|
| 897 |
if (elements.zoomIn) elements.zoomIn.addEventListener('click', () => {
|
| 898 |
-
let current = parseFloat(elements.scaleSelect.value)
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
config.currentScaleValue = config.scale.toString();
|
|
|
|
|
|
|
|
|
|
| 903 |
initViewer();
|
| 904 |
});
|
| 905 |
|
| 906 |
if (elements.zoomOut) elements.zoomOut.addEventListener('click', () => {
|
| 907 |
-
if (config.scale <= 0.
|
| 908 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 909 |
config.currentScaleValue = config.scale.toString();
|
|
|
|
|
|
|
|
|
|
| 910 |
initViewer();
|
| 911 |
});
|
| 912 |
|
|
@@ -923,13 +1057,15 @@
|
|
| 923 |
link.click();
|
| 924 |
});
|
| 925 |
|
|
|
|
|
|
|
|
|
|
| 926 |
// Start loading
|
| 927 |
if (typeof pdfjsLib !== 'undefined') {
|
| 928 |
loadPDF();
|
| 929 |
} else {
|
| 930 |
-
// Wait for script load if needed (though position at end of body usually avoids this)
|
| 931 |
window.addEventListener('load', loadPDF);
|
| 932 |
}
|
| 933 |
</script>
|
| 934 |
</body>
|
| 935 |
-
</html>
|
|
|
|
| 2 |
<html lang="en" dir="ltr">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
| 6 |
<title>{{ pdf_title }}</title>
|
| 7 |
<!-- Bootstrap Icons -->
|
| 8 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
| 9 |
+
<!-- Inline Styles -->
|
| 10 |
<style>
|
| 11 |
/* Mozilla PDF.js Viewer Visual Replication */
|
| 12 |
:root {
|
|
|
|
| 27 |
* {
|
| 28 |
box-sizing: border-box;
|
| 29 |
user-select: none;
|
| 30 |
+
-webkit-user-select: none;
|
| 31 |
}
|
| 32 |
|
| 33 |
body {
|
|
|
|
| 41 |
display: flex;
|
| 42 |
flex-direction: column;
|
| 43 |
color: var(--text-color);
|
| 44 |
+
/* Prevent native pull-to-refresh and other touch behaviors */
|
| 45 |
+
overscroll-behavior: none;
|
| 46 |
}
|
| 47 |
|
| 48 |
#outerContainer {
|
|
|
|
| 70 |
flex: 1;
|
| 71 |
overflow-y: auto;
|
| 72 |
padding: 10px;
|
| 73 |
+
position: relative;
|
| 74 |
}
|
| 75 |
|
| 76 |
#sidebarContent::-webkit-scrollbar { width: 10px; }
|
|
|
|
| 100 |
|
| 101 |
.thumbnail-canvas {
|
| 102 |
display: block;
|
| 103 |
+
width: 100px;
|
| 104 |
height: auto;
|
| 105 |
}
|
| 106 |
|
|
|
|
| 198 |
right: 0; bottom: 0; left: 0;
|
| 199 |
background-color: var(--body-bg);
|
| 200 |
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=');
|
| 201 |
+
/* Important for touch handling */
|
| 202 |
+
touch-action: pan-x pan-y;
|
| 203 |
+
-webkit-overflow-scrolling: touch;
|
| 204 |
}
|
| 205 |
|
| 206 |
#viewerContainer::-webkit-scrollbar { width: 14px; height: 14px; }
|
|
|
|
| 214 |
align-items: center;
|
| 215 |
padding: 20px 0;
|
| 216 |
position: relative;
|
| 217 |
+
/* Enhance hardware acceleration for smoother pinch */
|
| 218 |
+
will-change: transform;
|
| 219 |
+
transform-origin: top center;
|
| 220 |
}
|
| 221 |
|
| 222 |
.page {
|
|
|
|
| 255 |
}
|
| 256 |
|
| 257 |
.toast {
|
| 258 |
+
background-color: #2b3035;
|
| 259 |
color: #fff;
|
| 260 |
border-radius: 0.375rem;
|
| 261 |
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
|
|
| 475 |
toast: document.getElementById('cacheToast')
|
| 476 |
};
|
| 477 |
|
| 478 |
+
// ==========================================
|
| 479 |
+
// PINCH TO ZOOM MODULE
|
| 480 |
+
// ==========================================
|
| 481 |
+
const PinchZoom = {
|
| 482 |
+
state: {
|
| 483 |
+
active: false,
|
| 484 |
+
startDist: 0,
|
| 485 |
+
startScale: 1,
|
| 486 |
+
lastRatio: 1
|
| 487 |
+
},
|
| 488 |
+
|
| 489 |
+
init() {
|
| 490 |
+
// Attach listeners to the container to capture gestures anywhere on the viewer
|
| 491 |
+
elements.container.addEventListener('touchstart', this.onStart.bind(this), { passive: false });
|
| 492 |
+
elements.container.addEventListener('touchmove', this.onMove.bind(this), { passive: false });
|
| 493 |
+
elements.container.addEventListener('touchend', this.onEnd.bind(this));
|
| 494 |
+
},
|
| 495 |
+
|
| 496 |
+
getDistance(e) {
|
| 497 |
+
return Math.hypot(
|
| 498 |
+
e.touches[0].clientX - e.touches[1].clientX,
|
| 499 |
+
e.touches[0].clientY - e.touches[1].clientY
|
| 500 |
+
);
|
| 501 |
+
},
|
| 502 |
+
|
| 503 |
+
getCenter(e) {
|
| 504 |
+
return {
|
| 505 |
+
x: (e.touches[0].clientX + e.touches[1].clientX) / 2,
|
| 506 |
+
y: (e.touches[0].clientY + e.touches[1].clientY) / 2
|
| 507 |
+
};
|
| 508 |
+
},
|
| 509 |
+
|
| 510 |
+
onStart(e) {
|
| 511 |
+
if (e.touches.length === 2) {
|
| 512 |
+
e.preventDefault(); // Prevent native browser zoom/pan
|
| 513 |
+
this.state.active = true;
|
| 514 |
+
this.state.startDist = this.getDistance(e);
|
| 515 |
+
this.state.startScale = config.scale;
|
| 516 |
+
this.state.lastRatio = 1;
|
| 517 |
+
|
| 518 |
+
// Calculate transform origin based on finger position relative to viewer
|
| 519 |
+
const center = this.getCenter(e);
|
| 520 |
+
const rect = elements.viewer.getBoundingClientRect();
|
| 521 |
+
|
| 522 |
+
// The viewer might be scrolled, getBoundingClientRect handles that
|
| 523 |
+
const originX = center.x - rect.left;
|
| 524 |
+
const originY = center.y - rect.top;
|
| 525 |
+
|
| 526 |
+
// Set transform origin so we zoom into the space between fingers
|
| 527 |
+
elements.viewer.style.transformOrigin = `${originX}px ${originY}px`;
|
| 528 |
+
elements.viewer.style.transition = 'none';
|
| 529 |
+
}
|
| 530 |
+
},
|
| 531 |
+
|
| 532 |
+
onMove(e) {
|
| 533 |
+
if (this.state.active && e.touches.length === 2) {
|
| 534 |
+
e.preventDefault();
|
| 535 |
+
const dist = this.getDistance(e);
|
| 536 |
+
const ratio = dist / this.state.startDist;
|
| 537 |
+
this.state.lastRatio = ratio;
|
| 538 |
+
|
| 539 |
+
// Apply temporary visual scale using CSS transform
|
| 540 |
+
// This is performant (60fps) compared to re-rendering canvas
|
| 541 |
+
elements.viewer.style.transform = `scale(${ratio})`;
|
| 542 |
+
}
|
| 543 |
+
},
|
| 544 |
+
|
| 545 |
+
onEnd(e) {
|
| 546 |
+
if (this.state.active && e.touches.length < 2) {
|
| 547 |
+
this.state.active = false;
|
| 548 |
+
|
| 549 |
+
// Calculate new final scale
|
| 550 |
+
let newScale = this.state.startScale * this.state.lastRatio;
|
| 551 |
+
|
| 552 |
+
// Clamp scale to reasonable limits (0.25x to 5.0x)
|
| 553 |
+
newScale = Math.max(0.25, Math.min(newScale, 5.0));
|
| 554 |
+
|
| 555 |
+
// Reset CSS transform
|
| 556 |
+
elements.viewer.style.transform = 'none';
|
| 557 |
+
elements.viewer.style.transformOrigin = 'center top';
|
| 558 |
+
|
| 559 |
+
// Apply new scale to config
|
| 560 |
+
config.scale = newScale;
|
| 561 |
+
config.currentScaleValue = newScale.toString();
|
| 562 |
+
|
| 563 |
+
// Update UI Dropdown
|
| 564 |
+
this.updateScaleDropdown(newScale);
|
| 565 |
+
|
| 566 |
+
// Re-render PDF at new resolution (crisp text)
|
| 567 |
+
initViewer();
|
| 568 |
+
}
|
| 569 |
+
},
|
| 570 |
+
|
| 571 |
+
updateScaleDropdown(scaleVal) {
|
| 572 |
+
// Check if value exists in dropdown
|
| 573 |
+
const rounded = Math.round(scaleVal * 100) / 100;
|
| 574 |
+
let exists = false;
|
| 575 |
+
|
| 576 |
+
// Try to find exact match
|
| 577 |
+
for(let option of elements.scaleSelect.options) {
|
| 578 |
+
if (Math.abs(parseFloat(option.value) - scaleVal) < 0.05) {
|
| 579 |
+
elements.scaleSelect.value = option.value;
|
| 580 |
+
exists = true;
|
| 581 |
+
break;
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
// If no match, add custom option
|
| 586 |
+
if (!exists) {
|
| 587 |
+
const opt = document.createElement('option');
|
| 588 |
+
opt.value = scaleVal;
|
| 589 |
+
opt.textContent = `${Math.round(scaleVal * 100)}%`;
|
| 590 |
+
elements.scaleSelect.appendChild(opt);
|
| 591 |
+
elements.scaleSelect.value = scaleVal;
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
};
|
| 595 |
+
|
| 596 |
// ==========================================
|
| 597 |
// INDEXED DB CACHE
|
| 598 |
// ==========================================
|
|
|
|
| 627 |
|
| 628 |
if (!record) return null;
|
| 629 |
|
|
|
|
| 630 |
const data = record.data || record;
|
|
|
|
|
|
|
| 631 |
if (record.data) {
|
| 632 |
this.touch(url, record);
|
| 633 |
}
|
|
|
|
| 654 |
const tx = db.transaction(this.storeName, 'readwrite');
|
| 655 |
const store = tx.objectStore(this.storeName);
|
| 656 |
|
|
|
|
| 657 |
let currentSize = 0;
|
| 658 |
const entries = [];
|
| 659 |
|
|
|
|
| 662 |
req.onsuccess = (e) => {
|
| 663 |
const cursor = e.target.result;
|
| 664 |
if (cursor) {
|
|
|
|
| 665 |
if (cursor.key !== url) {
|
| 666 |
const val = cursor.value;
|
| 667 |
const itemSize = (val.data || val).byteLength || 0;
|
| 668 |
+
const itemTime = val.timestamp || 0;
|
| 669 |
currentSize += itemSize;
|
| 670 |
entries.push({ key: cursor.key, size: itemSize, timestamp: itemTime });
|
| 671 |
}
|
|
|
|
| 677 |
req.onerror = () => resolve();
|
| 678 |
});
|
| 679 |
|
|
|
|
| 680 |
if (currentSize + newSize > this.MAX_TOTAL_SIZE) {
|
|
|
|
| 681 |
entries.sort((a, b) => a.timestamp - b.timestamp);
|
|
|
|
| 682 |
while (entries.length > 0 && (currentSize + newSize > this.MAX_TOTAL_SIZE)) {
|
| 683 |
const entry = entries.shift();
|
| 684 |
store.delete(entry.key);
|
|
|
|
| 687 |
}
|
| 688 |
}
|
| 689 |
|
|
|
|
| 690 |
const record = { data: data, timestamp: Date.now() };
|
| 691 |
store.put(record, url);
|
| 692 |
|
|
|
|
| 710 |
elements.toast.classList.remove('show');
|
| 711 |
}
|
| 712 |
|
| 713 |
+
window.hideToast = hideToast;
|
| 714 |
|
| 715 |
// ==========================================
|
| 716 |
// LOADING
|
|
|
|
| 727 |
let isCached = !!data;
|
| 728 |
|
| 729 |
if (isCached) {
|
| 730 |
+
showToast();
|
| 731 |
}
|
| 732 |
|
|
|
|
| 733 |
const params = {
|
| 734 |
cMapUrl: 'https://cdn.jsdelivr.net/npm/pdf.js@3.11.174/dist/cmaps/',
|
| 735 |
cMapPacked: true,
|
|
|
|
| 752 |
state.pdfDoc = await loadingTask.promise;
|
| 753 |
|
| 754 |
if (!isCached) {
|
|
|
|
| 755 |
state.pdfDoc.getData().then(pdfData => {
|
|
|
|
| 756 |
if (pdfData.byteLength <= 100 * 1024 * 1024) {
|
| 757 |
PDFCache.put(config.url, pdfData);
|
| 758 |
} else {
|
|
|
|
| 779 |
elements.viewer.innerHTML = '';
|
| 780 |
state.pages = [];
|
| 781 |
|
| 782 |
+
// Get last viewed page for this PDF to restore scroll position
|
| 783 |
+
// If just loaded, use storage. If zoomed, use current state.pageNum
|
| 784 |
+
const lastPage = state.pageNum > 1 ? state.pageNum : PageMemory.getLastPage(config.pdfIdentifier);
|
| 785 |
|
| 786 |
for (let i = 1; i <= state.pdfDoc.numPages; i++) {
|
| 787 |
const pageDiv = document.createElement('div');
|
|
|
|
| 789 |
pageDiv.setAttribute('data-page-number', i);
|
| 790 |
pageDiv.id = `pageContainer${i}`;
|
| 791 |
|
| 792 |
+
// Initial size guess (letter) until rendered
|
| 793 |
pageDiv.style.width = '612px';
|
| 794 |
pageDiv.style.height = '792px';
|
| 795 |
|
|
|
|
| 798 |
}
|
| 799 |
|
| 800 |
setupIntersectionObserver();
|
|
|
|
| 801 |
|
| 802 |
+
if (!state.thumbnailsRendered) {
|
| 803 |
+
generateThumbnails();
|
| 804 |
+
state.thumbnailsRendered = true;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
// Restore scroll position
|
| 808 |
if (lastPage > 1) {
|
| 809 |
setTimeout(() => scrollToPage(lastPage), 100);
|
| 810 |
}
|
|
|
|
| 837 |
if (entry.isIntersecting && entry.intersectionRatio > 0.5) {
|
| 838 |
const pageNum = parseInt(entry.target.getAttribute('data-page-number'));
|
| 839 |
updateUIState(pageNum);
|
|
|
|
| 840 |
PageMemory.saveLastPage(config.pdfIdentifier, pageNum);
|
| 841 |
}
|
| 842 |
});
|
|
|
|
| 903 |
imgContainer.className = 'thumbnail-image-container';
|
| 904 |
imgContainer.id = `thumb-img-${i}`;
|
| 905 |
|
|
|
|
| 906 |
imgContainer.style.width = '100px';
|
| 907 |
imgContainer.style.height = '130px';
|
| 908 |
|
|
|
|
| 915 |
elements.sidebarContent.appendChild(thumbContainer);
|
| 916 |
}
|
| 917 |
|
|
|
|
|
|
|
| 918 |
renderThumbnailsQueue();
|
| 919 |
}
|
| 920 |
|
| 921 |
async function renderThumbnailsQueue() {
|
| 922 |
for (let i = 1; i <= state.pdfDoc.numPages; i++) {
|
|
|
|
| 923 |
await renderSingleThumbnail(i);
|
| 924 |
}
|
| 925 |
}
|
|
|
|
| 927 |
async function renderSingleThumbnail(i) {
|
| 928 |
try {
|
| 929 |
const page = await state.pdfDoc.getPage(i);
|
| 930 |
+
const viewport = page.getViewport({ scale: 0.2 });
|
| 931 |
const canvas = document.createElement('canvas');
|
| 932 |
canvas.className = 'thumbnail-canvas';
|
| 933 |
canvas.height = viewport.height;
|
|
|
|
| 953 |
if (num < 1 || num > state.pdfDoc.numPages) return;
|
| 954 |
const pageDiv = document.getElementById(`pageContainer${num}`);
|
| 955 |
if (pageDiv) {
|
|
|
|
|
|
|
| 956 |
elements.container.scrollTop = pageDiv.offsetTop;
|
| 957 |
}
|
| 958 |
}
|
|
|
|
| 967 |
if (thumb) {
|
| 968 |
thumb.classList.add('selected');
|
| 969 |
|
|
|
|
| 970 |
const thumbTop = thumb.offsetTop;
|
| 971 |
const thumbHeight = thumb.clientHeight;
|
| 972 |
const containerHeight = elements.sidebarContent.clientHeight;
|
| 973 |
const containerScroll = elements.sidebarContent.scrollTop;
|
| 974 |
|
|
|
|
| 975 |
if (thumbTop < containerScroll) {
|
| 976 |
elements.sidebarContent.scrollTop = thumbTop - 10;
|
| 977 |
}
|
|
|
|
| 978 |
else if (thumbTop + thumbHeight > containerScroll + containerHeight) {
|
| 979 |
elements.sidebarContent.scrollTop = thumbTop + thumbHeight - containerHeight + 10;
|
| 980 |
}
|
|
|
|
| 1006 |
});
|
| 1007 |
|
| 1008 |
if (elements.zoomIn) elements.zoomIn.addEventListener('click', () => {
|
| 1009 |
+
let current = parseFloat(elements.scaleSelect.value);
|
| 1010 |
+
if(isNaN(current)) current = config.scale;
|
| 1011 |
+
const newScale = (Math.floor(current * 4) + 1) / 4; // Step to next 0.25 increment
|
| 1012 |
+
|
| 1013 |
+
// Check if current was page-fit or similar
|
| 1014 |
+
if(isNaN(parseFloat(elements.scaleSelect.value))) {
|
| 1015 |
+
config.scale = config.scale + 0.25;
|
| 1016 |
+
} else {
|
| 1017 |
+
config.scale = newScale;
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
config.currentScaleValue = config.scale.toString();
|
| 1021 |
+
|
| 1022 |
+
// Sync Dropdown
|
| 1023 |
+
PinchZoom.updateScaleDropdown(config.scale);
|
| 1024 |
initViewer();
|
| 1025 |
});
|
| 1026 |
|
| 1027 |
if (elements.zoomOut) elements.zoomOut.addEventListener('click', () => {
|
| 1028 |
+
if (config.scale <= 0.25) return;
|
| 1029 |
+
|
| 1030 |
+
let current = parseFloat(elements.scaleSelect.value);
|
| 1031 |
+
if(isNaN(current)) current = config.scale;
|
| 1032 |
+
const newScale = (Math.ceil(current * 4) - 1) / 4; // Step to prev 0.25 increment
|
| 1033 |
+
|
| 1034 |
+
if(isNaN(parseFloat(elements.scaleSelect.value))) {
|
| 1035 |
+
config.scale = Math.max(0.25, config.scale - 0.25);
|
| 1036 |
+
} else {
|
| 1037 |
+
config.scale = Math.max(0.25, newScale);
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
config.currentScaleValue = config.scale.toString();
|
| 1041 |
+
|
| 1042 |
+
// Sync Dropdown
|
| 1043 |
+
PinchZoom.updateScaleDropdown(config.scale);
|
| 1044 |
initViewer();
|
| 1045 |
});
|
| 1046 |
|
|
|
|
| 1057 |
link.click();
|
| 1058 |
});
|
| 1059 |
|
| 1060 |
+
// Initialize Pinch Zoom
|
| 1061 |
+
PinchZoom.init();
|
| 1062 |
+
|
| 1063 |
// Start loading
|
| 1064 |
if (typeof pdfjsLib !== 'undefined') {
|
| 1065 |
loadPDF();
|
| 1066 |
} else {
|
|
|
|
| 1067 |
window.addEventListener('load', loadPDF);
|
| 1068 |
}
|
| 1069 |
</script>
|
| 1070 |
</body>
|
| 1071 |
+
</html>
|