Reader: Marker-Modus statt impliziter Auswahl, Notiz-Anker rein positional (SW v23)
Drei Nachbesserungen am Annotationen-Feature nach Live-Test: - Neuer Stift-Button (Marker-Modus) in der Kopfleiste: Textauswahl löst das Highlight-Popup nur noch aus, wenn der Modus aktiv ist — vorher war die Funktion unauffindbar (impliziter "einfach Text markieren"-Gestus ohne sichtbaren Einstiegspunkt) und hätte jede Textauswahl (auch zum Kopieren) gekapert. - Freiform-Notizen bekommen keine Textdekoration mehr im Lesefluss — der Anker dient nur der Positionierung im Rand, nicht der optischen Markierung. - Die Margin-Sidebar zeigt Notiztext jetzt direkt lesbar als kleine Post-it-Karte an der zugehörigen Stelle (mit Stapel-Logik gegen Überlappung), statt nur ein abstraktes Symbol zum Anklicken. Reine Highlights ohne Notiz bleiben ein kleiner Farbpunkt. - Highlight-Popover/Tooltip klammern ihre Position jetzt hart an den Viewport, damit eine Auswahl nahe Seitenrand nicht unsichtbar außerhalb des sichtbaren Bereichs landet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
090aec9c60
commit
bc180daab0
4 changed files with 122 additions and 44 deletions
|
|
@ -1800,12 +1800,12 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
|
||||||
.epub-highlight[data-color="blue"] { background:rgba(52,152,219,.35); }
|
.epub-highlight[data-color="blue"] { background:rgba(52,152,219,.35); }
|
||||||
.epub-highlight[data-color="red"] { background:rgba(230,57,70,.35); }
|
.epub-highlight[data-color="red"] { background:rgba(230,57,70,.35); }
|
||||||
|
|
||||||
/* Freeform note anchor (no colored highlight, just a subtle in-text cue) */
|
/* Margin ("page margin") panel — left of the reader content. Highlights without
|
||||||
.epub-note-anchor { cursor:pointer; border-bottom:2px dotted var(--accent,#e63946); }
|
a note show as a small color dot; anything with note text (freeform notes or
|
||||||
|
a highlight+note) shows as a readable little "post-it" card, positioned next
|
||||||
/* Margin ("page margin") panel — left of the reader content, holds highlight/note
|
to the text height it belongs to. Click empty space to place a new freeform
|
||||||
markers aligned to the text height they belong to. Click empty space to place a
|
note anchored to that line. Freeform notes have NO in-text mark — the anchor
|
||||||
new freeform note anchored to that line. */
|
is purely positional, only ever visible here in the margin. */
|
||||||
.reader-body-row { display:flex; flex:1; min-height:0; }
|
.reader-body-row { display:flex; flex:1; min-height:0; }
|
||||||
.reader-margin {
|
.reader-margin {
|
||||||
width:0; flex-shrink:0; overflow:hidden;
|
width:0; flex-shrink:0; overflow:hidden;
|
||||||
|
|
@ -1813,28 +1813,40 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
|
||||||
border-right:1px solid var(--border); background:var(--bg);
|
border-right:1px solid var(--border); background:var(--bg);
|
||||||
transition:width 0.25s ease;
|
transition:width 0.25s ease;
|
||||||
}
|
}
|
||||||
.reader-margin.open { width:72px; }
|
.reader-margin.open { width:220px; }
|
||||||
.reader-margin-header {
|
.reader-margin-header {
|
||||||
display:none; align-items:center; justify-content:space-between;
|
display:none; align-items:center; justify-content:space-between;
|
||||||
padding:6px 8px; border-bottom:1px solid var(--border); white-space:nowrap; flex-shrink:0;
|
padding:6px 8px; border-bottom:1px solid var(--border); white-space:nowrap; flex-shrink:0;
|
||||||
}
|
}
|
||||||
.reader-margin.open .reader-margin-header { display:flex; }
|
.reader-margin.open .reader-margin-header { display:flex; }
|
||||||
.reader-margin-title { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted,#888); }
|
.reader-margin-title { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--muted,#888); }
|
||||||
.reader-margin-markers { position:relative; flex:1; overflow:hidden; cursor:crosshair; }
|
.reader-margin-markers { position:relative; flex:1; overflow-y:auto; overflow-x:hidden; cursor:crosshair; }
|
||||||
.margin-marker {
|
|
||||||
position:absolute; left:50%; transform:translate(-50%,-50%);
|
.margin-note { position:absolute; left:8px; right:8px; cursor:pointer; }
|
||||||
width:18px; height:18px; border-radius:50%; border:none; cursor:pointer;
|
|
||||||
display:flex; align-items:center; justify-content:center; font-size:11px; line-height:1;
|
.margin-note-dot { left:8px; right:auto; width:12px; height:12px; border-radius:50%; }
|
||||||
padding:0;
|
.margin-note-dot[data-color="yellow"] { background:rgba(241,196,15,.85); }
|
||||||
|
.margin-note-dot[data-color="green"] { background:rgba(46,204,113,.85); }
|
||||||
|
.margin-note-dot[data-color="blue"] { background:rgba(52,152,219,.85); }
|
||||||
|
.margin-note-dot[data-color="red"] { background:rgba(230,57,70,.85); }
|
||||||
|
|
||||||
|
.margin-note-text {
|
||||||
|
font-size:11px; line-height:1.4; padding:5px 7px; border-radius:3px;
|
||||||
|
background:var(--bg-card,#1a1a1a); border-left:3px solid var(--muted,#888);
|
||||||
|
box-shadow:1px 2px 5px rgba(0,0,0,.3);
|
||||||
|
white-space:pre-wrap; word-break:break-word;
|
||||||
|
}
|
||||||
|
.margin-note-text[data-color="yellow"] { border-left-color:#f1c40f; }
|
||||||
|
.margin-note-text[data-color="green"] { border-left-color:#2ecc71; }
|
||||||
|
.margin-note-text[data-color="blue"] { border-left-color:#3498db; }
|
||||||
|
.margin-note-text[data-color="red"] { border-left-color:#e63946; }
|
||||||
|
/* Freeform notes get a paper-like tint, like a post-it note pinned to the text */
|
||||||
|
.margin-note-text.margin-note-freeform {
|
||||||
|
border-left-color:#e6c229; background:rgba(241,196,15,.13);
|
||||||
}
|
}
|
||||||
.margin-marker[data-color="yellow"] { background:rgba(241,196,15,.85); }
|
|
||||||
.margin-marker[data-color="green"] { background:rgba(46,204,113,.85); }
|
|
||||||
.margin-marker[data-color="blue"] { background:rgba(52,152,219,.85); }
|
|
||||||
.margin-marker[data-color="red"] { background:rgba(230,57,70,.85); }
|
|
||||||
.margin-marker.margin-marker-note { background:var(--bg-card,#1a1a1a); border:1px solid var(--accent,#e63946); color:var(--accent,#e63946); }
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.reader-margin.open { width:40px; }
|
.reader-margin.open { width:150px; }
|
||||||
.reader-margin-title { display:none; }
|
.reader-margin-title { display:none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
115
static/js/app.js
115
static/js/app.js
|
|
@ -4027,6 +4027,10 @@ function closeReader() {
|
||||||
const marginMarkersEl = $('reader-margin-markers');
|
const marginMarkersEl = $('reader-margin-markers');
|
||||||
if (marginMarkersEl) marginMarkersEl.innerHTML = '';
|
if (marginMarkersEl) marginMarkersEl.innerHTML = '';
|
||||||
|
|
||||||
|
// Disarm marker mode
|
||||||
|
markerModeActive = false;
|
||||||
|
$('reader-marker-btn')?.classList.remove('active');
|
||||||
|
|
||||||
// Clear search before wiping content
|
// Clear search before wiping content
|
||||||
clearReaderSearch();
|
clearReaderSearch();
|
||||||
|
|
||||||
|
|
@ -4194,10 +4198,13 @@ function applyReaderSettings(isPdf) {
|
||||||
if (isPdf && readerSettings.pdfInverted) overlay.classList.add('pdf-inverted');
|
if (isPdf && readerSettings.pdfInverted) overlay.classList.add('pdf-inverted');
|
||||||
else overlay.classList.remove('pdf-inverted');
|
else overlay.classList.remove('pdf-inverted');
|
||||||
|
|
||||||
// Margin panel (highlights/notes) is EPUB-only for now — PDFs have no text layer yet
|
// Annotations (marker mode + margin panel) are EPUB-only for now — PDFs have no text layer yet
|
||||||
const marginBtn = $('reader-margin-btn');
|
const marginBtn = $('reader-margin-btn');
|
||||||
if (marginBtn) marginBtn.style.display = isPdf ? 'none' : '';
|
if (marginBtn) marginBtn.style.display = isPdf ? 'none' : '';
|
||||||
if (isPdf && readerAnnotationsMarginOpen) toggleAnnotationsMargin();
|
if (isPdf && readerAnnotationsMarginOpen) toggleAnnotationsMargin();
|
||||||
|
const markerBtn = $('reader-marker-btn');
|
||||||
|
if (markerBtn) markerBtn.style.display = isPdf ? 'none' : '';
|
||||||
|
if (isPdf && markerModeActive) toggleMarkerMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSettingsPanel() {
|
function toggleSettingsPanel() {
|
||||||
|
|
@ -5043,18 +5050,19 @@ function resolveAnchorRange(h) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHighlight(h) {
|
function renderHighlight(h) {
|
||||||
|
// Freeform notes carry no in-text mark — their anchor exists purely to
|
||||||
|
// position the note card in the margin (resolveAnchorRange handles that
|
||||||
|
// directly from the stored xpath, no DOM wrapper needed).
|
||||||
|
if (h.type === 'note') return;
|
||||||
|
|
||||||
const range = resolveAnchorRange(h);
|
const range = resolveAnchorRange(h);
|
||||||
if (!range) return;
|
if (!range) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mark = document.createElement('mark');
|
const mark = document.createElement('mark');
|
||||||
mark.dataset.highlightId = h.id;
|
mark.dataset.highlightId = h.id;
|
||||||
if (h.type === 'note') {
|
|
||||||
mark.className = 'epub-note-anchor';
|
|
||||||
} else {
|
|
||||||
mark.className = 'epub-highlight';
|
mark.className = 'epub-highlight';
|
||||||
mark.dataset.color = h.color || 'yellow';
|
mark.dataset.color = h.color || 'yellow';
|
||||||
}
|
|
||||||
range.surroundContents(mark);
|
range.surroundContents(mark);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
@ -5103,8 +5111,19 @@ function buildEpubAnchor(range) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marker mode — armed via the pen button in the reader header. Text selection
|
||||||
|
// only opens the highlight popover while this is active, so normal selection
|
||||||
|
// (e.g. to copy text) isn't hijacked, and the feature has a discoverable
|
||||||
|
// on/off switch instead of being an invisible "just select text" gesture.
|
||||||
|
let markerModeActive = false;
|
||||||
|
|
||||||
|
function toggleMarkerMode() {
|
||||||
|
markerModeActive = !markerModeActive;
|
||||||
|
$('reader-marker-btn')?.classList.toggle('active', markerModeActive);
|
||||||
|
}
|
||||||
|
|
||||||
function handleReaderSelection(e) {
|
function handleReaderSelection(e) {
|
||||||
// If clicking an existing highlight, show tooltip
|
// Clicking an existing highlight always shows its tooltip, regardless of marker mode
|
||||||
const hlMark = e.target.closest('.epub-highlight');
|
const hlMark = e.target.closest('.epub-highlight');
|
||||||
if (hlMark) {
|
if (hlMark) {
|
||||||
dismissHighlightPopover();
|
dismissHighlightPopover();
|
||||||
|
|
@ -5114,6 +5133,8 @@ function handleReaderSelection(e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!markerModeActive) return;
|
||||||
|
|
||||||
dismissHighlightPopover();
|
dismissHighlightPopover();
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
||||||
|
|
@ -5137,11 +5158,25 @@ function showHighlightPopover(range) {
|
||||||
<button class="hl-color-btn" data-hl-color="red" style="background:#e63946" title="Red">A</button>
|
<button class="hl-color-btn" data-hl-color="red" style="background:#e63946" title="Red">A</button>
|
||||||
<button class="hl-note-btn" title="Add note">✎</button>
|
<button class="hl-note-btn" title="Add note">✎</button>
|
||||||
`;
|
`;
|
||||||
popover.style.top = (rect.top + window.scrollY - 44) + 'px';
|
popover.style.visibility = 'hidden'; // measure before showing, to avoid a flash at the wrong spot
|
||||||
popover.style.left = (rect.left + window.scrollX + rect.width / 2 - 70) + 'px';
|
|
||||||
document.body.appendChild(popover);
|
document.body.appendChild(popover);
|
||||||
currentHighlightPopover = popover;
|
currentHighlightPopover = popover;
|
||||||
|
|
||||||
|
// Clamp within the viewport — a selection near the top/edge of the page must
|
||||||
|
// not produce an invisible, off-screen popover (flip below when there's no
|
||||||
|
// room above; keep it horizontally on-screen).
|
||||||
|
const pRect = popover.getBoundingClientRect();
|
||||||
|
const minTop = window.scrollY + 8;
|
||||||
|
const maxTop = window.scrollY + window.innerHeight - pRect.height - 8;
|
||||||
|
let top = rect.top + window.scrollY - 44;
|
||||||
|
if (top < minTop) top = rect.bottom + window.scrollY + 8; // no room above -> try below
|
||||||
|
top = Math.max(minTop, Math.min(top, maxTop));
|
||||||
|
let left = rect.left + window.scrollX + rect.width / 2 - pRect.width / 2;
|
||||||
|
left = Math.max(window.scrollX + 8, Math.min(left, window.scrollX + window.innerWidth - pRect.width - 8));
|
||||||
|
popover.style.top = top + 'px';
|
||||||
|
popover.style.left = left + 'px';
|
||||||
|
popover.style.visibility = '';
|
||||||
|
|
||||||
// Store range info before selection is cleared
|
// Store range info before selection is cleared
|
||||||
const savedRange = range.cloneRange();
|
const savedRange = range.cloneRange();
|
||||||
|
|
||||||
|
|
@ -5171,11 +5206,22 @@ function showHighlightTooltip(markEl, h) {
|
||||||
<button class="btn btn-sm btn-danger" data-hl-delete="${escapeHtml(h?.id || '')}">Delete</button>
|
<button class="btn btn-sm btn-danger" data-hl-delete="${escapeHtml(h?.id || '')}">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
popover.style.top = (rect.bottom + window.scrollY + 4) + 'px';
|
popover.style.visibility = 'hidden';
|
||||||
popover.style.left = (rect.left + window.scrollX) + 'px';
|
|
||||||
document.body.appendChild(popover);
|
document.body.appendChild(popover);
|
||||||
currentHighlightPopover = popover;
|
currentHighlightPopover = popover;
|
||||||
|
|
||||||
|
const pRect = popover.getBoundingClientRect();
|
||||||
|
const minTop = window.scrollY + 8;
|
||||||
|
const maxTop = window.scrollY + window.innerHeight - pRect.height - 8;
|
||||||
|
let top = rect.bottom + window.scrollY + 4;
|
||||||
|
if (top > maxTop) top = rect.top + window.scrollY - pRect.height - 4; // flip above if no room below
|
||||||
|
top = Math.max(minTop, Math.min(top, maxTop));
|
||||||
|
let left = rect.left + window.scrollX;
|
||||||
|
left = Math.max(window.scrollX + 8, Math.min(left, window.scrollX + window.innerWidth - pRect.width - 8));
|
||||||
|
popover.style.top = top + 'px';
|
||||||
|
popover.style.left = left + 'px';
|
||||||
|
popover.style.visibility = '';
|
||||||
|
|
||||||
popover.addEventListener('click', ev => {
|
popover.addEventListener('click', ev => {
|
||||||
const editBtn = ev.target.closest('[data-hl-edit-note]');
|
const editBtn = ev.target.closest('[data-hl-edit-note]');
|
||||||
const delBtn = ev.target.closest('[data-hl-delete]');
|
const delBtn = ev.target.closest('[data-hl-delete]');
|
||||||
|
|
@ -5248,6 +5294,7 @@ function openNoteEditor(h) {
|
||||||
const text = (body.querySelector('#hl-note-input')?.value || '').trim();
|
const text = (body.querySelector('#hl-note-input')?.value || '').trim();
|
||||||
h.note = text;
|
h.note = text;
|
||||||
highlightsDirty = true;
|
highlightsDirty = true;
|
||||||
|
_repositionMarginMarkers();
|
||||||
debounceSaveHighlights();
|
debounceSaveHighlights();
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
});
|
});
|
||||||
|
|
@ -5301,6 +5348,11 @@ function toggleAnnotationsMargin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Renders each highlight/note at its text position in the margin. Entries
|
||||||
|
// with note text become a readable "post-it" card (the whole point of the
|
||||||
|
// margin — you can read notes there, not just see that one exists); a plain
|
||||||
|
// highlight with no note is just a small color dot. Cards are stacked
|
||||||
|
// top-to-bottom so overlapping anchors don't overlap visually.
|
||||||
function _repositionMarginMarkers() {
|
function _repositionMarginMarkers() {
|
||||||
const markersEl = $('reader-margin-markers');
|
const markersEl = $('reader-margin-markers');
|
||||||
if (!markersEl) return;
|
if (!markersEl) return;
|
||||||
|
|
@ -5308,32 +5360,45 @@ function _repositionMarginMarkers() {
|
||||||
if (!readerAnnotationsMarginOpen || currentPdfDoc) return;
|
if (!readerAnnotationsMarginOpen || currentPdfDoc) return;
|
||||||
|
|
||||||
const areaRect = markersEl.getBoundingClientRect();
|
const areaRect = markersEl.getBoundingClientRect();
|
||||||
|
const entries = [];
|
||||||
for (const h of currentHighlights) {
|
for (const h of currentHighlights) {
|
||||||
let range;
|
let range;
|
||||||
try { range = resolveAnchorRange(h); } catch (e) { continue; }
|
try { range = resolveAnchorRange(h); } catch (e) { continue; }
|
||||||
if (!range) continue;
|
if (!range) continue;
|
||||||
const rect = range.getBoundingClientRect();
|
const rect = range.getBoundingClientRect();
|
||||||
if (!rect.width && !rect.height) continue; // detached/invalid
|
if (!rect.width && !rect.height) continue; // detached/invalid
|
||||||
const y = rect.top + rect.height / 2;
|
const y = rect.top + rect.height / 2 - areaRect.top;
|
||||||
if (y < areaRect.top - 20 || y > areaRect.bottom + 20) continue; // off-screen band
|
if (y < -60 || y > areaRect.height + 60) continue; // off-screen band
|
||||||
|
entries.push({h, y});
|
||||||
const btn = document.createElement('button');
|
|
||||||
btn.className = 'margin-marker' + (h.type === 'note' ? ' margin-marker-note' : '');
|
|
||||||
if (h.type === 'note') {
|
|
||||||
btn.textContent = '✎';
|
|
||||||
} else {
|
|
||||||
btn.dataset.color = h.color || 'yellow';
|
|
||||||
}
|
}
|
||||||
btn.style.top = (y - areaRect.top) + 'px';
|
entries.sort((a, b) => a.y - b.y);
|
||||||
btn.dataset.highlightId = h.id;
|
|
||||||
const preview = h.note || h.anchor?.quote || '';
|
let nextTop = -Infinity;
|
||||||
if (preview) btn.title = preview.slice(0, 80);
|
const GAP = 4;
|
||||||
markersEl.appendChild(btn);
|
for (const {h, y} of entries) {
|
||||||
|
const hasText = !!h.note;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'margin-note ' + (hasText ? 'margin-note-text' : 'margin-note-dot');
|
||||||
|
el.dataset.highlightId = h.id;
|
||||||
|
if (h.type === 'note') {
|
||||||
|
if (hasText) el.classList.add('margin-note-freeform');
|
||||||
|
} else {
|
||||||
|
el.dataset.color = h.color || 'yellow';
|
||||||
|
}
|
||||||
|
if (hasText) el.textContent = h.note;
|
||||||
|
|
||||||
|
const desiredTop = hasText ? y - 8 : y - 6;
|
||||||
|
const top = Math.max(desiredTop, nextTop);
|
||||||
|
el.style.top = top + 'px';
|
||||||
|
markersEl.appendChild(el);
|
||||||
|
|
||||||
|
const measured = el.getBoundingClientRect().height || (hasText ? 20 : 12);
|
||||||
|
nextTop = top + measured + GAP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMarginClick(e) {
|
function handleMarginClick(e) {
|
||||||
const markerBtn = e.target.closest('.margin-marker');
|
const markerBtn = e.target.closest('.margin-note');
|
||||||
if (markerBtn) {
|
if (markerBtn) {
|
||||||
const h = currentHighlights.find(x => x.id === markerBtn.dataset.highlightId);
|
const h = currentHighlights.find(x => x.id === markerBtn.dataset.highlightId);
|
||||||
if (h) showHighlightTooltip(markerBtn, h);
|
if (h) showHighlightTooltip(markerBtn, h);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* diora service worker — caches the app shell for offline use.
|
* diora service worker — caches the app shell for offline use.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE = 'diora-v22';
|
const CACHE = 'diora-v23';
|
||||||
const PODCAST_CACHE = 'diora-podcast-v1';
|
const PODCAST_CACHE = 'diora-podcast-v1';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,7 @@
|
||||||
<button class="btn-icon" id="reader-bookmark-btn" onclick="addBookmark()" title="Bookmark">★</button>
|
<button class="btn-icon" id="reader-bookmark-btn" onclick="addBookmark()" title="Bookmark">★</button>
|
||||||
<button class="btn-icon" id="reader-bm-list-btn" onclick="openBookmarksSidebar()" title="Bookmarks">▤</button>
|
<button class="btn-icon" id="reader-bm-list-btn" onclick="openBookmarksSidebar()" title="Bookmarks">▤</button>
|
||||||
<button class="btn-icon" id="reader-toc-btn" onclick="openTocSidebar()" title="Table of contents">≡</button>
|
<button class="btn-icon" id="reader-toc-btn" onclick="openTocSidebar()" title="Table of contents">≡</button>
|
||||||
|
<button class="btn-icon" id="reader-marker-btn" onclick="toggleMarkerMode()" title="Mark text">✒</button>
|
||||||
<button class="btn-icon" id="reader-margin-btn" onclick="toggleAnnotationsMargin()" title="Notes & highlights">✎</button>
|
<button class="btn-icon" id="reader-margin-btn" onclick="toggleAnnotationsMargin()" title="Notes & highlights">✎</button>
|
||||||
<button class="btn-icon" id="reader-reset-pos-btn" onclick="saveReaderProgress(true)" title="Diese Position als Lesefortschritt setzen (überschreibt gespeicherten Fortschritt)">⚑</button>
|
<button class="btn-icon" id="reader-reset-pos-btn" onclick="saveReaderProgress(true)" title="Diese Position als Lesefortschritt setzen (überschreibt gespeicherten Fortschritt)">⚑</button>
|
||||||
<button class="btn-icon" onclick="closeReader()" title="Close (Esc)">✕</button>
|
<button class="btn-icon" onclick="closeReader()" title="Close (Esc)">✕</button>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue