Bücherliste: Seitenzahl neben dem Lesefortschritt
All checks were successful
Build and push Docker image / build (push) Successful in 16s
Test / test (push) Successful in 7m42s

PDFs liefern die echte Seitenzahl, bei EPUBs wird sie aus der Zeichenzahl
geschätzt (1800 Zeichen je Seite). Gespeichert in den verschlüsselten Metadaten
beim Öffnen des Buchs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Marwin Schulz 2026-10-07 08:50:07 +02:00
parent 3d659fc809
commit aa4cf23b58
2 changed files with 15 additions and 5 deletions

View file

@ -1991,6 +1991,8 @@ let readerScrollSaveTimer = null;
let _resizeObserver = null; let _resizeObserver = null;
let _currentPositionAnchor = ''; let _currentPositionAnchor = '';
let _currentBookCharCount = 0; 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} const bookMetaCache = {}; // id → {title, author, type}
// Reading-position percentages (the progress readout, annotation "% im Buch" // Reading-position percentages (the progress readout, annotation "% im Buch"
@ -2493,8 +2495,8 @@ async function loadBookList() {
try { try {
const metaBuf = await decryptBytes(key, b.meta_iv, b.meta_ct); const metaBuf = await decryptBytes(key, b.meta_iv, b.meta_ct);
const meta = JSON.parse(new TextDecoder().decode(metaBuf)); 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 || ''}; 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 || '', 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}); 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) { } catch (e) {
bookMetaCache[b.id] = {title: `Buch #${b.id}`, author: '', type: 'epub', filename: '', folder: '', isbn: '', shelfTag: ''}; 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}); 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; if (b.last_read) return 1;
return (b.uploaded_at || '').localeCompare(a.uploaded_at || ''); 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'); const uploadArea = $('book-upload-area');
if (uploadArea) uploadArea.style.display = 'none'; if (uploadArea) uploadArea.style.display = 'none';
renderBookList(cachedBooks); renderBookList(cachedBooks);
@ -2698,7 +2700,7 @@ function _renderBookItemHtml(b) {
<strong class="book-title">${escapeHtml(b.title)}${keyWarning}${b.is_read ? ' <span class="muted book-read-badge">✓ gelesen</span>' : ''}</strong> <strong class="book-title">${escapeHtml(b.title)}${keyWarning}${b.is_read ? ' <span class="muted book-read-badge">✓ gelesen</span>' : ''}</strong>
<span class="muted book-author">${escapeHtml(b.author)}</span> <span class="muted book-author">${escapeHtml(b.author)}</span>
<span class="book-item-meta-row"> <span class="book-item-meta-row">
${pct > 0 ? `<span class="muted book-progress">${pct} % gelesen</span>` : ''} ${pct > 0 ? `<span class="muted book-progress">${pct} % gelesen${b.pages ? ` (≈ ${Math.max(1, Math.round(b.pages * pct / 100))} von ${b.pages} Seiten)` : ''}</span>` : (b.pages ? `<span class="muted book-progress">ca. ${b.pages} Seiten</span>` : '')}
${b.shelfTag ? `<span class="book-shelf-badge" title="Regal-Kategorie">${escapeHtml(b.shelfTag)}</span>` : ''} ${b.shelfTag ? `<span class="book-shelf-badge" title="Regal-Kategorie">${escapeHtml(b.shelfTag)}</span>` : ''}
</span> </span>
</div> </div>
@ -2825,6 +2827,7 @@ async function _updateBookMeta(bookId, overrides) {
isbn: cached.isbn || book.isbn || '', isbn: cached.isbn || book.isbn || '',
folder: cached.folder || book.folder || '', folder: cached.folder || book.folder || '',
shelfTag: cached.shelfTag || book.shelfTag || '', shelfTag: cached.shelfTag || book.shelfTag || '',
pages: cached.pages || book.pages || 0,
...overrides, ...overrides,
}; };
const key = await getOrCreateEncKey(); const key = await getOrCreateEncKey();
@ -3767,6 +3770,13 @@ async function _openBookOnce(bookId) {
currentBookId = 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 // Load bookmarks and highlights
await Promise.all([ await Promise.all([
loadBookmarks(bookId), loadBookmarks(bookId),

View file

@ -2,7 +2,7 @@
* diora service worker — caches the app shell for offline use. * diora service worker — caches the app shell for offline use.
*/ */
const CACHE = 'diora-v95'; const CACHE = 'diora-v96';
const SHELL = [ const SHELL = [
'/static/css/app.css', '/static/css/app.css',
'/static/js/app.js', '/static/js/app.js',