'use strict'; // --------------------------------------------------------------------------- // Bookmarks // --------------------------------------------------------------------------- async function loadBookmarks(bookId) { try { const res = await fetch(`/books/${bookId}/bookmarks/`); const {ct, iv} = await res.json(); if (ct) { _saveAnnotationsCache(bookId, 'bookmarks', ct, iv); // persist for offline const key = await getOrCreateEncKey(); const plain = await decryptBytes(key, iv, ct); currentBookmarks = JSON.parse(new TextDecoder().decode(plain)); } else { currentBookmarks = []; } } catch (e) { // offline fallback const cached = await _loadAnnotationsCache(bookId, 'bookmarks'); if (cached?.ct) { try { const key = await getOrCreateEncKey(); const plain = await decryptBytes(key, cached.iv, cached.ct); currentBookmarks = JSON.parse(new TextDecoder().decode(plain)); } catch { currentBookmarks = []; } } else { currentBookmarks = []; } } } async function saveBookmarks() { if (!currentBookId) return; try { const key = await getOrCreateEncKey(); const plain = new TextEncoder().encode(JSON.stringify(currentBookmarks)); const {iv, ciphertext} = await encryptBytes(key, plain); const body = JSON.stringify({ct: ciphertext, iv}); const url = `/books/${currentBookId}/bookmarks/`; _lastBookmarkBeacon = {url, body}; _saveAnnotationsCache(currentBookId, 'bookmarks', ciphertext, iv); // keep local cache current await fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, body, }); bookmarksDirty = false; } catch (e) { _markPendingSync(currentBookId, 'bookmarks'); } } function addBookmark() { const contentEl = $('reader-content'); if (!contentEl || !currentBookId) return; let label, anchor, scrollFraction; if (currentPdfDoc) { const page = pdfCurrentPage || 1; label = `Seite ${pdfPageLabel(page)}`; anchor = `pdf-page-${page}`; scrollFraction = (page - 1) / Math.max(1, pdfTotalPages - 1); } else { // Find first visible chapter div const chapters = contentEl.querySelectorAll('[data-epub-src]'); let visibleChapter = null; for (const ch of chapters) { const rect = ch.getBoundingClientRect(); if (rect.bottom > 0 && rect.top < window.innerHeight) { visibleChapter = ch; break; } } const src = visibleChapter?.getAttribute('data-epub-src') || ''; label = src.split('/').pop().replace(/\.x?html?$/i, '') || 'Bookmark'; anchor = src; scrollFraction = contentEl.scrollTop / (contentEl.scrollHeight - contentEl.clientHeight || 1); } const bm = { id: crypto.randomUUID(), label, anchor, scrollFraction, createdAt: new Date().toISOString(), }; currentBookmarks.unshift(bm); bookmarksDirty = true; saveBookmarks(); // Toast const toast = document.createElement('div'); toast.className = 'reader-toast'; toast.textContent = `★ Lesezeichen: ${label}`; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2200); } function openBookmarksSidebar() { if (!currentBookmarks.length) { openSidebar('Lesezeichen', '
Noch keine Lesezeichen. Tippe beim Lesen auf ★.
'); return; } let html = '