Reader: Notizen auf Mobile schnell setzbar statt über Randspalte (SW v32)
Auf schmalen Screens saß der Marker-Modus-Button bisher nur in der 150px- Randspalte — man musste sie erst öffnen (und damit den ohnehin knappen Lesebereich verkleinern), um überhaupt markieren zu können. Jetzt zusätzlich ein Marker-Button direkt im Haupt-Header (nur <=600px sichtbar, Desktop bleibt bei der bisherigen Anordnung in der Notes-Leiste). Im Marker-Modus legt ein einfacher Tap auf reinen Lesetext (ohne Auswahl) jetzt direkt eine freie Notiz an der Wortposition an — man muss nicht mehr den exakten Klick-Punkt in der Randspalte treffen. Das Bearbeiten der Notiz läuft auf schmalen Viewports über ein neues Bottom-Sheet (volle Breite) statt über das Inline-Textfeld in der 150px-Spalte, das auf dem Handy-Keyboard unangenehm zu bedienen ist. Notiz-Objekte werden jetzt erst bei tatsächlich eingegebenem Text angelegt und gespeichert (vorher: sofort beim Setzen des Ankers) — verhindert, dass ein abgebrochener/leerer Tap eine Ghost-Notiz in currentHighlights hinterlässt. Gilt für alle Eingabewege (Bottom-Sheet UND die bestehende Inline-Randspalten-Bearbeitung), per Playwright verifiziert inkl. Escape- Cancel auf Desktop. Priorität laut Nutzer: Notizen-Erfassung muss auf Mobile gut funktionieren; dass bestehende Notizen dort weniger prominent sichtbar sind, ist explizit in Ordnung — die vertiefte Auseinandersetzung mit Notizen bleibt eine Desktop-Aufgabe. Verifiziert per Playwright (390×844 mobil + 1280×900 Desktop-Regression): Marker-Button-Sichtbarkeit, Bottom-Sheet-Flow (Erstellen/Abbrechen/Speichern, kein Ghost-Eintrag), reale Text-Selektion + Highlight+Notiz weiterhin funktionsfähig auf Mobile, Desktop-Workflow (Randspalte, Inline-Editor, Escape-Cancel, Blur-Commit) unverändert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
76068f6ed8
commit
d3c435dd59
4 changed files with 198 additions and 46 deletions
|
|
@ -1480,6 +1480,8 @@ body.dnd-mode .timer-display {
|
||||||
padding: 4px 6px; font-size: 0.82rem; cursor: pointer;
|
padding: 4px 6px; font-size: 0.82rem; cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reader-marker-btn-mobile { display: none; }
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.sidebar { width: 100vw; }
|
.sidebar { width: 100vw; }
|
||||||
.podcast-seek-bar { padding: 0 6px; }
|
.podcast-seek-bar { padding: 0 6px; }
|
||||||
|
|
@ -1494,6 +1496,12 @@ body.dnd-mode .timer-display {
|
||||||
.reader-header-actions { gap: 4px; }
|
.reader-header-actions { gap: 4px; }
|
||||||
.reader-progress-wrap { display: none; }
|
.reader-progress-wrap { display: none; }
|
||||||
.reader-content { padding: 16px 10px; }
|
.reader-content { padding: 16px 10px; }
|
||||||
|
|
||||||
|
/* On a phone, opening the 150px margin just to reach the marker-mode toggle
|
||||||
|
eats too much of an already-narrow reading column — surface it directly
|
||||||
|
in the main header instead (desktop keeps it only in the margin header,
|
||||||
|
grouped with "Notes", per earlier feedback against back-and-forth). */
|
||||||
|
.reader-marker-btn-mobile { display: inline-block; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================================================
|
/* =========================================================
|
||||||
|
|
@ -1880,6 +1888,35 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h)
|
||||||
box-shadow:1px 2px 5px rgba(0,0,0,.4); resize:vertical;
|
box-shadow:1px 2px 5px rgba(0,0,0,.4); resize:vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Note bottom sheet — the mobile alternative to the inline margin editor.
|
||||||
|
Typing a note into a 150px-wide margin column is unpleasant on a phone
|
||||||
|
keyboard, and getting there first requires opening that column at all, so
|
||||||
|
on narrow viewports note editing uses this full-width sheet instead (see
|
||||||
|
_isMobileReader() / _openNoteEditor() in app.js). Existing notes staying
|
||||||
|
less discoverable on mobile is an accepted tradeoff — reviewing them in
|
||||||
|
depth is expected to happen on desktop; setting them fast is what matters
|
||||||
|
here. */
|
||||||
|
.note-bottom-sheet { position:fixed; inset:0; z-index:650; }
|
||||||
|
.note-bottom-sheet-backdrop {
|
||||||
|
position:absolute; inset:0; background:rgba(0,0,0,.5);
|
||||||
|
opacity:0; transition:opacity .2s ease; pointer-events:none;
|
||||||
|
}
|
||||||
|
.note-bottom-sheet.open .note-bottom-sheet-backdrop { opacity:1; pointer-events:auto; }
|
||||||
|
.note-bottom-sheet-panel {
|
||||||
|
position:absolute; left:0; right:0; bottom:0;
|
||||||
|
background:var(--surface,#111); border-top:1px solid var(--border,#333);
|
||||||
|
border-radius:12px 12px 0 0; padding:12px 14px calc(14px + env(safe-area-inset-bottom));
|
||||||
|
transform:translateY(100%); transition:transform .25s ease;
|
||||||
|
display:flex; flex-direction:column; gap:10px;
|
||||||
|
}
|
||||||
|
.note-bottom-sheet.open .note-bottom-sheet-panel { transform:translateY(0); }
|
||||||
|
.note-bottom-sheet-textarea {
|
||||||
|
width:100%; min-height:110px; font-size:15px; line-height:1.4; padding:8px 10px;
|
||||||
|
border-radius:var(--radius); border:1px solid var(--border,#333);
|
||||||
|
background:var(--bg-card,#1a1a1a); color:var(--fg,#eee); resize:vertical;
|
||||||
|
}
|
||||||
|
.note-bottom-sheet-actions { display:flex; justify-content:flex-end; gap:8px; }
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.reader-margin.open { width:150px; }
|
.reader-margin.open { width:150px; }
|
||||||
.reader-margin-title { display:none; }
|
.reader-margin-title { display:none; }
|
||||||
|
|
|
||||||
190
static/js/app.js
190
static/js/app.js
|
|
@ -3691,7 +3691,10 @@ let _immBarsVisible = true;
|
||||||
|
|
||||||
function _immHandleTap(e) {
|
function _immHandleTap(e) {
|
||||||
// Ignore taps on interactive elements (buttons, links, inputs, settings panel, footnote popover)
|
// 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')) return;
|
if (e.target.closest('button, a, input, select, label, #reader-settings-panel, .reader-header, .footnote-popover, #reader-margin, #highlight-popover, .note-bottom-sheet')) 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)
|
// Don't toggle bars while the user has just finished selecting text (e.g. to highlight it)
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
if (sel && !sel.isCollapsed) return;
|
if (sel && !sel.isCollapsed) return;
|
||||||
|
|
@ -4113,7 +4116,8 @@ function closeReader() {
|
||||||
|
|
||||||
// Disarm marker mode
|
// Disarm marker mode
|
||||||
markerModeActive = false;
|
markerModeActive = false;
|
||||||
$('reader-marker-btn')?.classList.remove('active');
|
document.querySelectorAll('.marker-mode-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
dismissNoteBottomSheet();
|
||||||
|
|
||||||
// Clear search before wiping content
|
// Clear search before wiping content
|
||||||
clearReaderSearch();
|
clearReaderSearch();
|
||||||
|
|
@ -4287,8 +4291,7 @@ function applyReaderSettings(isPdf) {
|
||||||
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');
|
document.querySelectorAll('.marker-mode-btn').forEach(b => { b.style.display = isPdf ? 'none' : ''; });
|
||||||
if (markerBtn) markerBtn.style.display = isPdf ? 'none' : '';
|
|
||||||
if (isPdf && markerModeActive) toggleMarkerMode();
|
if (isPdf && markerModeActive) toggleMarkerMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5221,7 +5224,17 @@ let markerModeActive = false;
|
||||||
|
|
||||||
function toggleMarkerMode() {
|
function toggleMarkerMode() {
|
||||||
markerModeActive = !markerModeActive;
|
markerModeActive = !markerModeActive;
|
||||||
$('reader-marker-btn')?.classList.toggle('active', markerModeActive);
|
document.querySelectorAll('.marker-mode-btn').forEach(b => b.classList.toggle('active', markerModeActive));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note editing/creation happens in the margin column on desktop, but a note
|
||||||
|
// bottom sheet (see showNoteBottomSheet) on narrow viewports — typing into a
|
||||||
|
// 150px margin strip on a phone keyboard is unpleasant, and opening that
|
||||||
|
// column at all is exactly the friction we're trying to remove for mobile
|
||||||
|
// note-taking. Reviewing existing notes in depth is expected to stay a
|
||||||
|
// desktop activity; this only affects where the editor for a note appears.
|
||||||
|
function _isMobileReader() {
|
||||||
|
return window.innerWidth <= 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReaderSelection(e) {
|
function handleReaderSelection(e) {
|
||||||
|
|
@ -5236,16 +5249,33 @@ function handleReaderSelection(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!markerModeActive) return;
|
if (!markerModeActive) return;
|
||||||
|
// Don't hijack taps on real interactive content (footnote refs, external
|
||||||
|
// links) — but plenty of EPUBs wrap plain reading text in a bare, hrefless
|
||||||
|
// <a id="..."> used only as an internal jump target (sanitizeEpubHtml
|
||||||
|
// strips its href but the tag itself stays); those aren't actually
|
||||||
|
// clickable and must not block note-taking, so check for a surviving
|
||||||
|
// href/footnote-ref rather than just "is inside some <a>".
|
||||||
|
if (e.target.closest('button, [data-footnote-ref], a[href]')) return;
|
||||||
|
|
||||||
dismissHighlightPopover();
|
dismissHighlightPopover();
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
if (sel && !sel.isCollapsed && sel.rangeCount) {
|
||||||
const range = sel.getRangeAt(0);
|
const range = sel.getRangeAt(0);
|
||||||
const contentEl = $('reader-content');
|
const contentEl = $('reader-content');
|
||||||
if (!contentEl || !contentEl.contains(range.commonAncestorContainer)) return;
|
if (!contentEl || !contentEl.contains(range.commonAncestorContainer)) return;
|
||||||
if (range.toString().trim().length === 0) return;
|
if (range.toString().trim().length === 0) return;
|
||||||
|
|
||||||
showHighlightPopover(range);
|
showHighlightPopover(range);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A plain tap with nothing selected — in marker mode this means "drop a
|
||||||
|
// freeform note here" instead of doing nothing, so a note-only annotation
|
||||||
|
// never depends on finding the margin's precise click target (especially
|
||||||
|
// hard on a narrow phone screen — see createFreeformNote/_openNoteEditor).
|
||||||
|
if (typeof e.clientX === 'number' && typeof e.clientY === 'number') {
|
||||||
|
const range = _wordRangeAtPoint(e.clientX, e.clientY);
|
||||||
|
if (range) createFreeformNote(range);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showHighlightPopover(range) {
|
function showHighlightPopover(range) {
|
||||||
|
|
@ -5329,7 +5359,7 @@ function showHighlightTooltip(markEl, h) {
|
||||||
const delBtn = ev.target.closest('[data-hl-delete]');
|
const delBtn = ev.target.closest('[data-hl-delete]');
|
||||||
if (editBtn && h) {
|
if (editBtn && h) {
|
||||||
dismissHighlightPopover();
|
dismissHighlightPopover();
|
||||||
_openMarginAndEditNote(h);
|
_openNoteEditor(h);
|
||||||
}
|
}
|
||||||
if (delBtn && h) {
|
if (delBtn && h) {
|
||||||
dismissHighlightPopover();
|
dismissHighlightPopover();
|
||||||
|
|
@ -5378,16 +5408,23 @@ function createHighlightWithNote(range) {
|
||||||
dismissHighlightPopover();
|
dismissHighlightPopover();
|
||||||
renderHighlight(h);
|
renderHighlight(h);
|
||||||
_repositionMarginMarkers();
|
_repositionMarginMarkers();
|
||||||
_openMarginAndEditNote(h);
|
_openNoteEditor(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every "add/edit a note" entry point routes here — notes are only ever
|
// Every "add/edit a note" entry point routes here — notes are always edited
|
||||||
// written where they live, in the margin, never in the separate right-hand
|
// right where they conceptually live (the margin), never in the separate
|
||||||
// sidebar (that used to mean a jarring jump away from the text you're
|
// right-hand sidebar, EXCEPT on narrow viewports, where the margin itself is
|
||||||
// annotating).
|
// too cramped to type into and a full-width bottom sheet takes over instead.
|
||||||
function _openMarginAndEditNote(h) {
|
// `pending` is only set for a not-yet-created freeform note (see
|
||||||
|
// createFreeformNote); h is null in that case until the user actually saves
|
||||||
|
// text, so an empty tap doesn't leave a permanent ghost entry behind.
|
||||||
|
function _openNoteEditor(h, pending) {
|
||||||
|
if (_isMobileReader()) {
|
||||||
|
showNoteBottomSheet(h, pending);
|
||||||
|
} else {
|
||||||
if (!readerAnnotationsMarginOpen) toggleAnnotationsMargin();
|
if (!readerAnnotationsMarginOpen) toggleAnnotationsMargin();
|
||||||
editNoteInlineInMargin(h);
|
editNoteInlineInMargin(h, pending);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteHighlight(id) {
|
function deleteHighlight(id) {
|
||||||
|
|
@ -5508,7 +5545,7 @@ function handleMarginClick(e) {
|
||||||
const markerBtn = e.target.closest('.margin-note');
|
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) editNoteInlineInMargin(h);
|
if (h) _openNoteEditor(h);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5575,33 +5612,62 @@ function _wordRangeAtPoint(x, y) {
|
||||||
|
|
||||||
function createFreeformNote(range) {
|
function createFreeformNote(range) {
|
||||||
const anchor = buildEpubAnchor(range);
|
const anchor = buildEpubAnchor(range);
|
||||||
const h = {
|
// Where the note's card/dot would sit in the margin, computed from the live
|
||||||
id: crypto.randomUUID(),
|
// range right now (before it goes stale) — used to position the editor,
|
||||||
type: 'note',
|
// whether or not the margin is actually open.
|
||||||
anchor,
|
let top = 0;
|
||||||
color: null,
|
const markersEl = $('reader-margin-markers');
|
||||||
note: '',
|
if (markersEl) {
|
||||||
createdAt: new Date().toISOString(),
|
const areaRect = markersEl.getBoundingClientRect();
|
||||||
};
|
const rangeRect = range.getBoundingClientRect();
|
||||||
currentHighlights.push(h);
|
top = Math.max(0, rangeRect.top + rangeRect.height / 2 - areaRect.top - 8);
|
||||||
|
}
|
||||||
|
// The highlight object itself isn't created yet — see _openNoteEditor's
|
||||||
|
// `pending` param — so a tap that's immediately cancelled or left empty
|
||||||
|
// never leaves a ghost entry in currentHighlights.
|
||||||
|
_openNoteEditor(null, {anchor, top});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commits an edit for both note-editor UIs (inline margin + mobile bottom
|
||||||
|
// sheet): updates an existing note in place, or — only if there's actual
|
||||||
|
// text — creates and persists a not-yet-existing freeform note. An empty
|
||||||
|
// tap/cancel on a pending note must never leave a permanent ghost entry.
|
||||||
|
function _commitNoteEdit(h, pending, text) {
|
||||||
|
if (h) {
|
||||||
|
if (h.note !== text) {
|
||||||
|
h.note = text;
|
||||||
highlightsDirty = true;
|
highlightsDirty = true;
|
||||||
renderHighlight(h);
|
|
||||||
_repositionMarginMarkers();
|
|
||||||
debounceSaveHighlights();
|
debounceSaveHighlights();
|
||||||
editNoteInlineInMargin(h);
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
if (!text) return null;
|
||||||
|
const newH = {
|
||||||
|
id: crypto.randomUUID(), type: 'note', anchor: pending.anchor,
|
||||||
|
color: null, note: text, createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
currentHighlights.push(newH);
|
||||||
|
highlightsDirty = true;
|
||||||
|
debounceSaveHighlights();
|
||||||
|
return newH;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit a note's text right where it lives in the margin, instead of jumping
|
// Edit a note's text right where it lives in the margin, instead of jumping
|
||||||
// to the separate right-hand sidebar — the whole point of setting a note in
|
// to the separate right-hand sidebar — the whole point of setting a note in
|
||||||
// the margin is that you shouldn't have to look away from it to write it.
|
// the margin is that you shouldn't have to look away from it to write it.
|
||||||
function editNoteInlineInMargin(h) {
|
// `h` is null for a not-yet-created freeform note (see createFreeformNote);
|
||||||
|
// `pending` then carries its anchor + intended margin position instead.
|
||||||
|
function editNoteInlineInMargin(h, pending) {
|
||||||
const markersEl = $('reader-margin-markers');
|
const markersEl = $('reader-margin-markers');
|
||||||
if (!markersEl) return;
|
if (!markersEl) return;
|
||||||
markersEl.querySelector('.margin-note-editor')?.remove();
|
markersEl.querySelector('.margin-note-editor')?.remove();
|
||||||
|
|
||||||
|
let top = pending?.top || 0;
|
||||||
|
if (h) {
|
||||||
const existingCard = markersEl.querySelector(`[data-highlight-id="${h.id}"]`);
|
const existingCard = markersEl.querySelector(`[data-highlight-id="${h.id}"]`);
|
||||||
const top = existingCard ? (parseFloat(existingCard.style.top) || 0) : 0;
|
top = existingCard ? (parseFloat(existingCard.style.top) || 0) : 0;
|
||||||
if (existingCard) existingCard.style.display = 'none';
|
if (existingCard) existingCard.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
const editor = document.createElement('div');
|
const editor = document.createElement('div');
|
||||||
editor.className = 'margin-note-editor';
|
editor.className = 'margin-note-editor';
|
||||||
|
|
@ -5609,15 +5675,10 @@ function editNoteInlineInMargin(h) {
|
||||||
editor.innerHTML = '<textarea class="margin-note-textarea"></textarea>';
|
editor.innerHTML = '<textarea class="margin-note-textarea"></textarea>';
|
||||||
markersEl.appendChild(editor);
|
markersEl.appendChild(editor);
|
||||||
const textarea = editor.querySelector('textarea');
|
const textarea = editor.querySelector('textarea');
|
||||||
textarea.value = h.note || '';
|
textarea.value = h?.note || '';
|
||||||
|
|
||||||
function commit() {
|
function commit() {
|
||||||
const text = textarea.value.trim();
|
_commitNoteEdit(h, pending, textarea.value.trim());
|
||||||
if (h.note !== text) {
|
|
||||||
h.note = text;
|
|
||||||
highlightsDirty = true;
|
|
||||||
debounceSaveHighlights();
|
|
||||||
}
|
|
||||||
editor.remove();
|
editor.remove();
|
||||||
_repositionMarginMarkers();
|
_repositionMarginMarkers();
|
||||||
}
|
}
|
||||||
|
|
@ -5643,6 +5704,56 @@ function editNoteInlineInMargin(h) {
|
||||||
textarea.select();
|
textarea.select();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mobile alternative to editNoteInlineInMargin — a full-width sheet instead
|
||||||
|
// of squeezing a textarea into the 150px margin column (see _openNoteEditor).
|
||||||
|
let currentNoteBottomSheet = null;
|
||||||
|
|
||||||
|
function dismissNoteBottomSheet() {
|
||||||
|
currentNoteBottomSheet?.remove();
|
||||||
|
currentNoteBottomSheet = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNoteBottomSheet(h, pending) {
|
||||||
|
dismissNoteBottomSheet();
|
||||||
|
|
||||||
|
const sheet = document.createElement('div');
|
||||||
|
sheet.className = 'note-bottom-sheet';
|
||||||
|
sheet.innerHTML = `
|
||||||
|
<div class="note-bottom-sheet-backdrop"></div>
|
||||||
|
<div class="note-bottom-sheet-panel">
|
||||||
|
<textarea class="note-bottom-sheet-textarea" placeholder="Notiz…"></textarea>
|
||||||
|
<div class="note-bottom-sheet-actions">
|
||||||
|
<button class="btn btn-sm" data-nbs-cancel>Abbrechen</button>
|
||||||
|
<button class="btn btn-sm btn-primary" data-nbs-save>Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(sheet);
|
||||||
|
currentNoteBottomSheet = sheet;
|
||||||
|
|
||||||
|
const textarea = sheet.querySelector('.note-bottom-sheet-textarea');
|
||||||
|
textarea.value = h?.note || '';
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
_commitNoteEdit(h, pending, textarea.value.trim());
|
||||||
|
dismissNoteBottomSheet();
|
||||||
|
if (readerAnnotationsMarginOpen) _repositionMarginMarkers();
|
||||||
|
}
|
||||||
|
function cancel() { dismissNoteBottomSheet(); }
|
||||||
|
|
||||||
|
sheet.querySelector('[data-nbs-save]').addEventListener('click', commit);
|
||||||
|
sheet.querySelector('[data-nbs-cancel]').addEventListener('click', cancel);
|
||||||
|
sheet.querySelector('.note-bottom-sheet-backdrop').addEventListener('click', cancel);
|
||||||
|
textarea.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Escape') { e.stopPropagation(); cancel(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
sheet.classList.add('open');
|
||||||
|
textarea.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Same plain-text export as exportAnnotations(), but callable straight from the
|
// Same plain-text export as exportAnnotations(), but callable straight from the
|
||||||
// book list (no open reader, so no live DOM/TOC to resolve percent position or
|
// book list (no open reader, so no live DOM/TOC to resolve percent position or
|
||||||
// chapter titles against — falls back to the raw chapterSrc path and orders by
|
// chapter titles against — falls back to the raw chapterSrc path and orders by
|
||||||
|
|
@ -6073,7 +6184,10 @@ function openRadioSidebar() {
|
||||||
_readerContentEl.addEventListener('mouseup', handleReaderSelection);
|
_readerContentEl.addEventListener('mouseup', handleReaderSelection);
|
||||||
_readerContentEl.addEventListener('touchend', e => {
|
_readerContentEl.addEventListener('touchend', e => {
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
setTimeout(() => handleReaderSelection({target}), 50);
|
const touch = e.changedTouches && e.changedTouches[0];
|
||||||
|
const clientX = touch ? touch.clientX : undefined;
|
||||||
|
const clientY = touch ? touch.clientY : undefined;
|
||||||
|
setTimeout(() => handleReaderSelection({target, clientX, clientY}), 50);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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-v31';
|
const CACHE = 'diora-v32';
|
||||||
const PODCAST_CACHE = 'diora-podcast-v1';
|
const PODCAST_CACHE = 'diora-podcast-v1';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/static/css/app.css',
|
'/static/css/app.css',
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,7 @@
|
||||||
<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-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 marker-mode-btn reader-marker-btn-mobile" id="reader-marker-btn-mobile" onclick="toggleMarkerMode()" title="Text markieren">✒</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -357,7 +358,7 @@
|
||||||
<div class="reader-margin-header">
|
<div class="reader-margin-header">
|
||||||
<span class="reader-margin-title">Notes</span>
|
<span class="reader-margin-title">Notes</span>
|
||||||
<span class="reader-margin-header-actions">
|
<span class="reader-margin-header-actions">
|
||||||
<button class="btn-icon" id="reader-marker-btn" onclick="toggleMarkerMode()" title="Mark text">✒</button>
|
<button class="btn-icon marker-mode-btn" id="reader-marker-btn" onclick="toggleMarkerMode()" title="Mark text">✒</button>
|
||||||
<button class="btn-icon" id="reader-margin-list-btn" onclick="openAnnotationsSidebar()" title="List all highlights & notes, jump to any of them">▦</button>
|
<button class="btn-icon" id="reader-margin-list-btn" onclick="openAnnotationsSidebar()" title="List all highlights & notes, jump to any of them">▦</button>
|
||||||
<button class="btn-icon" id="reader-margin-export-btn" onclick="exportAnnotations()" title="Export as text file">⭳</button>
|
<button class="btn-icon" id="reader-margin-export-btn" onclick="exportAnnotations()" title="Export as text file">⭳</button>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue