'use strict'; // --------------------------------------------------------------------------- // Reader Settings // --------------------------------------------------------------------------- // Typography defaults. Phones keep the long-standing ones; a wide screen // (desktop browser, diora desktop) starts bigger and wider. const _READER_TYPE_DEFAULTS_NARROW = { fontSize: 16, lineHeight: 1.8, maxWidth: 65, fontWeight: null }; const _READER_TYPE_DEFAULTS_WIDE = { fontSize: 20, lineHeight: 1.6, maxWidth: 90, fontWeight: 375 }; const _READER_WIDTH_MAX = 140; // ch, the width slider's upper end ("Full" stays separate) function _readerTypeDefaults() { return window.matchMedia('(min-width: 900px)').matches ? _READER_TYPE_DEFAULTS_WIDE : _READER_TYPE_DEFAULTS_NARROW; } function loadReaderSettings(bookId) { // Reset to defaults, then apply per-book overrides Object.assign(readerSettings, { theme: 'app', fontFamily: 'serif', noBold: false, pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false }, _readerTypeDefaults()); try { const saved = JSON.parse(localStorage.getItem(`diora_reader_settings_${bookId}`) || '{}'); // Settings saved before v2 hold every value, changed or not. A value equal // to the old default was never chosen, so it gives way to today's default. if (!saved.v) { for (const [key, old] of Object.entries(_READER_TYPE_DEFAULTS_NARROW)) { if (saved[key] === old) delete saved[key]; } } // Before v3 the reader's theme was always stored, "dark" by default: that now means "Wie die App". if ((saved.v || 0) < 3 && saved.theme === 'dark') delete saved.theme; delete saved.v; Object.assign(readerSettings, saved); if (saved.pdfPaginated === undefined) { readerSettings.pdfPaginated = window.innerWidth < 768; } } catch (e) {} } function saveReaderSettings() { if (currentBookId) { localStorage.setItem(`diora_reader_settings_${currentBookId}`, JSON.stringify({ ...readerSettings, v: 3 })); } } function applyReaderSettings(isPdf) { const overlay = $('reader-overlay'); const contentEl = $('reader-content'); if (!overlay || !contentEl) return; if (!isPdf) { contentEl.style.fontSize = readerSettings.fontSize + 'px'; contentEl.style.lineHeight = readerSettings.lineHeight; contentEl.style.setProperty('--reader-max-width', readerSettings.maxWidth + 'ch'); const fontMap = { serif: "'Source Serif 4', Georgia, 'Times New Roman', serif", sans: 'system-ui, -apple-system, sans-serif', humanist: 'Verdana, Geneva, sans-serif', mono: "'Courier New', monospace", }; contentEl.style.fontFamily = fontMap[readerSettings.fontFamily] || fontMap.serif; contentEl.classList.toggle('reader-no-bold', !!readerSettings.noBold); // Weight: null keeps the stylesheet's. --reader-weight lets old WebViews // without variable-font support (which drop weights like 425) pick a // static cut instead. contentEl.style.fontWeight = ''; if (readerSettings.fontWeight) { contentEl.style.fontWeight = String(readerSettings.fontWeight); contentEl.style.setProperty('--reader-weight', String(readerSettings.fontWeight)); } else { contentEl.style.removeProperty('--reader-weight'); } if (_currentPositionAnchor && currentBookId) { _suppressScrollJumpDetect(); requestAnimationFrame(() => restoreFromAnchor($('reader-content'), _currentPositionAnchor)); } } // Theme overlay.classList.remove('reader-theme-sepia', 'reader-theme-bright'); const readerTheme = readerSettings.theme === 'app' ? (document.documentElement.getAttribute('data-theme') === 'light' ? 'bright' : 'dark') : readerSettings.theme; if (readerTheme === 'sepia') overlay.classList.add('reader-theme-sepia'); else if (readerTheme === 'bright') overlay.classList.add('reader-theme-bright'); // PDF invert if (isPdf && readerSettings.pdfInverted) overlay.classList.add('pdf-inverted'); else overlay.classList.remove('pdf-inverted'); // Annotations (marker mode + margin panel) are EPUB-only for now — PDFs have no text layer yet const marginBtn = $('reader-margin-btn'); if (marginBtn) marginBtn.style.display = isPdf ? 'none' : ''; if (isPdf && readerAnnotationsMarginOpen) toggleAnnotationsMargin(); document.querySelectorAll('.marker-mode-btn').forEach(b => { b.style.display = isPdf ? 'none' : ''; }); if (isPdf && markerModeActive) toggleMarkerMode(); // Line length or font changed: the column's place next to an open margin too. if (readerAnnotationsMarginOpen) _centerReaderColumn(); } // Font weight slider: the variable Source Serif 4 takes any weight, the // system fonts only their fixed cuts (multiples of 100). function _readerWeightStep() { return readerSettings.fontFamily === 'serif' ? 25 : 100; } function _readerWeight() { const step = _readerWeightStep(); const w = readerSettings.fontWeight || parseInt(getComputedStyle($('reader-content')).fontWeight, 10) || 400; return Math.round(w / step) * step; } function toggleSettingsPanel() { const overlay = $('reader-overlay'); const contentEl = $('reader-content'); if (!overlay || !contentEl) return; const existing = document.getElementById('reader-settings-panel'); if (existing) { existing.remove(); readerSettingsPanelOpen = false; return; } readerSettingsPanelOpen = true; const isPdf = !!currentPdfDoc; const panel = document.createElement('div'); panel.id = 'reader-settings-panel'; panel.className = 'reader-settings-panel'; if (!isPdf) { panel.innerHTML = ` `; } else { panel.innerHTML = ` `; } overlay.insertBefore(panel, $('reader-body-row')); if (!isPdf) { const fontRange = panel.querySelector('#rs-font'); const fontVal = panel.querySelector('#rs-font-val'); fontRange.addEventListener('input', () => { readerSettings.fontSize = parseInt(fontRange.value, 10); fontVal.textContent = readerSettings.fontSize + 'px'; applyReaderSettings(false); saveReaderSettings(); }); panel.querySelector('#rs-font-minus').addEventListener('click', () => { fontRange.value = Math.max(+fontRange.min, parseInt(fontRange.value, 10) - 1); fontRange.dispatchEvent(new Event('input')); }); panel.querySelector('#rs-font-plus').addEventListener('click', () => { fontRange.value = Math.min(+fontRange.max, parseInt(fontRange.value, 10) + 1); fontRange.dispatchEvent(new Event('input')); }); const lineRange = panel.querySelector('#rs-line'); const lineVal = panel.querySelector('#rs-line-val'); lineRange.addEventListener('input', () => { readerSettings.lineHeight = (parseInt(lineRange.value, 10) / 10).toFixed(1); lineVal.textContent = readerSettings.lineHeight; applyReaderSettings(false); saveReaderSettings(); }); panel.querySelector('#rs-line-minus').addEventListener('click', () => { lineRange.value = Math.max(+lineRange.min, parseInt(lineRange.value, 10) - 1); lineRange.dispatchEvent(new Event('input')); }); panel.querySelector('#rs-line-plus').addEventListener('click', () => { lineRange.value = Math.min(+lineRange.max, parseInt(lineRange.value, 10) + 1); lineRange.dispatchEvent(new Event('input')); }); const widthRange = panel.querySelector('#rs-width'); const widthVal = panel.querySelector('#rs-width-val'); widthRange.addEventListener('input', () => { readerSettings.maxWidth = parseInt(widthRange.value, 10); widthVal.textContent = readerSettings.maxWidth + 'ch'; applyReaderSettings(false); saveReaderSettings(); }); panel.querySelector('#rs-width-minus').addEventListener('click', () => { widthRange.value = Math.max(+widthRange.min, parseInt(widthRange.value, 10) - 5); widthRange.dispatchEvent(new Event('input')); }); panel.querySelector('#rs-width-plus').addEventListener('click', () => { widthRange.value = Math.min(+widthRange.max, parseInt(widthRange.value, 10) + 5); widthRange.dispatchEvent(new Event('input')); }); panel.querySelector('#rs-width-full').addEventListener('click', () => { readerSettings.maxWidth = 999; widthRange.value = 90; widthVal.textContent = 'full'; applyReaderSettings(false); saveReaderSettings(); }); const weightRange = panel.querySelector('#rs-weight'); const weightVal = panel.querySelector('#rs-weight-val'); weightRange.addEventListener('input', () => { readerSettings.fontWeight = parseInt(weightRange.value, 10); weightVal.textContent = readerSettings.fontWeight; applyReaderSettings(false); saveReaderSettings(); }); panel.querySelector('#rs-weight-minus').addEventListener('click', () => { weightRange.value = Math.max(+weightRange.min, parseInt(weightRange.value, 10) - +weightRange.step); weightRange.dispatchEvent(new Event('input')); }); panel.querySelector('#rs-weight-plus').addEventListener('click', () => { weightRange.value = Math.min(+weightRange.max, parseInt(weightRange.value, 10) + +weightRange.step); weightRange.dispatchEvent(new Event('input')); }); panel.querySelectorAll('[data-rs-font]').forEach(btn => { btn.addEventListener('click', () => { readerSettings.fontFamily = btn.dataset.rsFont; panel.querySelectorAll('[data-rs-font]').forEach(b => b.classList.toggle('active', b === btn)); // The system fonts only have cuts at multiples of 100. weightRange.step = _readerWeightStep(); if (readerSettings.fontWeight) { readerSettings.fontWeight = Math.round(readerSettings.fontWeight / weightRange.step) * weightRange.step; } applyReaderSettings(false); saveReaderSettings(); weightRange.value = _readerWeight(); weightVal.textContent = _readerWeight(); }); }); panel.querySelector('#rs-no-bold').addEventListener('click', function () { readerSettings.noBold = !readerSettings.noBold; this.classList.toggle('active', readerSettings.noBold); applyReaderSettings(false); saveReaderSettings(); }); panel.querySelectorAll('[data-rs-theme]').forEach(btn => { btn.addEventListener('click', () => { readerSettings.theme = btn.dataset.rsTheme; panel.querySelectorAll('[data-rs-theme]').forEach(b => b.classList.toggle('active', b === btn)); applyReaderSettings(false); saveReaderSettings(); }); }); } else { const zoomRange = panel.querySelector('#rs-zoom'); zoomRange.addEventListener('input', () => applyPdfZoom(parseInt(zoomRange.value, 10))); panel.querySelector('#rs-zoom-minus').addEventListener('click', () => applyPdfZoom(Math.max(50, readerSettings.pdfZoom - 10))); panel.querySelector('#rs-zoom-plus').addEventListener('click', () => applyPdfZoom(Math.min(200, readerSettings.pdfZoom + 10))); panel.querySelector('#rs-invert').addEventListener('click', function () { readerSettings.pdfInverted = !readerSettings.pdfInverted; this.classList.toggle('active', readerSettings.pdfInverted); applyReaderSettings(true); saveReaderSettings(); }); panel.querySelector('#rs-spread').addEventListener('click', function () { readerSettings.pdfSpread = !readerSettings.pdfSpread; this.classList.toggle('active', readerSettings.pdfSpread); saveReaderSettings(); reRenderPdf(); }); } } async function applyPdfZoom(newZoom) { readerSettings.pdfZoom = newZoom; const zoomRange = document.getElementById('rs-zoom'); const zoomVal = document.getElementById('rs-zoom-val'); if (zoomRange) zoomRange.value = newZoom; if (zoomVal) zoomVal.textContent = newZoom + '%'; saveReaderSettings(); if (readerSettings.pdfPaginated) { pdfSmartZoomPage(pdfCurrentPage); } else { await reRenderPdf(); } } function _currentScrollPage(contentEl) { const wrappers = contentEl.querySelectorAll('.pdf-page-wrapper'); const viewTop = contentEl.scrollTop; for (const w of wrappers) { if (w.offsetTop + w.offsetHeight > viewTop) { const n = parseInt(w.id.replace('pdf-page-', ''), 10); if (n) return n; } } return pdfCurrentPage || 1; } async function reRenderPdf() { if (!currentPdfBuffer) return; const contentEl = $('reader-content'); if (!contentEl) return; const pivot = readerSettings.pdfPaginated ? pdfCurrentPage : _currentScrollPage(contentEl); await renderPdf(currentPdfBuffer, contentEl, undefined, pivot); if (readerSettings.pdfPaginated) enterPdfPaginatedMode(); }