PDFs: nur Seiten nahe der Ansicht zeichnen (ferne wieder freigeben), sofort öffnen; Öffnen lässt sich abbrechen (Knopf, Schließen, anderes Buch)
Ein MEW-Band mit 1000 Seiten bereitete vorher jede Seite vor und zeichnete alle, bevor er sich zeigte – Minuten und Gigabytes. e2e/pdf.spec.js mit einem 400-Seiten-PDF (e2e/make_pdf.py, pypdf). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7f9d6fc22a
commit
46e47df6d0
7 changed files with 251 additions and 37 deletions
|
|
@ -45,6 +45,8 @@ User.objects.create_user(${JSON.stringify(TEST_USER.username)}, "", ${JSON.strin
|
|||
);
|
||||
|
||||
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
||||
// A big PDF (e2e/make_pdf.py): opening draws only the pages near the view.
|
||||
execFileSync(PYTHON, [path.join(__dirname, 'make_pdf.py'), path.join(AUTH_DIR, 'big.pdf'), '400'], { stdio: 'inherit' });
|
||||
// The book the reader tests read (e2e/make_epub.py).
|
||||
execFileSync(PYTHON, [path.join(__dirname, 'make_epub.py'), path.join(AUTH_DIR, 'book.epub')], { stdio: 'inherit' });
|
||||
// Two more for uploading several at once (navigation.spec.js).
|
||||
|
|
|
|||
12
e2e/make_pdf.py
Normal file
12
e2e/make_pdf.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
"""A PDF of many pages for the reader tests (big scanned books): make_pdf.py OUT [PAGES]."""
|
||||
import sys
|
||||
|
||||
from pypdf import PdfWriter
|
||||
|
||||
out, pages = sys.argv[1], int(sys.argv[2]) if len(sys.argv) > 2 else 400
|
||||
w = PdfWriter()
|
||||
for _ in range(pages):
|
||||
w.add_blank_page(width=420, height=595)
|
||||
w.add_metadata({'/Title': 'Dicker Band', '/Author': 'diora'})
|
||||
with open(out, 'wb') as f:
|
||||
w.write(f)
|
||||
71
e2e/pdf.spec.js
Normal file
71
e2e/pdf.spec.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// A big PDF (a scanned volume of 400 pages, e2e/make_pdf.py): it opens at once, only the
|
||||
// pages near the view are drawn, the end can be reached, and opening can be cancelled.
|
||||
const path = require('path');
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.use({ storageState: path.join(__dirname, '.auth', 'user.json') });
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
const PDF = path.join(__dirname, '.auth', 'big.pdf');
|
||||
let bookId = null;
|
||||
|
||||
let pageErrors = [];
|
||||
test.beforeEach(({ page }) => {
|
||||
pageErrors = [];
|
||||
page.on('pageerror', e => pageErrors.push(String(e)));
|
||||
});
|
||||
test.afterEach(() => {
|
||||
expect(pageErrors, 'errors in the page').toEqual([]);
|
||||
});
|
||||
|
||||
async function ready(page) {
|
||||
await page.goto('/#books');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForFunction(() => typeof openBook === 'function' && Array.isArray(_lastBookListData));
|
||||
}
|
||||
|
||||
const drawn = page => page.evaluate(() =>
|
||||
[...document.querySelectorAll('#reader-content canvas.pdf-page')].filter(c => c.width > 0).length);
|
||||
|
||||
test('the big PDF goes in', async ({ page }) => {
|
||||
await ready(page);
|
||||
await page.locator('#book-file-input').setInputFiles(PDF);
|
||||
await page.waitForFunction(() => _lastBookListData.some(b => b.title === 'Dicker Band'), null, { timeout: 30000 });
|
||||
bookId = await page.evaluate(() => _lastBookListData.find(b => b.title === 'Dicker Band').id);
|
||||
});
|
||||
|
||||
test('it opens with every page placed, only those near the view drawn', async ({ page }) => {
|
||||
await ready(page);
|
||||
await page.evaluate(id => openBook(id), bookId);
|
||||
await page.waitForFunction(() => _readPos && _readPos.ready, null, { timeout: 20000 });
|
||||
await expect(page.locator('#reader-content .pdf-page-wrapper')).toHaveCount(400);
|
||||
await expect(page.locator('.pdf-loading-overlay')).toHaveCount(0);
|
||||
await expect.poll(() => drawn(page)).toBeGreaterThan(0);
|
||||
expect(await drawn(page)).toBeLessThan(30);
|
||||
// To the end: the last page gets drawn, the first ones are let go.
|
||||
await page.evaluate(() => { const c = document.getElementById('reader-content'); c.scrollTop = c.scrollHeight; });
|
||||
await expect.poll(() => page.evaluate(() => document.querySelector('#pdf-page-400 canvas').width > 0)).toBe(true);
|
||||
await expect.poll(() => page.evaluate(() => document.querySelector('#pdf-page-1 canvas').width)).toBe(0);
|
||||
expect(await drawn(page)).toBeLessThan(30);
|
||||
await page.evaluate(() => closeReader());
|
||||
});
|
||||
|
||||
test('opening can be cancelled, and another book opens after it', async ({ page }) => {
|
||||
await ready(page);
|
||||
// Slow the file down so the opening is still under way.
|
||||
await page.route(`**/books/${bookId}/data/`, async route => {
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
await route.continue().catch(() => {});
|
||||
});
|
||||
const p = page.evaluate(id => openBook(id).then(() => 'done', e => 'error ' + e), bookId);
|
||||
await expect(page.locator('.reader-opening-cancel')).toBeVisible({ timeout: 5000 });
|
||||
await page.locator('.reader-opening-cancel').click();
|
||||
await expect(page.locator('#reader-overlay')).toBeHidden();
|
||||
expect(await p).toBe('done');
|
||||
await page.unroute(`**/books/${bookId}/data/`);
|
||||
// Opening it again works, all the way.
|
||||
await page.evaluate(id => openBook(id), bookId);
|
||||
await page.waitForFunction(() => _readPos && _readPos.ready, null, { timeout: 20000 });
|
||||
await page.evaluate(() => closeReader());
|
||||
await page.request.post(`/books/${bookId}/delete/`);
|
||||
});
|
||||
|
|
@ -1846,6 +1846,7 @@ html.nui-tts-on body.reader-immersive .reader-overlay { bottom: var(--bar-h); }
|
|||
animation: pdf-spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes pdf-spin { to { transform: rotate(360deg); } }
|
||||
.reader-opening { display: flex; flex-direction: column; align-items: center; gap: 16px; }
|
||||
|
||||
/* PDF two-page spread */
|
||||
.pdf-spread-wrapper { display:flex; flex-direction:row; gap:8px; justify-content:center; margin-bottom:1rem; }
|
||||
|
|
|
|||
|
|
@ -22,9 +22,34 @@ async function _parsePdfOutline(pdf, items, depth) {
|
|||
return result;
|
||||
}
|
||||
|
||||
// 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.
|
||||
async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
||||
const myGen = ++_pdfRenderGen;
|
||||
const pdf = currentPdfDoc || await pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))}).promise;
|
||||
_pdfStopLazy();
|
||||
let pdf = currentPdfDoc;
|
||||
if (!pdf) {
|
||||
_pdfLoadingTask = pdfjsLib.getDocument({data: new Uint8Array(arrayBuffer.slice(0))});
|
||||
try {
|
||||
pdf = await _pdfLoadingTask.promise;
|
||||
} finally {
|
||||
_pdfLoadingTask = null;
|
||||
}
|
||||
}
|
||||
if (_pdfRenderGen !== myGen) return null;
|
||||
currentPdfDoc = pdf;
|
||||
|
||||
|
|
@ -40,6 +65,7 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
|||
const outline = await pdf.getOutline();
|
||||
if (outline && outline.length) toc = await _parsePdfOutline(pdf, outline);
|
||||
} catch (e) {}
|
||||
if (_pdfRenderGen !== myGen) return null;
|
||||
|
||||
contentEl.innerHTML = '';
|
||||
|
||||
|
|
@ -52,35 +78,30 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
|||
: Math.min(contentEl.clientWidth - 32, 900);
|
||||
const pageWidth = readerSettings.pdfSpread ? (containerWidth - 8) / 2 : containerWidth;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const scaleFor = vp => (scaleOverride != null ? scaleOverride
|
||||
: Math.max(0.5, pageWidth / vp.width) * (readerSettings.pdfZoom / 100));
|
||||
|
||||
// Pass 1: build DOM structure and size all canvases (no pixel rendering yet)
|
||||
const pageEntries = [];
|
||||
// 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}))});
|
||||
|
||||
const entries = [];
|
||||
let spreadContainer = null;
|
||||
|
||||
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
||||
if (_pdfRenderGen !== myGen) { contentEl.innerHTML = ''; return null; }
|
||||
const page = await pdf.getPage(pageNum);
|
||||
const naturalVp = page.getViewport({scale: 1});
|
||||
const scale = scaleOverride != null ? scaleOverride
|
||||
: Math.max(0.5, pageWidth / naturalVp.width) * (readerSettings.pdfZoom / 100);
|
||||
const viewport = page.getViewport({scale});
|
||||
|
||||
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';
|
||||
canvas.width = Math.round(viewport.width * dpr);
|
||||
canvas.height = Math.round(viewport.height * dpr);
|
||||
canvas.style.width = viewport.width + 'px';
|
||||
canvas.style.height = viewport.height + 'px';
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
canvas.style.width = firstVp.width + 'px';
|
||||
canvas.style.height = firstVp.height + 'px';
|
||||
inner.appendChild(canvas);
|
||||
wrapper.appendChild(inner);
|
||||
|
||||
if (!readerSettings.pdfSpread) {
|
||||
pdfVp.appendChild(wrapper);
|
||||
} else if (pageNum === 1) {
|
||||
|
|
@ -96,29 +117,85 @@ async function renderPdf(arrayBuffer, contentEl, scaleOverride, pivotPage) {
|
|||
if (spreadContainer) spreadContainer.appendChild(wrapper);
|
||||
else pdfVp.appendChild(wrapper);
|
||||
}
|
||||
|
||||
pageEntries.push({page, canvas, viewport});
|
||||
entries.push({num: pageNum, wrapper, canvas, drawn: false, near: false, task: null});
|
||||
}
|
||||
|
||||
pdfTotalPages = pdf.numPages;
|
||||
|
||||
// Scroll to pivot page immediately — DOM and canvas sizes are set
|
||||
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'});
|
||||
|
||||
// Pass 2: render pixels from pivot outward (pivot→end, then pivot-1→start)
|
||||
const order = [];
|
||||
for (let i = pivot - 1; i < pdf.numPages; i++) order.push(i);
|
||||
for (let i = pivot - 2; i >= 0; i--) order.push(i);
|
||||
const lazy = {stopped: false, observer: null, queue: new Set(), working: false};
|
||||
_pdfLazy = lazy;
|
||||
const gone = () => lazy.stopped || _pdfRenderGen !== myGen;
|
||||
|
||||
for (const idx of order) {
|
||||
if (_pdfRenderGen !== myGen) { contentEl.innerHTML = ''; return null; }
|
||||
const {page, canvas, viewport} = pageEntries[idx];
|
||||
const ctx = canvas.getContext('2d');
|
||||
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');
|
||||
ctx.scale(dpr, dpr);
|
||||
await page.render({canvasContext: ctx, viewport}).promise;
|
||||
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();
|
||||
}
|
||||
|
||||
// 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;
|
||||
return {title: pdfTitle, author: pdfAuthor, toc, numPages: pdf.numPages};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,10 +47,34 @@ function exitReaderImmersiveMode() {
|
|||
// progress saved from that doubled DOM had block indices beyond the real book,
|
||||
// which then out-ranked every real position on the server.
|
||||
let _openBookInFlight = null;
|
||||
// The opening under way: {cancelled, ctrl} — "Abbrechen", closing the reader or opening another
|
||||
// book end it (docs: a big scanned PDF used to take minutes and could not be left).
|
||||
let _openToken = null;
|
||||
|
||||
function cancelBookOpening() {
|
||||
const t = _openToken;
|
||||
if (!t || t.cancelled) return false;
|
||||
t.cancelled = true;
|
||||
try { t.ctrl.abort(); } catch (e) {}
|
||||
_pdfRenderGen++;
|
||||
if (typeof _pdfStopLazy === 'function') _pdfStopLazy();
|
||||
if (typeof _pdfLoadingTask !== 'undefined' && _pdfLoadingTask) { try { _pdfLoadingTask.destroy(); } catch (e) {} }
|
||||
document.querySelectorAll('.pdf-loading-overlay').forEach(el => el.remove());
|
||||
const overlay = $('reader-overlay');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
const contentEl = $('reader-content');
|
||||
if (contentEl) contentEl.innerHTML = '';
|
||||
currentPdfDoc = null;
|
||||
currentPdfBuffer = null;
|
||||
_readPos = null;
|
||||
return true;
|
||||
}
|
||||
window.cancelBookOpening = cancelBookOpening;
|
||||
|
||||
async function openBook(bookId) {
|
||||
if (_openBookInFlight) {
|
||||
if (_openBookInFlight.bookId === bookId) return _openBookInFlight.promise;
|
||||
cancelBookOpening(); // another book: the one still loading is left
|
||||
await _openBookInFlight.promise.catch(() => {});
|
||||
}
|
||||
const promise = _openBookOnce(bookId);
|
||||
|
|
@ -73,6 +97,16 @@ async function _openBookOnce(bookId) {
|
|||
titleEl.textContent = 'Lade …';
|
||||
contentEl.innerHTML = '';
|
||||
overlay.style.display = '';
|
||||
const tok = {cancelled: false, ctrl: new AbortController()};
|
||||
_openToken = tok;
|
||||
// A moment later (not for a book that opens at once): the spinner with "Abbrechen".
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.className = 'pdf-loading-overlay';
|
||||
loadingEl.innerHTML = '<div class="reader-opening"><span class="pdf-loading-spinner"></span>' +
|
||||
'<button type="button" class="btn btn-sm reader-opening-cancel">Abbrechen</button></div>';
|
||||
loadingEl.querySelector('button').addEventListener('click', () => cancelBookOpening());
|
||||
const showLoading = setTimeout(() => { if (!tok.cancelled) overlay.appendChild(loadingEl); }, 400);
|
||||
const left = () => tok.cancelled;
|
||||
|
||||
try {
|
||||
loadReaderSettings(bookId);
|
||||
|
|
@ -80,13 +114,15 @@ async function _openBookOnce(bookId) {
|
|||
const key = await getOrCreateEncKey();
|
||||
let data_ct, data_iv;
|
||||
const cached = await _getCachedBook(bookId);
|
||||
if (left()) return;
|
||||
if (cached) {
|
||||
({data_ct, data_iv} = cached);
|
||||
} else {
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(`/books/${bookId}/data/`);
|
||||
res = await fetch(`/books/${bookId}/data/`, {signal: tok.ctrl.signal});
|
||||
} catch {
|
||||
if (left()) return;
|
||||
// offline and book not cached yet
|
||||
overlay.style.display = 'none';
|
||||
const toast = document.createElement('div');
|
||||
|
|
@ -97,9 +133,11 @@ async function _openBookOnce(bookId) {
|
|||
return;
|
||||
}
|
||||
({data_ct, data_iv} = await res.json());
|
||||
if (left()) return;
|
||||
_setCachedBook(bookId, data_ct, data_iv); // fire-and-forget
|
||||
}
|
||||
const plain = await decryptBytes(key, data_iv, data_ct);
|
||||
if (left()) return;
|
||||
|
||||
// Revoke any previous image blob URLs
|
||||
for (const url of Object.values(currentImageMap)) URL.revokeObjectURL(url);
|
||||
|
|
@ -114,12 +152,8 @@ async function _openBookOnce(bookId) {
|
|||
|
||||
if (isPdfBook) {
|
||||
currentPdfDoc = null; // reset so renderPdf creates fresh doc
|
||||
const loadingEl = document.createElement('div');
|
||||
loadingEl.className = 'pdf-loading-overlay';
|
||||
loadingEl.innerHTML = '<span class="pdf-loading-spinner"></span>';
|
||||
overlay.appendChild(loadingEl);
|
||||
const result = await renderPdf(plain, contentEl);
|
||||
loadingEl.remove();
|
||||
if (left() || !result) return;
|
||||
title = result.title || title;
|
||||
author = result.author || author;
|
||||
toc = result.toc;
|
||||
|
|
@ -130,6 +164,7 @@ async function _openBookOnce(bookId) {
|
|||
// Prepared before (same book file, same preparation code): only the images to unpack.
|
||||
const prepared = await _getParsedBook(bookId, data_iv, key);
|
||||
const result = prepared ? await _restoreParsedBook(prepared, plain) : await parseEpub(plain);
|
||||
if (left()) return;
|
||||
if (!prepared) {
|
||||
// Kept for the next open once the book is on screen (encrypting takes a moment).
|
||||
setTimeout(() => _setParsedBook(bookId, data_iv, key, result), 3000);
|
||||
|
|
@ -150,12 +185,19 @@ async function _openBookOnce(bookId) {
|
|||
} while (i < result.chapters.length && performance.now() - start < 50);
|
||||
contentEl.appendChild(frag);
|
||||
if (i < result.chapters.length) await new Promise(r => setTimeout(r, 0));
|
||||
if (left()) return;
|
||||
}
|
||||
// On screen: no more need to offer "Abbrechen".
|
||||
clearTimeout(showLoading);
|
||||
loadingEl.remove();
|
||||
// Used to decide whole-percent vs one-decimal display further down —
|
||||
// see _formatBookPercent().
|
||||
_currentBookCharCount = contentEl.textContent.length;
|
||||
}
|
||||
|
||||
clearTimeout(showLoading);
|
||||
loadingEl.remove();
|
||||
if (_openToken === tok) _openToken = null;
|
||||
currentBookToc = toc;
|
||||
titleEl.textContent = title + (author ? ` — ${author}` : '');
|
||||
|
||||
|
|
@ -364,9 +406,14 @@ async function _openBookOnce(bookId) {
|
|||
if (typeof _issueReaderOpened === 'function') _issueReaderOpened(bookId, isPdf);
|
||||
|
||||
} catch (e) {
|
||||
if (left()) return; // cancelled: the half-loaded document may complain, that's all
|
||||
console.error('openBook failed:', e);
|
||||
overlay.style.display = 'none';
|
||||
markBookBroken(bookId);
|
||||
} finally {
|
||||
clearTimeout(showLoading);
|
||||
loadingEl.remove();
|
||||
if (_openToken === tok) _openToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -499,6 +546,8 @@ async function saveReaderProgress(force = false) {
|
|||
}
|
||||
|
||||
function closeReader() {
|
||||
// Still opening: just leave it (nothing to save yet).
|
||||
if (cancelBookOpening()) return;
|
||||
if (typeof _issueReaderClosed === 'function') _issueReaderClosed();
|
||||
exitReaderImmersiveMode();
|
||||
stopReadAloud();
|
||||
|
|
@ -564,6 +613,8 @@ function closeReader() {
|
|||
currentBookId = null;
|
||||
currentBookToc = [];
|
||||
_currentBookCharCount = 0;
|
||||
if (typeof _pdfStopLazy === 'function') _pdfStopLazy();
|
||||
if (currentPdfDoc) { try { currentPdfDoc.destroy(); } catch (e) {} }
|
||||
currentPdfDoc = null;
|
||||
currentPdfBuffer = null;
|
||||
currentBookmarks = [];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* diora service worker — caches the app shell for offline use.
|
||||
*/
|
||||
|
||||
const CACHE = 'diora-v131';
|
||||
const CACHE = 'diora-v132';
|
||||
const SHELL = [
|
||||
'/static/css/app.css',
|
||||
'/static/js/app/01-core.js',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue