Reader: Buch nie doppelt öffnen; ungültige Anker selbst reparieren (SW v48)
openBook leert #reader-content und hängt die Kapitel über mehrere awaits an. Ein zweiter Aufruf dazwischen (zweites Tippen, während ein langsamer E-Reader noch entschlüsselt) hing das Buch doppelt an. Daraus gespeicherte Anker lagen hinter dem echten Buchende (tolino: Block 1491 bei 1206 Blöcken) und schlugen beim Server-Vergleich "weitester Stand gewinnt" jede echte Position: Fortschritt wurde nicht mehr gespeichert. - openBook läuft nur einmal gleichzeitig (gleiches Buch: auf den laufenden Aufruf warten; anderes Buch: danach öffnen). - Aus einem doppelt gerenderten Dokument wird kein Fortschritt gespeichert. - Ein gespeicherter Anker, der nicht zum Buch passt, führt zum Textanteil (scroll_fraction) statt an den Anfang, und die nächste Speicherung überschreibt ihn (force), damit der Fortschritt wieder läuft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7ee44622ee
commit
086ce29020
2 changed files with 43 additions and 4 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue