Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
async function _parsePdfOutline(pdf, items, depth) {
|
|
|
|
|
depth = depth || 0;
|
|
|
|
|
const result = [];
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
let href = '';
|
|
|
|
|
if (item.dest) {
|
|
|
|
|
try {
|
|
|
|
|
const dest = typeof item.dest === 'string' ? await pdf.getDestination(item.dest) : item.dest;
|
|
|
|
|
if (dest) {
|
|
|
|
|
const pageIndex = await pdf.getPageIndex(dest[0]);
|
|
|
|
|
href = `#pdf-page-${pageIndex + 1}`;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
}
|
|
|
|
|
result.push({label: item.title || '(untitled)', href, depth});
|
|
|
|
|
if (item.items && item.items.length) {
|
|
|
|
|
result.push(...await _parsePdfOutline(pdf, item.items, depth + 1));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-19 14:57:03 +02:00
|
|
|
// The document being loaded (pdf.js loading task), so opening can be cancelled.
|
|
|
|
|
let _pdfLoadingTask = null;
|
|
|
|
|
// The pages' drawing of the last renderPdf: an observer and its queue (stopped on the next one).
|
|
|
|
|
let _pdfLazy = null;
|
|
|
|
|
|
|
|
|
|
function _pdfStopLazy() {
|
|
|
|
|
if (_pdfLazy) {
|
|
|
|
|
_pdfLazy.stopped = true;
|
|
|
|
|
if (_pdfLazy.observer) _pdfLazy.observer.disconnect();
|
|
|
|
|
_pdfLazy = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Every page gets its place at once (sized like the first page until it is drawn), but only the
|
|
|
|
|
// pages near the view are drawn; ones far away are let go again. A book of 1000 scanned pages
|
|
|
|
|
// opens as fast as one of ten, and doesn't hold 1000 canvases.
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
|
|
|
|
const myGen = ++_pdfRenderGen;
|
2026-10-19 14:57:03 +02:00
|
|
|
_pdfStopLazy();
|
|
|
|
|
let pdf = currentPdfDoc;
|
|
|
|
|
if (!pdf) {
|
|
|
|
|
_pdfLoadingTask = pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))});
|
|
|
|
|
try {
|
|
|
|
|
pdf = await _pdfLoadingTask.promise;
|
|
|
|
|
} finally {
|
|
|
|
|
_pdfLoadingTask = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
if (_pdfRenderGen !== myGen) return null;
|
|
|
|
|
currentPdfDoc = pdf;
|
|
|
|
|
|
|
|
|
|
let pdfTitle = '', pdfAuthor = '';
|
|
|
|
|
try {
|
|
|
|
|
const meta = await pdf.getMetadata();
|
|
|
|
|
pdfTitle = meta.info?.Title?.trim() || '';
|
|
|
|
|
pdfAuthor = meta.info?.Author?.trim() || '';
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
|
|
let toc = [];
|
|
|
|
|
try {
|
|
|
|
|
const outline = await pdf.getOutline();
|
|
|
|
|
if (outline && outline.length) toc = await _parsePdfOutline(pdf, outline);
|
|
|
|
|
} catch (e) {}
|
2026-10-19 14:57:03 +02:00
|
|
|
if (_pdfRenderGen !== myGen) return null;
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
|
|
|
|
|
contentEl.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
const pdfVp = document.createElement('div');
|
|
|
|
|
pdfVp.id = 'pdf-viewport';
|
|
|
|
|
contentEl.appendChild(pdfVp);
|
|
|
|
|
|
|
|
|
|
const containerWidth = readerSettings.pdfSpread
|
|
|
|
|
? contentEl.clientWidth - 32
|
|
|
|
|
: Math.min(contentEl.clientWidth - 32, 900);
|
|
|
|
|
const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth;
|
|
|
|
|
const dpr = window.devicePixelRatio || 1;
|
2026-10-19 14:57:03 +02:00
|
|
|
const scaleFor = vp => (scaleOverride != null ? scaleOverride
|
|
|
|
|
: Math.max(0.5, pageWidth / vp.width) * (readerSettings.pdfZoom / 100));
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
|
2026-10-19 14:57:03 +02:00
|
|
|
// The first page's size stands for all until each is drawn (a scan's pages are alike).
|
|
|
|
|
const first = await pdf.getPage(1);
|
|
|
|
|
if (_pdfRenderGen !== myGen) return null;
|
|
|
|
|
const firstVp = first.getViewport({scale: scaleFor(first.getViewport({scale: 1}))});
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
|
2026-10-19 14:57:03 +02:00
|
|
|
const entries = [];
|
|
|
|
|
let spreadContainer = null;
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
|
wrapper.className = 'pdf-page-wrapper';
|
|
|
|
|
wrapper.id = `pdf-page-${pageNum}`;
|
|
|
|
|
const inner = document.createElement('div');
|
|
|
|
|
inner.className = 'pdf-page-inner';
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
canvas.className = 'pdf-page';
|
2026-10-19 14:57:03 +02:00
|
|
|
canvas.width = 0;
|
|
|
|
|
canvas.height = 0;
|
|
|
|
|
canvas.style.width = firstVp.width + 'px';
|
|
|
|
|
canvas.style.height = firstVp.height + 'px';
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
inner.appendChild(canvas);
|
|
|
|
|
wrapper.appendChild(inner);
|
|
|
|
|
if (!readerSettings.pdfSpread) {
|
|
|
|
|
pdfVp.appendChild(wrapper);
|
|
|
|
|
} else if (pageNum === 1) {
|
|
|
|
|
wrapper.classList.add('pdf-spread-cover');
|
|
|
|
|
pdfVp.appendChild(wrapper);
|
|
|
|
|
spreadContainer = null;
|
|
|
|
|
} else {
|
|
|
|
|
if (pageNum % 2 === 0) {
|
|
|
|
|
spreadContainer = document.createElement('div');
|
|
|
|
|
spreadContainer.className = 'pdf-spread-wrapper';
|
|
|
|
|
pdfVp.appendChild(spreadContainer);
|
|
|
|
|
}
|
|
|
|
|
if (spreadContainer) spreadContainer.appendChild(wrapper);
|
|
|
|
|
else pdfVp.appendChild(wrapper);
|
|
|
|
|
}
|
2026-10-19 14:57:03 +02:00
|
|
|
entries.push({num: pageNum, wrapper, canvas, drawn: false, near: false, task: null});
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pdfTotalPages = pdf.numPages;
|
|
|
|
|
|
|
|
|
|
const pivot = Math.max(1, Math.min(pdf.numPages, pivotPage || 1));
|
|
|
|
|
const pivotEl = document.getElementById(`pdf-page-${pivot}`);
|
|
|
|
|
if (pivotEl && pivot > 1) pivotEl.scrollIntoView({block: 'start'});
|
|
|
|
|
|
2026-10-19 14:57:03 +02:00
|
|
|
const lazy = {stopped: false, observer: null, queue: new Set(), working: false};
|
|
|
|
|
_pdfLazy = lazy;
|
|
|
|
|
const gone = () => lazy.stopped || _pdfRenderGen !== myGen;
|
|
|
|
|
|
|
|
|
|
async function draw(e) {
|
|
|
|
|
const page = await pdf.getPage(e.num);
|
|
|
|
|
if (gone() || !e.near) return;
|
|
|
|
|
const viewport = page.getViewport({scale: scaleFor(page.getViewport({scale: 1}))});
|
|
|
|
|
e.canvas.style.width = viewport.width + 'px';
|
|
|
|
|
e.canvas.style.height = viewport.height + 'px';
|
|
|
|
|
e.canvas.width = Math.round(viewport.width * dpr);
|
|
|
|
|
e.canvas.height = Math.round(viewport.height * dpr);
|
|
|
|
|
const ctx = e.canvas.getContext('2d');
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
ctx.scale(dpr, dpr);
|
2026-10-19 14:57:03 +02:00
|
|
|
e.task = page.render({canvasContext: ctx, viewport});
|
|
|
|
|
try {
|
|
|
|
|
await e.task.promise;
|
|
|
|
|
e.drawn = true;
|
|
|
|
|
} catch (err) { /* cancelled: let go below */ }
|
|
|
|
|
e.task = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function letGo(e) {
|
|
|
|
|
if (e.task) { try { e.task.cancel(); } catch (err) {} }
|
|
|
|
|
if (!e.drawn) return;
|
|
|
|
|
e.drawn = false;
|
|
|
|
|
e.canvas.width = 0; // frees the pixels; the CSS size keeps the page's place
|
|
|
|
|
e.canvas.height = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// One page at a time, the nearest to the current one first.
|
|
|
|
|
async function work() {
|
|
|
|
|
if (lazy.working) return;
|
|
|
|
|
lazy.working = true;
|
|
|
|
|
try {
|
|
|
|
|
while (!gone() && lazy.queue.size) {
|
|
|
|
|
const cur = typeof _currentScrollPage === 'function' && !readerSettings.pdfPaginated
|
|
|
|
|
? _currentScrollPage(contentEl) : (pdfCurrentPage || pivot);
|
|
|
|
|
const next = Array.from(lazy.queue).sort((a, b) => Math.abs(a.num - cur) - Math.abs(b.num - cur))[0];
|
|
|
|
|
lazy.queue.delete(next);
|
|
|
|
|
if (next.near && !next.drawn) await draw(next);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
lazy.working = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const byEl = new Map(entries.map(e => [e.wrapper, e]));
|
|
|
|
|
if (typeof IntersectionObserver === 'function') {
|
|
|
|
|
lazy.observer = new IntersectionObserver(items => {
|
|
|
|
|
for (const it of items) {
|
|
|
|
|
const e = byEl.get(it.target);
|
|
|
|
|
if (!e) continue;
|
|
|
|
|
e.near = it.isIntersecting;
|
|
|
|
|
if (e.near) { if (!e.drawn) lazy.queue.add(e); }
|
|
|
|
|
else { lazy.queue.delete(e); letGo(e); }
|
|
|
|
|
}
|
|
|
|
|
work();
|
|
|
|
|
}, {root: contentEl, rootMargin: '150% 0px'});
|
|
|
|
|
entries.forEach(e => lazy.observer.observe(e.wrapper));
|
|
|
|
|
} else {
|
|
|
|
|
// Very old WebViews: every page, from the pivot on (as before).
|
|
|
|
|
entries.forEach(e => { e.near = true; lazy.queue.add(e); });
|
|
|
|
|
work();
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-19 14:57:03 +02:00
|
|
|
// The book shows as soon as the page it opens at is drawn.
|
|
|
|
|
const pe = entries[pivot - 1];
|
|
|
|
|
pe.near = true;
|
|
|
|
|
if (!pe.drawn) await draw(pe);
|
|
|
|
|
if (gone()) return null;
|
Refactoring Schritt 2: app.js nach Bereichen in static/js/app/ aufgeteilt; sw v106
32 klassische Skripte in der alten Reihenfolge (01-core … 99-init), ohne
Änderung am Code darin: geschnitten nur zwischen Anweisungen, je Datei
'use strict' wie vorher für die ganze Datei. Geprüft (acorn): kein Code, der
beim Laden läuft, braucht einen Namen aus einer späteren Datei. Globale
Funktionen und let/const bleiben global, die App-Schichten (native.js,
native-eink.js, …) umhüllen sie wie bisher. Template und sw.js-SHELL listen
die Dateien; diora-android build_web nimmt static/js rekursiv.
Getestet: 14 Browser-Tests (e2e), Desktop-App (Buch, Lesestand, Leiste im
Reader, Hell/Dunkel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:30:43 +02:00
|
|
|
return {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages};
|
|
|
|
|
}
|