'use strict'; // --------------------------------------------------------------------------- // Immersive reader mode — tap centre of screen to toggle bars // --------------------------------------------------------------------------- let _immBarsVisible = true; function _immHandleTap(e) { // Ignore taps on interactive elements (buttons, links, inputs, settings panel, footnote popover) if (e.target.closest('button, a, input, select, label, #reader-settings-panel, .reader-header, .footnote-popover, #reader-margin, #highlight-popover, .note-bottom-sheet, .tts-bar')) return; // In marker mode, taps have a dedicated meaning (highlight/create a note) — // don't also toggle the immersive bars underneath. if (markerModeActive) return; // Don't toggle bars while the user has just finished selecting text (e.g. to highlight it) const sel = window.getSelection(); if (sel && !sel.isCollapsed) return; // If a footnote popover is open, dismiss it instead of toggling bars if (currentFootnotePopover && !currentFootnotePopover.contains(e.target)) { dismissFootnotePopover(); return; } _immBarsVisible = !_immBarsVisible; document.body.classList.toggle('reader-immersive', !_immBarsVisible); // Close settings panel when bars disappear if (!_immBarsVisible && readerSettingsPanelOpen) { const sp = document.getElementById('reader-settings-panel'); if (sp) sp.remove(); readerSettingsPanelOpen = false; } } // A double click on the text (with a mouse): full screen, the bars out of the // way; again (or Esc): back as it was. Fingers keep their double tap; beside // the text a click turns the page (63-pages.js), so only on the text itself. let _readerFsHidBars = false; // What openBook hangs on the content and the page number for this book, taken off again in // closeReader — else every book opened adds its own, and a PDF's handlers run under the next // EPUB. (Not addEventListener's signal: the tolino's Chrome 61 ignores it.) let _readerListeners = []; function _readerListen(el, type, fn, opts) { if (!el) return; el.addEventListener(type, fn, opts); _readerListeners.push([el, type, fn, opts]); } function _readerUnlistenAll() { for (const [el, type, fn, opts] of _readerListeners) el.removeEventListener(type, fn, opts); _readerListeners = []; } function _readerDblClick(e) { if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return; if (!e.target.closest('[data-epub-src], .pdf-page-wrapper')) return; if (e.target.closest('button, a, input, select, textarea, label, [data-footnote-ref], mark.epub-highlight')) return; // The double click also picked a word: that's not a marking. const sel = window.getSelection(); if (sel) sel.removeAllRanges(); if (typeof dismissHighlightPopover === 'function') dismissHighlightPopover(); if (document.fullscreenElement) { if (document.exitFullscreen) document.exitFullscreen().catch(() => {}); return; } const el = document.documentElement; if (!el.requestFullscreen) return; el.requestFullscreen().then(() => { if (_immBarsVisible) { _immBarsVisible = false; document.body.classList.add('reader-immersive'); _readerFsHidBars = true; } }).catch(() => {}); } document.addEventListener('fullscreenchange', () => { if (document.fullscreenElement || !_readerFsHidBars) return; _readerFsHidBars = false; const overlay = $('reader-overlay'); if (overlay && overlay.style.display !== 'none' && !_immBarsVisible) { _immBarsVisible = true; document.body.classList.remove('reader-immersive'); } }); document.addEventListener('DOMContentLoaded', () => { const row = $('reader-body-row'); if (row) row.addEventListener('dblclick', _readerDblClick); }); function enterReaderImmersiveMode() { _immBarsVisible = true; document.body.classList.remove('reader-immersive'); document.addEventListener('click', _immHandleTap); } function exitReaderImmersiveMode() { _immBarsVisible = true; document.body.classList.remove('reader-immersive'); document.removeEventListener('click', _immHandleTap); } // Serializes openBook: it empties #reader-content, then appends the chapters // across several awaits. A second call in between (a second tap while a slow // e-reader is still decrypting) appended the whole book a second time — and // progress saved from that doubled DOM had block indices beyond the real book, // which then out-ranked every real position on the server. let _openBookInFlight = null; // The opening under way: {cancelled, ctrl} — "Abbrechen", closing the reader or opening another // book end it (docs: a big scanned PDF used to take minutes and could not be left). let _openToken = null; function cancelBookOpening() { const t = _openToken; if (!t || t.cancelled) return false; t.cancelled = true; try { t.ctrl.abort(); } catch (e) {} _pdfRenderGen++; if (typeof _pdfStopLazy === 'function') _pdfStopLazy(); if (typeof _pdfLoadingTask !== 'undefined' && _pdfLoadingTask) { try { _pdfLoadingTask.destroy(); } catch (e) {} } document.querySelectorAll('.pdf-loading-overlay').forEach(el => el.remove()); const overlay = $('reader-overlay'); if (overlay) overlay.style.display = 'none'; const contentEl = $('reader-content'); if (contentEl) contentEl.innerHTML = ''; if (currentPdfDoc) { try { currentPdfDoc.destroy(); } catch (e) {} } currentPdfDoc = null; currentPdfBuffer = null; _readerUnlistenAll(); _readPos = null; return true; } window.cancelBookOpening = cancelBookOpening; async function openBook(bookId) { if (_openBookInFlight) { if (_openBookInFlight.bookId === bookId) return _openBookInFlight.promise; cancelBookOpening(); // another book: the one still loading is left await _openBookInFlight.promise.catch(() => {}); } const promise = _openBookOnce(bookId); _openBookInFlight = {bookId, promise}; try { return await promise; } finally { if (_openBookInFlight && _openBookInFlight.promise === promise) _openBookInFlight = null; } } async function _openBookOnce(bookId) { // Nothing is saved for this book until its place is restored. _readPos = {bookId, ready: false, key: null, readAt: 0, moved: false, hold: false}; const overlay = $('reader-overlay'); const contentEl = $('reader-content'); const titleEl = $('reader-title'); if (!overlay || !contentEl) return; titleEl.textContent = 'Lade …'; contentEl.innerHTML = ''; overlay.style.display = ''; const tok = {cancelled: false, ctrl: new AbortController()}; _openToken = tok; // A moment later (not for a book that opens at once): the spinner with "Abbrechen". const loadingEl = document.createElement('div'); loadingEl.className = 'pdf-loading-overlay'; loadingEl.innerHTML = '
Füge den Schlüssel ein, den du im anderen Browser exportiert hast:
Das ersetzt den Schlüssel in diesem Browser. Hier hochgeladene Bücher sind erst wieder lesbar, wenn du den Schlüssel zurück abgleichst.
`); body.addEventListener('click', async function _importClick(e) { if (!e.target.closest('[data-import-key-apply]')) return; body.removeEventListener('click', _importClick); const b64 = (body.querySelector('#import-key-input')?.value || '').trim(); const statusEl = $('book-key-status'); try { const raw = base64ToBytes(b64); const importedKey = await crypto.subtle.importKey('raw', raw, {name: 'AES-GCM', length: 256}, true, ['encrypt', 'decrypt']); const re_exported = await crypto.subtle.exportKey('raw', importedKey); localStorage.setItem(`diora_enc_key_${window.USER_ID}`, bytesToBase64(re_exported)); closeSidebar(); if (statusEl) statusEl.textContent = '✓ Schlüssel importiert – Bücher werden neu geladen …'; await loadBookList(); if (statusEl) setTimeout(() => { statusEl.textContent = ''; }, 3000); } catch (e) { if ($('book-key-status')) $('book-key-status').textContent = 'Import fehlgeschlagen: ungültiger Schlüssel'; } }); } async function deleteBook(bookId) { if (!await customConfirm('Dieses Buch löschen? Das lässt sich nicht rückgängig machen.', { danger: true })) return; try { const res = await fetch(`/books/${bookId}/delete/`, { method: 'POST', headers: {'X-CSRFToken': getCsrfToken()}, }); const data = await res.json(); if (data.ok) loadBookList(); } catch (e) {} } // Opened somewhere before the furthest place reached: offer it. function _offerFurthestPosition(p) { if (!p) return; const furthest = p.furthest_fraction || 0; if (furthest - (p.scroll_fraction || 0) < 0.02 || furthest >= 0.999) return; const pos = _readPos; _readerToast(`Weiteste Stelle: ${_formatBookPercent(furthest)} %`, 'Dorthin', () => { if (_readPos !== pos) return; _noteReaderInteraction(); _jumpToPosition(p.furthest_anchor || '', furthest); }, 8000); } async function saveReaderProgress(force = false) { if (!currentBookId) return; const contentEl = $('reader-content'); if (!contentEl) return; let fraction; if (readerSettings.pdfPaginated && currentPdfDoc && pdfTotalPages > 1) { fraction = (pdfCurrentPage - 1) / (pdfTotalPages - 1); } else { fraction = contentEl.scrollTop / (contentEl.scrollHeight - contentEl.clientHeight || 1); } // Chapter wrappers carry unique ids; seeing one twice means the book was // rendered twice. Positions from that DOM are wrong — don't store them. if (!currentPdfDoc && contentEl.querySelectorAll('#epub-chapter-0').length > 1) { console.warn('Book rendered twice, progress not saved'); return; } // Not restored yet (or another book): what's shown isn't a place read. const pos = _readPos; if (!pos || !pos.ready || pos.bookId !== currentBookId) return; // A jump that's only a look: the place before it stays. if (pos.hold && !force) return; let anchor = ''; if (!currentPdfDoc) { anchor = getPositionAnchor(contentEl); _currentPositionAnchor = anchor; // EPUB: share of the text, the same on every device and layout. const textFraction = _fractionFromAnchor(contentEl, anchor); if (textFraction !== null) fraction = textFraction; } fraction = Math.min(1.0, Math.max(0.0, fraction)); _noteReadingPosition(anchor, fraction); if (force) { pos.readAt = Date.now(); pos.moved = true; } // Nothing read here since opening: the server has this place already. if (!pos.moved) return; const entry = {book: currentBookId, fraction, anchor, read_at: pos.readAt, device: _progressDevice(), ...(force && {force: true})}; const sig = `${entry.book}|${anchor}|${fraction.toFixed(5)}|${entry.read_at}`; _lastProgressBeacon = entry; if (!force && sig === pos.sentSig) return; // unchanged since the last save pos.sentSig = sig; _progressOutboxPut(entry); _updateCachedBookProgress(currentBookId, fraction, anchor, force, pos.readAt); const sent = await _sendProgress(entry); if (!sent) pos.sentSig = null; // try again with the next save if (sent && force) { const toast = document.createElement('div'); toast.className = 'reader-toast'; toast.textContent = 'Position gesetzt'; document.body.appendChild(toast); setTimeout(() => toast.remove(), 1800); } } function closeReader() { // Still opening: just leave it (nothing to save yet). if (cancelBookOpening()) return; if (typeof _issueReaderClosed === 'function') _issueReaderClosed(); // An article (65-article.js): where it was read to, before the reader empties. if (typeof _articleReaderClosed === 'function') _articleReaderClosed(); exitReaderImmersiveMode(); stopReadAloud(); // Save progress BEFORE hiding — scrollHeight/clientHeight return 0 once display:none saveReaderProgress(); _readPos = null; // nothing more is saved for this book if (bookmarksDirty) saveBookmarks(); if (highlightsDirty) saveHighlights(); const overlay = $('reader-overlay'); if (overlay) overlay.style.display = 'none'; if (readerScrollSaveTimer) { clearInterval(readerScrollSaveTimer); readerScrollSaveTimer = null; } if (_resizeObserver) { _resizeObserver.disconnect(); _resizeObserver = null; } _currentPositionAnchor = ''; // Close the margin panel readerAnnotationsMarginOpen = false; const marginEl = $('reader-margin'); if (marginEl) marginEl.classList.remove('open'); _centerReaderColumn(); const marginMarkersEl = $('reader-margin-markers'); if (marginMarkersEl) marginMarkersEl.innerHTML = ''; // Disarm marker mode markerModeActive = false; document.querySelectorAll('.marker-mode-btn').forEach(b => b.classList.remove('active')); dismissNoteBottomSheet(); // Clear search before wiping content clearReaderSearch(); // Close settings panel if open readerSettingsPanelOpen = false; const sp = document.getElementById('reader-settings-panel'); if (sp) sp.remove(); // Reset progress input const progressInput = $('reader-progress-input'); if (progressInput) { progressInput.style.display = 'none'; progressInput.value = 0; } const progressSuffix = $('reader-progress-suffix'); if (progressSuffix) progressSuffix.textContent = ''; // Remove touch handlers and free image blob URLs const contentEl = $('reader-content'); _readerUnlistenAll(); if (contentEl) { contentEl.removeEventListener('click', _pdfPaginatedClick); contentEl.removeEventListener('wheel', _pdfPaginatedWheel); contentEl.classList.remove('pinch-active', 'pdf-paginated'); contentEl.style.overflow = ''; contentEl.innerHTML = ''; } for (const url of Object.values(currentImageMap)) URL.revokeObjectURL(url); currentImageMap = {}; // Reset all state currentBookId = null; currentBookToc = []; _currentBookCharCount = 0; if (typeof _pdfStopLazy === 'function') _pdfStopLazy(); if (currentPdfDoc) { try { currentPdfDoc.destroy(); } catch (e) {} } currentPdfDoc = null; currentPdfBuffer = null; currentBookmarks = []; bookmarksDirty = false; currentHighlights = []; highlightsDirty = false; _lastProgressBeacon = null; _lastBookmarkBeacon = null; _lastHighlightBeacon = null; dismissHighlightPopover(); pdfCurrentPage = 1; pdfTotalPages = 0; _pdfPageTextBoxCache = {}; // Remove PDF invert class if (overlay) overlay.classList.remove('pdf-inverted'); } function openTocSidebar() { if (!currentBookToc.length) { openSidebar('Inhaltsverzeichnis', 'Dieses Buch hat kein Inhaltsverzeichnis.
'); return; } let html = '