'use strict'; // --------------------------------------------------------------------------- // Export annotations — walks all highlights/notes of the current book in // reading order and offers them as a downloadable plain-text file. // --------------------------------------------------------------------------- function _tocTitleForChapterSrc(chapterSrc) { if (!chapterSrc) return ''; const entry = currentBookToc.find(t => (t.href || '').split('#')[0] === chapterSrc); return entry ? entry.label : ''; } // Rough "how far through the book" position for a resolved range — there's no // real page concept for reflowable EPUB text, so this is a coordinate of last // resort: percent of total scrollable content height, good enough to roughly // relocate a highlight/note later. function _percentFromRange(range) { const contentEl = $('reader-content'); if (!contentEl || !range) return null; // Share of the text before the range, like the reading progress. const block = _blockAncestor(range.startContainer); if (block && contentEl.contains(block)) { const blocks = Array.from(contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR)); const i = blocks.indexOf(block); if (i >= 0) { const idx = _bookCharIndex(contentEl); const at = idx.starts[i] + _charOffsetInBlock(block, range.startContainer, range.startOffset); return Math.max(0, Math.min(100, (at / idx.total) * 100)); } } if (!contentEl.scrollHeight) return null; const contentRect = contentEl.getBoundingClientRect(); const rangeRect = range.getBoundingClientRect(); if (!rangeRect.width && !rangeRect.height) return null; const offsetWithinScroll = (rangeRect.top - contentRect.top) + contentEl.scrollTop; return Math.max(0, Math.min(100, (offsetWithinScroll / contentEl.scrollHeight) * 100)); } function _sortedAnnotations() { const resolved = currentHighlights.map(h => { let range; try { range = resolveAnchorRange(h); } catch (e) { range = null; } return {h, range}; }); resolved.sort((a, b) => { if (!a.range || !b.range) return 0; return a.range.compareBoundaryPoints(Range.START_TO_START, b.range); }); return resolved; } // Highlight colours as the export and the list name them. function _hlColorName(color) { return ({yellow: 'gelb', green: 'grün', blue: 'blau', red: 'rot'})[color] || color; } function exportAnnotations() { if (!currentHighlights.length) { customAlert('In diesem Buch gibt es noch keine Markierungen oder Notizen.'); return; } const resolved = _sortedAnnotations(); const titleEl = $('reader-title'); const bookTitle = (titleEl?.textContent || 'Buch').split(' — ')[0]; const lines = [`${bookTitle} — Markierungen & Notizen`, `Exportiert: ${new Date().toISOString().slice(0, 10)}`, '']; let lastChapter = null; for (const {h, range} of resolved) { const chapterTitle = _tocTitleForChapterSrc(h.anchor?.chapterSrc) || h.anchor?.chapterSrc || ''; if (chapterTitle !== lastChapter) { lines.push(`── ${chapterTitle || 'Abschnitt ohne Titel'} ──`); lastChapter = chapterTitle; } const pct = _percentFromRange(range); const coord = pct !== null ? ` (≈${_formatBookPercent(pct / 100)}% im Buch)` : ''; if (h.type === 'note') { lines.push(`[Notiz]${coord} bei: „${(h.anchor?.quote || '').trim()}“`); if (h.note) lines.push(` ${h.note}`); } else { lines.push(`[Markierung · ${_hlColorName(h.color || 'yellow')}]${coord} „${(h.anchor?.quote || '').trim()}“`); if (h.note) lines.push(` Notiz: ${h.note}`); } lines.push(''); } const blob = new Blob([lines.join('\n')], {type: 'text/plain;charset=utf-8'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${bookTitle.replace(/[^\w\-]+/g, '_')}-notizen.txt`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } // List of every highlight/note in the book, in reading order, each jumpable — // the same pattern as the bookmarks sidebar, but for annotations. Unlike the // margin panel (which only shows what's currently on-screen), this is a full // overview, useful once a book has annotations scattered throughout. function openAnnotationsSidebar() { if (!currentHighlights.length) { openSidebar('Markierungen & Notizen', '
Noch keine Markierungen oder Notizen.
'); return; } const resolved = _sortedAnnotations(); let html = '