diff --git a/static/js/app.js b/static/js/app.js index cf513db..21376a4 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4505,7 +4505,30 @@ function exitReaderImmersiveMode() { document.removeEventListener('click', _immHandleTap); } +// Serializes openBook: it empties #reader-content, then appends the chapters +// across several awaits. A second call in between (a second tap while a slow +// e-reader is still decrypting) appended the whole book a second time — and +// 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; +let _repairProgressAnchor = false; // stored anchor didn't fit this book + async function openBook(bookId) { + if (_openBookInFlight) { + if (_openBookInFlight.bookId === bookId) return _openBookInFlight.promise; + await _openBookInFlight.promise.catch(() => {}); + } + const promise = _openBookOnce(bookId); + _openBookInFlight = {bookId, promise}; + try { + return await promise; + } finally { + if (_openBookInFlight && _openBookInFlight.promise === promise) _openBookInFlight = null; + } +} + +async function _openBookOnce(bookId) { + _repairProgressAnchor = false; const overlay = $('reader-overlay'); const contentEl = $('reader-content'); const titleEl = $('reader-title'); @@ -4690,7 +4713,14 @@ async function openBook(bookId) { } await new Promise(r => requestAnimationFrame(r)); if (!restoreFromAnchor(contentEl, anchor) && fraction > 0) { - contentEl.scrollTop = fraction * (contentEl.scrollHeight - contentEl.clientHeight); + restoreFromAnchor(contentEl, _anchorForFraction(contentEl, fraction)); + } + // An anchor past the end of this book can only come from a broken + // render. The server keeps the furthest block index, so it would block + // every real position from now on: overwrite it with the next save. + const [storedBlock] = _anchorParts(anchor); + if (storedBlock >= contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR).length) { + _repairProgressAnchor = true; } } } catch (e) {} @@ -4844,6 +4874,13 @@ async function saveReaderProgress(force = false) { fraction = contentEl.scrollTop / (contentEl.scrollHeight - contentEl.clientHeight || 1); } + // Chapter wrappers carry unique ids; seeing one twice means the book was + // rendered twice. Positions from that DOM are wrong — don't store them. + if (!currentPdfDoc && contentEl.querySelectorAll('#epub-chapter-0').length > 1) { + console.warn('Book rendered twice, progress not saved'); + return; + } + let anchor = ''; if (!currentPdfDoc) { anchor = getPositionAnchor(contentEl); @@ -4854,9 +4891,11 @@ async function saveReaderProgress(force = false) { } fraction = Math.min(1.0, Math.max(0.0, fraction)); - _updateCachedBookProgress(currentBookId, fraction, anchor, force); + _updateCachedBookProgress(currentBookId, fraction, anchor, force || (_repairProgressAnchor && !!anchor)); - const body = JSON.stringify({scroll_fraction: fraction, position_anchor: anchor, ...(force && {force: true})}); + const repair = _repairProgressAnchor && !!anchor; + if (repair) _repairProgressAnchor = false; + const body = JSON.stringify({scroll_fraction: fraction, position_anchor: anchor, ...((force || repair) && {force: true})}); _lastProgressBeacon = {url: `/books/${currentBookId}/progress/`, body}; try { diff --git a/static/js/sw.js b/static/js/sw.js index 2b72b57..c36b133 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v47'; +const CACHE = 'diora-v48'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css',