diora-web/static/js/app/68-bookmarks.js

159 lines
5.2 KiB
JavaScript
Raw Normal View History

'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 || parseInt($('reader-progress-input')?.value, 10) || 1;
label = `Seite ${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', '<p class="muted">Noch keine Lesezeichen. Tippe beim Lesen auf ★.</p>');
return;
}
let html = '<ul style="list-style:none;padding:0;">';
for (const bm of currentBookmarks) {
html += `<li class="bookmark-entry">
<button class="btn-link" data-jump-bookmark="${escapeHtml(bm.id)}" style="flex:1;text-align:left;">
${escapeHtml(bm.label)}
</button>
<button class="btn-icon" data-delete-bookmark="${escapeHtml(bm.id)}" title="Löschen">✕</button>
</li>`;
}
html += '</ul>';
openSidebar('Lesezeichen', html);
const body = $('sidebar-body');
body.addEventListener('click', function _bmClick(e) {
const jumpBtn = e.target.closest('[data-jump-bookmark]');
const delBtn = e.target.closest('[data-delete-bookmark]');
if (jumpBtn) {
body.removeEventListener('click', _bmClick);
jumpToBookmark(jumpBtn.dataset.jumpBookmark);
}
if (delBtn) {
const id = delBtn.dataset.deleteBookmark;
currentBookmarks = currentBookmarks.filter(b => b.id !== id);
bookmarksDirty = true;
saveBookmarks();
openBookmarksSidebar(); // re-render
}
});
}
function jumpToBookmark(id) {
const bm = currentBookmarks.find(b => b.id === id);
if (!bm) return;
closeSidebar();
_suppressScrollJumpDetect();
setTimeout(() => {
const contentEl = $('reader-content');
if (!contentEl) return;
if (bm.anchor.startsWith('pdf-page-')) {
PDF-Reader: ein Zeichenweg, Seitenmodus neu, Seitensprung für beide Modi (WIP, ungetestet) - 58-pdf.js: _pdfDraw – eine Zeichnung je Canvas (die vorige wird abgebrochen und abgewartet), Pixeldichte, höchstens 16,7 MP; Seiten, die gehen, geben auch pdf.js' Seitendaten frei; ein gemeinsamer pdf.js-Worker (_pdfLoad) auch für Uploads, Seitenzahl, Hefte und Cover; Inhalts- verzeichnis parallel aufgelöst, auch mit Seitenindex als Ziel; ResizeObserver: Drehen, Breite, Leisten – neu an derselben Seite. - 66-pdf-paginated.js: Seitenmodus auf _pdfDraw, Zuschnitt in Bildschirmkoordinaten (gedrehte Seiten) und höchstens 2,5× der ganzen Seite (eine fast leere Seite wird nicht unendlich groß); nächste und vorige Seite vorgezeichnet; Tippen zum Blättern blendet die Leisten nicht mehr ein; Wischen nur deutlich seitwärts; Mausrad; Pinch setzt die Vorschau zurück; nach dem Blättern wird gespeichert und 'diora-pdf-turn' gemeldet (E-Ink-Refresh). - pdfGoToPage für beide Modi: Inhaltsverzeichnis, Seitenzahl im Kopf, Lesezeichen, Tasten (Seitenmodus jetzt auch Leertaste, Bild auf/ab). - Beim Scrollen wird die aktuelle Seite mitgeführt (Lesezeichen sagten immer „Seite 1“). - Schalter Scrollen | Seiten im PDF-Panel; Zoom-Regler zeichnet erst neu, wenn er ruht. - openBook hängt seine Listener über _readerListen an; closeReader nimmt sie ab (vorher sammelten sie sich bei jedem Öffnen an). Abbrechen und ein zweites Buch geben das alte Dokument frei; der entschlüsselte PDF-Puffer wird nicht mehr gehalten. - Invertieren dreht die Farbtöne zurück (Fotos keine Negative). - e2e/fixtures/make_test_pdf.py: Test-PDF mit gemischten Formaten, Outline, Links und einer fast leeren Seite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:52:28 +01:00
pdfGoToPage(parseInt(bm.anchor.replace('pdf-page-', ''), 10) || 1);
} else {
const target = Array.from(contentEl.querySelectorAll('[data-epub-src]'))
.find(el => el.getAttribute('data-epub-src') === bm.anchor);
if (target) {
const top = target.getBoundingClientRect().top - contentEl.getBoundingClientRect().top;
contentEl.scrollBy({top: top - 16, behavior: 'smooth'});
} else {
contentEl.scrollTop = bm.scrollFraction * (contentEl.scrollHeight - contentEl.clientHeight);
}
}
}, 50);
}