diff --git a/static/js/app.js b/static/js/app.js index d12f1a5..5112828 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1991,6 +1991,8 @@ let readerScrollSaveTimer = null; let _resizeObserver = null; let _currentPositionAnchor = ''; let _currentBookCharCount = 0; +// Rough characters per printed book page, used to estimate a real page count for EPUBs. +const CHARS_PER_BOOK_PAGE = 1800; const bookMetaCache = {}; // id → {title, author, type} // Reading-position percentages (the progress readout, annotation "% im Buch" @@ -2493,8 +2495,8 @@ async function loadBookList() { try { const metaBuf = await decryptBytes(key, b.meta_iv, b.meta_ct); const meta = JSON.parse(new TextDecoder().decode(metaBuf)); - bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || ''}; - decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); + bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', pages: meta.pages || 0}; + decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', pages: meta.pages || 0, scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } catch (e) { bookMetaCache[b.id] = {title: `Buch #${b.id}`, author: '', type: 'epub', filename: '', folder: '', isbn: '', shelfTag: ''}; decrypted.push({id: b.id, title: `Buch #${b.id}`, author: '', type: 'epub', folder: '', isbn: '', shelfTag: '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); @@ -2536,7 +2538,7 @@ async function loadBookList() { if (b.last_read) return 1; return (b.uploaded_at || '').localeCompare(a.uploaded_at || ''); }); - for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || '', isbn: b.isbn || '', shelfTag: b.shelfTag || ''}; + for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || '', isbn: b.isbn || '', shelfTag: b.shelfTag || '', pages: b.pages || 0}; const uploadArea = $('book-upload-area'); if (uploadArea) uploadArea.style.display = 'none'; renderBookList(cachedBooks); @@ -2698,7 +2700,7 @@ function _renderBookItemHtml(b) { ${escapeHtml(b.title)}${keyWarning}${b.is_read ? ' ✓ gelesen' : ''} ${escapeHtml(b.author)} - ${pct > 0 ? `${pct} % gelesen` : ''} + ${pct > 0 ? `${pct} % gelesen${b.pages ? ` (≈ ${Math.max(1, Math.round(b.pages * pct / 100))} von ${b.pages} Seiten)` : ''}` : (b.pages ? `ca. ${b.pages} Seiten` : '')} ${b.shelfTag ? `${escapeHtml(b.shelfTag)}` : ''} @@ -2825,6 +2827,7 @@ async function _updateBookMeta(bookId, overrides) { isbn: cached.isbn || book.isbn || '', folder: cached.folder || book.folder || '', shelfTag: cached.shelfTag || book.shelfTag || '', + pages: cached.pages || book.pages || 0, ...overrides, }; const key = await getOrCreateEncKey(); @@ -3767,6 +3770,13 @@ async function _openBookOnce(bookId) { currentBookId = bookId; + // Remember the book's real (PDF) or estimated (EPUB) page count in its encrypted meta so + // the book list can show it; rewritten only when it changed. + const bookPages = isPdfBook ? numPages : Math.max(1, Math.round(_currentBookCharCount / CHARS_PER_BOOK_PAGE)); + if (bookPages > 0 && cachedMeta.pages !== bookPages) { + _updateBookMeta(bookId, {pages: bookPages}).catch(() => {}); + } + // Load bookmarks and highlights await Promise.all([ loadBookmarks(bookId), diff --git a/static/js/sw.js b/static/js/sw.js index dda6cf9..b6114eb 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-v95'; +const CACHE = 'diora-v96'; const SHELL = [ '/static/css/app.css', '/static/js/app.js',