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',