diff --git a/static/js/app.js b/static/js/app.js index 5ac24f8..39b8ae7 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2769,8 +2769,28 @@ let currentImageMap = {}; let readerScrollSaveTimer = null; let _resizeObserver = null; let _currentPositionAnchor = ''; +let _currentBookCharCount = 0; const bookMetaCache = {}; // id → {title, author, type} +// Reading-position percentages (the progress readout, annotation "% im Buch" +// coordinates): a whole percent is fine for a normal book, but on a long one +// (~600-700 physical pages and up) it barely moves during a whole scrolling +// session, which reads as broken. Past that length, show one decimal — +// formatted with the browser locale's own decimal separator. +const LONG_BOOK_CHAR_THRESHOLD = 1000000; + +function _formatBookPercent(fraction) { + const pct = Math.max(0, Math.min(100, fraction * 100)); + const decimals = _currentBookCharCount > LONG_BOOK_CHAR_THRESHOLD ? 1 : 0; + return pct.toLocaleString(undefined, {minimumFractionDigits: decimals, maximumFractionDigits: decimals}); +} + +// Accepts both '45.3' and '45,3' (locale-typed input) +function _parseLocalePercent(str) { + const v = parseFloat(String(str).trim().replace(',', '.')); + return isNaN(v) ? 0 : v; +} + // --------------------------------------------------------------------------- // Reader "scrolled too far" safety net — offers a toast to jump back after a // sudden, large scroll (accidental fling, fat-fingered keybind, etc.). Only @@ -3722,6 +3742,9 @@ async function openBook(bookId) { contentEl.appendChild(frag); await new Promise(r => requestAnimationFrame(r)); } + // Used to decide whole-percent vs one-decimal display further down — + // see _formatBookPercent(). + _currentBookCharCount = contentEl.textContent.length; } currentBookToc = toc; @@ -3752,14 +3775,10 @@ async function openBook(bookId) { if (progressInput) { progressInput.style.display = ''; if (isPdf) { - progressInput.min = 1; - progressInput.max = numPages; progressInput.value = 1; if (progressSuffix) progressSuffix.textContent = `/ ${numPages}`; } else { - progressInput.min = 0; - progressInput.max = 100; - progressInput.value = 0; + progressInput.value = _formatBookPercent(0); if (progressSuffix) progressSuffix.textContent = '%'; } @@ -3774,8 +3793,8 @@ async function openBook(bookId) { contentEl.scrollBy({top: top - 8, behavior: 'smooth'}); } } else { - const pct = Math.min(100, Math.max(0, parseInt(this.value, 10) || 0)); - this.value = pct; + const pct = Math.min(100, Math.max(0, _parseLocalePercent(this.value))); + this.value = _formatBookPercent(pct / 100); contentEl.scrollTop = (pct / 100) * contentEl.scrollHeight; } }); @@ -3847,7 +3866,7 @@ async function openBook(bookId) { progressInput.value = currentPage; } else { const f = contentEl.scrollTop / (contentEl.scrollHeight - contentEl.clientHeight || 1); - progressInput.value = Math.round(f * 100); + progressInput.value = _formatBookPercent(f); } }); @@ -4061,6 +4080,7 @@ function closeReader() { // Reset all state currentBookId = null; currentBookToc = []; + _currentBookCharCount = 0; currentPdfDoc = null; currentPdfBuffer = null; currentBookmarks = []; @@ -5599,7 +5619,7 @@ function exportAnnotations() { lastChapter = chapterTitle; } const pct = _percentFromRange(range); - const coord = pct !== null ? ` (≈${Math.round(pct)}% im Buch)` : ''; + const coord = pct !== null ? ` (≈${_formatBookPercent(pct / 100)}% im Buch)` : ''; if (h.type === 'note') { lines.push(`[Note]${coord} near: "${(h.anchor?.quote || '').trim()}"`); if (h.note) lines.push(` ${h.note}`); @@ -5635,7 +5655,7 @@ function openAnnotationsSidebar() { let html = '