Add IndexedDB book cache and fix PDF position on mobile
All checks were successful
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Successful in 15s

- Cache encrypted book data in IndexedDB (cache-first on open)
- Evict books not read for 4 weeks on book list load
- Fix PDF paginated mode not activating on book open (mobile)
- enterPdfPaginatedMode() now called after position restore in openBook()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
marwin 2026-04-01 16:10:03 +02:00
parent dbe3b46f3e
commit 68bb7b5920

View file

@ -2617,6 +2617,65 @@ function restoreFromAnchor(contentEl, anchor) {
return true;
}
// ---------------------------------------------------------------------------
// Book cache — IndexedDB, evict after 4 weeks of inactivity
// ---------------------------------------------------------------------------
const _BOOK_CACHE_STORE = 'books';
const _BOOK_CACHE_EVICT_MS = 4 * 7 * 24 * 60 * 60 * 1000;
function _openBookCacheDb() {
return new Promise((resolve, reject) => {
const req = indexedDB.open('diora_book_cache', 1);
req.onupgradeneeded = e => e.target.result.createObjectStore(_BOOK_CACHE_STORE, {keyPath: 'id'});
req.onsuccess = e => resolve(e.target.result);
req.onerror = () => reject();
});
}
async function _getCachedBook(bookId) {
try {
const db = await _openBookCacheDb();
return await new Promise((resolve) => {
const req = db.transaction(_BOOK_CACHE_STORE).objectStore(_BOOK_CACHE_STORE).get(bookId);
req.onsuccess = e => resolve(e.target.result || null);
req.onerror = () => resolve(null);
});
} catch { return null; }
}
async function _setCachedBook(bookId, data_ct, data_iv) {
try {
const db = await _openBookCacheDb();
await new Promise((resolve) => {
const tx = db.transaction(_BOOK_CACHE_STORE, 'readwrite');
tx.objectStore(_BOOK_CACHE_STORE).put({id: bookId, data_ct, data_iv, cached_at: Date.now()});
tx.oncomplete = resolve;
tx.onerror = resolve;
});
} catch {}
}
async function _evictBookCache(bookList) {
try {
const db = await _openBookCacheDb();
const now = Date.now();
const metaById = Object.fromEntries(bookList.map(b => [b.id, b]));
await new Promise((resolve) => {
const tx = db.transaction(_BOOK_CACHE_STORE, 'readwrite');
const store = tx.objectStore(_BOOK_CACHE_STORE);
store.openCursor().onsuccess = e => {
const cursor = e.target.result;
if (!cursor) { resolve(); return; }
const meta = metaById[cursor.value.id];
const lastRead = meta?.last_read ? new Date(meta.last_read).getTime() : 0;
if (!meta || (now - lastRead) > _BOOK_CACHE_EVICT_MS) cursor.delete();
cursor.continue();
};
tx.onerror = resolve;
});
} catch {}
}
// Reader settings
let readerSettings = { fontSize: 16, lineHeight: 1.8, maxWidth: 65, theme: 'dark',
pdfZoom: 100, pdfInverted: false, pdfPaginated: false };
@ -2668,6 +2727,7 @@ async function loadBookList() {
return;
}
const books = await res.json();
_evictBookCache(books); // fire-and-forget
if (!Array.isArray(books)) {
listEl.innerHTML = `<p class="muted">Unexpected response from server (not an array).</p>`;
return;
@ -2996,8 +3056,15 @@ async function openBook(bookId) {
try {
loadReaderSettings();
const key = await getOrCreateEncKey();
let data_ct, data_iv;
const cached = await _getCachedBook(bookId);
if (cached) {
({data_ct, data_iv} = cached);
} else {
const res = await fetch(`/books/${bookId}/data/`);
const {data_ct, data_iv} = await res.json();
({data_ct, data_iv} = await res.json());
_setCachedBook(bookId, data_ct, data_iv); // fire-and-forget
}
const plain = await decryptBytes(key, data_iv, data_ct);
// Revoke any previous image blob URLs
@ -3098,8 +3165,10 @@ async function openBook(bookId) {
const fraction = bookData ? (bookData.scroll_fraction || 0) : 0;
const anchor = bookData ? (bookData.position_anchor || '') : '';
_currentPositionAnchor = anchor;
if (isPdf && readerSettings.pdfPaginated && pdfTotalPages > 1) {
if (fraction > 0) pdfCurrentPage = Math.max(1, Math.round(fraction * (pdfTotalPages - 1)) + 1);
if (isPdf && readerSettings.pdfPaginated) {
if (fraction > 0 && pdfTotalPages > 1)
pdfCurrentPage = Math.max(1, Math.round(fraction * (pdfTotalPages - 1)) + 1);
enterPdfPaginatedMode();
} else if (!isPdf) {
// Wait for all images so scrollHeight is final
const imgs = Array.from(contentEl.querySelectorAll('img'));