Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Reading in pages ("Seiten" in the reader settings) — in the browser, the
|
|
|
|
|
|
// desktop app and the Android apps, whose e-reader mode always reads in pages
|
|
|
|
|
|
// (diora-android native-eink.js turns it on with DioraPages.force).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Pages without rewriting the reader: the book stays one long vertical
|
|
|
|
|
|
// document (so position anchors, highlights, TOC, search and read-aloud keep
|
|
|
|
|
|
// working and stay the same on every device), but free scrolling is off and
|
|
|
|
|
|
// the pager moves exactly one page at a time. A page ends before the first
|
|
|
|
|
|
// line that would be cut off (masked), a new chapter starts a new page, and
|
|
|
|
|
|
// the notes of the references on a page stand at its foot, like in a printed
|
|
|
|
|
|
// book. Only the current chapter is laid out; the others stay in the DOM
|
|
|
|
|
|
// (display:none), so block indices, search, notes and anchors see the whole
|
|
|
|
|
|
// book as before. Chrome-61-safe (tolino).
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
(function () {
|
|
|
|
|
|
const TOP_GAP = 14; // blank band above the first line of a page
|
|
|
|
|
|
const NOTES_GAP = 10; // space between the text and the notes' rule
|
|
|
|
|
|
const KEY_ON = 'diora_pages';
|
|
|
|
|
|
const KEY_NOTES = 'diora_eink_pagenotes'; // named after the e-reader mode, where pages began
|
|
|
|
|
|
const KEY_SPREAD = 'diora_pages_spread';
|
|
|
|
|
|
|
|
|
|
|
|
const get = (k, d) => { try { const v = localStorage.getItem(k); return v === null ? d : v; } catch (e) { return d; } };
|
|
|
|
|
|
const set = (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} };
|
|
|
|
|
|
|
|
|
|
|
|
let wanted = get(KEY_ON, '0') === '1'; // the reader's choice on this device
|
|
|
|
|
|
let forced = false; // e-reader mode (Android): always pages
|
|
|
|
|
|
let on = wanted;
|
|
|
|
|
|
let pageNotes = get(KEY_NOTES, '1') === '1';
|
|
|
|
|
|
const turnHooks = [];
|
|
|
|
|
|
|
|
|
|
|
|
const content = () => document.getElementById('reader-content');
|
|
|
|
|
|
const readerOpen = () => {
|
|
|
|
|
|
const o = document.getElementById('reader-overlay');
|
|
|
|
|
|
return !!o && o.style.display !== 'none';
|
|
|
|
|
|
};
|
|
|
|
|
|
const isPdf = () => typeof currentPdfDoc !== 'undefined' && !!currentPdfDoc;
|
|
|
|
|
|
const paging = () => on && readerOpen() && !isPdf();
|
|
|
|
|
|
|
|
|
|
|
|
// ---- page geometry ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// Rendered line boxes of the text at (x, y). Only text nodes count: an
|
|
|
|
|
|
// element's own box (a whole paragraph) is not a line and must never be
|
|
|
|
|
|
// taken for one, or a long paragraph would push the page end up to its top.
|
|
|
|
|
|
const BLOCKS = 'p, li, dd, dt, blockquote, h1, h2, h3, h4, h5, h6, figcaption, pre, td, div';
|
|
|
|
|
|
function linesAt(c, x, y) {
|
|
|
|
|
|
const el = bookElementAt(c, x, y);
|
|
|
|
|
|
if (!el || el === c) return [];
|
|
|
|
|
|
const block = el.closest(BLOCKS);
|
|
|
|
|
|
if (!block || block === c || block.hasAttribute('data-epub-src') || !c.contains(block)) return [];
|
|
|
|
|
|
const rects = [];
|
|
|
|
|
|
const range = document.createRange();
|
|
|
|
|
|
const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT, null, false);
|
|
|
|
|
|
let node, n = 0;
|
|
|
|
|
|
while ((node = walker.nextNode()) && n++ < 2000) {
|
|
|
|
|
|
if (!node.nodeValue.trim()) continue;
|
|
|
|
|
|
range.selectNodeContents(node);
|
|
|
|
|
|
const list = range.getClientRects();
|
|
|
|
|
|
for (let i = 0; i < list.length; i++) if (list[i].height > 0) rects.push(list[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
return rects;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The book's element at (x, y) — also under a toast or a popover lying over the page.
|
|
|
|
|
|
function bookElementAt(c, x, y) {
|
|
|
|
|
|
if (document.elementsFromPoint) {
|
|
|
|
|
|
const list = document.elementsFromPoint(x, y);
|
|
|
|
|
|
for (let i = 0; i < list.length; i++) if (c.contains(list[i])) return list[i];
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
const el = document.elementFromPoint(x, y);
|
|
|
|
|
|
return el && c.contains(el) ? el : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Where to look for lines: across the text column (on a wide screen the
|
|
|
|
|
|
// reader's edges are empty margin).
|
|
|
|
|
|
function probeXs(c) {
|
|
|
|
|
|
const col = columnOf(c);
|
|
|
|
|
|
return [col.left + 24, col.left + col.width / 2, col.left + col.width - 24];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Viewport y (px) where the current page must end: before the next chapter
|
|
|
|
|
|
// and before any text line or image that would only be partly visible.
|
|
|
|
|
|
function pageLimit(c, maxY) {
|
|
|
|
|
|
const box = c.getBoundingClientRect();
|
|
|
|
|
|
const top = box.top;
|
|
|
|
|
|
let limit = maxY !== undefined ? maxY : box.top + c.clientHeight;
|
|
|
|
|
|
// A new chapter starts a new page — but only the chapter *after* the one
|
|
|
|
|
|
// this page is in (the first chapter itself may start a bit down the page,
|
|
|
|
|
|
// below the reader's top padding and its heading's margin).
|
|
|
|
|
|
const chapters = chapterList(c);
|
|
|
|
|
|
for (let i = 0; i < chapters.length; i++) {
|
|
|
|
|
|
if (chapters[i].getBoundingClientRect().bottom <= top + TOP_GAP) continue;
|
|
|
|
|
|
const nextCh = chapters[i + 1];
|
|
|
|
|
|
if (nextCh) {
|
|
|
|
|
|
const t = nextCh.getBoundingClientRect().top;
|
|
|
|
|
|
if (t > top + TOP_GAP + 40 && t < limit) limit = t;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
const probeY = limit - 1;
|
|
|
|
|
|
for (const x of probeXs(c)) {
|
|
|
|
|
|
const el = bookElementAt(c, x, probeY);
|
|
|
|
|
|
if (el && c.contains(el) && /^(IMG|svg|SVG|image)$/.test(el.tagName)) {
|
|
|
|
|
|
const r = el.getBoundingClientRect();
|
|
|
|
|
|
if (r.top > top + TOP_GAP && r.height < c.clientHeight * 0.8) limit = Math.min(limit, r.top);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (const r of linesAt(c, x, probeY)) {
|
|
|
|
|
|
if (r.top < limit && r.bottom > limit + 0.5 && r.top > top + TOP_GAP) limit = Math.min(limit, r.top);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return limit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Make the first line of the page start right at TOP_GAP. `showCut`: if a
|
|
|
|
|
|
// line is cut at the top, scroll up to show it whole (used after jumps, so
|
|
|
|
|
|
// the jump target stays visible); otherwise start at the next line.
|
|
|
|
|
|
function alignTop(c, showCut) {
|
|
|
|
|
|
const box = c.getBoundingClientRect();
|
|
|
|
|
|
const edge = box.top + TOP_GAP;
|
|
|
|
|
|
for (const x of probeXs(c)) {
|
|
|
|
|
|
for (const r of linesAt(c, x, edge + 1)) {
|
|
|
|
|
|
if (r.top < edge - 0.5 && r.bottom > edge) {
|
|
|
|
|
|
ownScroll(c, showCut ? c.scrollTop - (edge - r.top) : c.scrollTop + (r.bottom - edge));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- masks: hide the cut line at the bottom and whatever sits in the top gap ----
|
|
|
|
|
|
|
|
|
|
|
|
// The colour behind the text: the reader's own, or the first opaque one around it.
|
|
|
|
|
|
function backgroundOf(el) {
|
|
|
|
|
|
for (; el && el.nodeType === 1; el = el.parentElement) {
|
|
|
|
|
|
const bg = getComputedStyle(el).backgroundColor;
|
|
|
|
|
|
if (bg && bg !== 'transparent' && !/^rgba\(.*,\s*0\)$/.test(bg)) return bg;
|
|
|
|
|
|
}
|
|
|
|
|
|
return '#fff';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// What lies over one page: a mask for the top gap, one from the cut line
|
|
|
|
|
|
// down, and the notes. The book's page has one, the right page of a spread another.
|
|
|
|
|
|
function makeCover() {
|
|
|
|
|
|
const k = {top: document.createElement('div'), bottom: document.createElement('div'), notes: makeNotesBox(), refs: []};
|
|
|
|
|
|
k.top.className = k.bottom.className = 'nui-page-mask';
|
|
|
|
|
|
document.body.appendChild(k.top);
|
|
|
|
|
|
document.body.appendChild(k.bottom);
|
|
|
|
|
|
return k;
|
|
|
|
|
|
}
|
|
|
|
|
|
let cover = null; // over #reader-content
|
|
|
|
|
|
function mainCover() {
|
|
|
|
|
|
if (!cover) {
|
|
|
|
|
|
cover = makeCover();
|
|
|
|
|
|
cover.notes.id = 'nui-page-notes';
|
|
|
|
|
|
}
|
|
|
|
|
|
return cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
let rightCover = null; // over the right page of a spread
|
|
|
|
|
|
function hideCover(k) {
|
|
|
|
|
|
if (!k) return;
|
|
|
|
|
|
k.top.style.display = k.bottom.style.display = k.notes.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
function hideMasks() {
|
|
|
|
|
|
hideCover(cover);
|
|
|
|
|
|
hideCover(rightCover);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Lay `page` ({limit, notesH}, its notes already filled in k.notes) over the
|
|
|
|
|
|
// pane `el` (its viewport box `box`); `blank`: nothing of the text shows.
|
|
|
|
|
|
function placeCover(k, el, box, bg, page, gap, blank) {
|
|
|
|
|
|
const bottom = box.top + el.clientHeight;
|
|
|
|
|
|
const place = (m, t, h) => {
|
|
|
|
|
|
m.style.cssText = `display:${h > 0 ? 'block' : 'none'};left:${box.left}px;width:${el.clientWidth}px;top:${t}px;height:${h}px;background:${bg}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (blank) {
|
|
|
|
|
|
place(k.top, box.top, el.clientHeight + 5);
|
|
|
|
|
|
k.bottom.style.display = k.notes.style.display = 'none';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
place(k.top, box.top, gap ? TOP_GAP : 0);
|
|
|
|
|
|
// Down to the bottom, across the whole width: the notes lie on it, only as wide as the text.
|
|
|
|
|
|
place(k.bottom, page.limit - 2, bottom - page.limit + 2 + 5);
|
|
|
|
|
|
if (page.notesH) {
|
|
|
|
|
|
const col = columnOf(el);
|
|
|
|
|
|
k.notes.style.left = col.left + 'px';
|
|
|
|
|
|
k.notes.style.width = col.width + 'px';
|
|
|
|
|
|
k.notes.style.top = (bottom - page.notesH) + 'px';
|
|
|
|
|
|
k.notes.style.height = (page.notesH + 4) + 'px';
|
|
|
|
|
|
k.notes.style.background = bg;
|
|
|
|
|
|
k.notes.style.color = getComputedStyle(el).color;
|
|
|
|
|
|
k.notes.style.visibility = 'visible';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
k.notes.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function updateMasks() {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!paging() || !c) { hideMasks(); showSpread(false); renderFooter(); return; }
|
|
|
|
|
|
if (showSpread(spreadFits())) relayout(0); // the page got narrower or wider: line it up again
|
|
|
|
|
|
mainCover();
|
|
|
|
|
|
const box = c.getBoundingClientRect();
|
|
|
|
|
|
const bg = backgroundOf(c);
|
|
|
|
|
|
const page = layoutPage(c, cover);
|
|
|
|
|
|
placeCover(cover, c, box, bg, page, c.scrollTop > 0, false);
|
|
|
|
|
|
if (spreadShown) renderRightPage(c, box, bg, page);
|
|
|
|
|
|
else hideCover(rightCover);
|
|
|
|
|
|
renderFooter();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- footnotes at the foot of the page, like a printed book -------------------
|
|
|
|
|
|
// The notes of the references on this page go below its text; the text area
|
|
|
|
|
|
// shrinks to make room. A reference that no longer fits moves to the next
|
|
|
|
|
|
// page and takes its note along (settled in a few passes). The notes are
|
|
|
|
|
|
// found like everywhere else (78-footnotes.js: noteForLink).
|
|
|
|
|
|
|
|
|
|
|
|
function makeNotesBox() {
|
|
|
|
|
|
const box = document.createElement('div');
|
|
|
|
|
|
box.className = 'nui-page-notes';
|
|
|
|
|
|
box.style.display = 'none';
|
|
|
|
|
|
// A tap on a note opens it whole (long ones are cut off here) and turns no page.
|
|
|
|
|
|
box.addEventListener('click', e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
const item = e.target.closest('[data-note-idx]');
|
|
|
|
|
|
const k = [cover, rightCover].find(x => x && x.notes === box);
|
|
|
|
|
|
const link = item && k && k.refs[+item.dataset.noteIdx];
|
|
|
|
|
|
if (link) openNote(link, k === rightCover ? item : null);
|
|
|
|
|
|
});
|
|
|
|
|
|
document.body.appendChild(box);
|
|
|
|
|
|
return box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// A note opened from a page. `near`: show it there — the reference itself is
|
|
|
|
|
|
// not on screen (the right page of a spread is a copy).
|
|
|
|
|
|
function openNote(link, near) {
|
|
|
|
|
|
if (window.DioraUI && DioraUI.openNote) { DioraUI.openNote(link); return; }
|
|
|
|
|
|
showFootnotePopover(link);
|
|
|
|
|
|
const p = currentFootnotePopover;
|
|
|
|
|
|
if (!p || !near) return;
|
|
|
|
|
|
const r = near.getBoundingClientRect();
|
|
|
|
|
|
const pr = p.getBoundingClientRect();
|
|
|
|
|
|
let top = r.top - pr.height - 8;
|
|
|
|
|
|
if (top < 8) top = r.bottom + 8;
|
|
|
|
|
|
p.style.top = Math.max(8, Math.min(window.innerHeight - pr.height - 8, top)) + 'px';
|
|
|
|
|
|
p.style.left = Math.max(8, Math.min(window.innerWidth - pr.width - 8, r.left)) + 'px';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function refsOnPage(c, top, limit) {
|
|
|
|
|
|
const out = [];
|
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
|
const refs = ((on && currentChapter(c)) || c).querySelectorAll('[data-footnote-ref]');
|
|
|
|
|
|
for (let i = 0; i < refs.length; i++) {
|
|
|
|
|
|
const r = refs[i].getBoundingClientRect();
|
|
|
|
|
|
// On the page's first line a raised number (<sup>) reaches above the
|
|
|
|
|
|
// page's top edge: its bottom tells (it belongs to the page before only
|
|
|
|
|
|
// when its whole line was there).
|
|
|
|
|
|
if (r.height === 0 || r.bottom <= top + 1) continue;
|
|
|
|
|
|
if (r.top > limit + 4000) break; // document order: far below, done
|
|
|
|
|
|
if (r.bottom > limit) continue;
|
|
|
|
|
|
const key = refs[i].dataset.footnoteHref || refs[i].dataset.footnoteRef;
|
|
|
|
|
|
if (seen.has(key)) continue;
|
|
|
|
|
|
seen.add(key);
|
|
|
|
|
|
out.push(refs[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The text column (the chapter, at most --reader-max-width wide on a wide
|
|
|
|
|
|
// screen) of a pane: the notes line up with the text above them.
|
|
|
|
|
|
function columnOf(el) {
|
|
|
|
|
|
const ch = el.querySelector(':scope > .nui-ch-current') || el.querySelector(':scope > [data-epub-src]');
|
|
|
|
|
|
const r = ch && ch.getBoundingClientRect();
|
|
|
|
|
|
if (r && r.width) return {left: r.left, width: r.width};
|
|
|
|
|
|
return {left: el.getBoundingClientRect().left, width: el.clientWidth};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Put the notes for `refs` into k.notes; returns the height they need.
|
|
|
|
|
|
function fillNotes(c, refs, k) {
|
|
|
|
|
|
const box = k.notes;
|
|
|
|
|
|
const cs = getComputedStyle(c);
|
|
|
|
|
|
box.style.cssText = `display:block;visibility:hidden;left:0;top:0;height:auto;width:${columnOf(c).width}px;` +
|
|
|
|
|
|
`font-family:${cs.fontFamily};font-size:${(parseFloat(cs.fontSize) * 0.8).toFixed(1)}px`;
|
|
|
|
|
|
box.innerHTML = '<div class="nui-pn-rule"></div>';
|
|
|
|
|
|
k.refs = refs;
|
|
|
|
|
|
refs.forEach((ref, i) => {
|
|
|
|
|
|
const note = noteForLink(ref);
|
|
|
|
|
|
const item = document.createElement('div');
|
|
|
|
|
|
item.className = 'nui-pn-item';
|
|
|
|
|
|
item.dataset.noteIdx = i;
|
|
|
|
|
|
const label = document.createElement('span');
|
|
|
|
|
|
label.className = 'nui-pn-label';
|
|
|
|
|
|
label.textContent = (ref.textContent || '').trim().replace(/^[\[(]\s*|\s*[\])]$/g, '');
|
|
|
|
|
|
item.appendChild(label);
|
|
|
|
|
|
if (note) item.appendChild(note);
|
|
|
|
|
|
else item.appendChild(document.createTextNode('(Fußnote nicht gefunden)'));
|
|
|
|
|
|
box.appendChild(item);
|
|
|
|
|
|
});
|
|
|
|
|
|
return box.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Where the page shown in #reader-content ends ({limit, notesH}), its notes
|
|
|
|
|
|
// filled into k (the book page's cover unless said otherwise).
|
|
|
|
|
|
function layoutPage(c, k) {
|
|
|
|
|
|
let limit = pageLimit(c);
|
|
|
|
|
|
if (!pageNotes) return {limit, notesH: 0};
|
|
|
|
|
|
if (!k) k = mainCover();
|
|
|
|
|
|
const box = c.getBoundingClientRect();
|
|
|
|
|
|
const bottom = box.top + c.clientHeight;
|
|
|
|
|
|
const maxH = Math.round(c.clientHeight * 0.45);
|
|
|
|
|
|
let refs = refsOnPage(c, box.top + TOP_GAP, limit);
|
|
|
|
|
|
for (let pass = 0; pass < 6 && refs.length; pass++) {
|
|
|
|
|
|
const needed = fillNotes(c, refs, k);
|
|
|
|
|
|
const h = Math.min(maxH, needed);
|
|
|
|
|
|
k.notes.classList.toggle('nui-pn-cut', needed > maxH);
|
|
|
|
|
|
const textLimit = pageLimit(c, Math.min(limit, bottom - h - NOTES_GAP));
|
|
|
|
|
|
const fitting = refs.filter(r => r.getBoundingClientRect().bottom <= textLimit);
|
|
|
|
|
|
if (fitting.length === refs.length) return {limit: textLimit, notesH: h};
|
|
|
|
|
|
// A reference whose line the notes push off goes to the next page and
|
|
|
|
|
|
// takes its note along: this page ends above that line.
|
|
|
|
|
|
// (Measured in the middle of the reference: its line is cut there.)
|
|
|
|
|
|
const dropped = refs.filter(r => fitting.indexOf(r) < 0)[0].getBoundingClientRect();
|
|
|
|
|
|
limit = pageLimit(c, Math.min(limit, (dropped.top + dropped.bottom) / 2));
|
|
|
|
|
|
refs = refsOnPage(c, box.top + TOP_GAP, limit);
|
|
|
|
|
|
}
|
|
|
|
|
|
k.notes.style.display = 'none';
|
|
|
|
|
|
k.refs = [];
|
|
|
|
|
|
return {limit, notesH: 0};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- two pages side by side, like an open book ---------------------------------------
|
|
|
|
|
|
// On a wide screen ("Doppelseite"): the book's page is the left one — position,
|
|
|
|
|
|
// marking, notes, read-aloud all happen there — and the right page shows the
|
|
|
|
|
|
// page after it: a copy of the chapter beside it, scrolled to where the next
|
|
|
|
|
|
// page begins, masked and with its own notes. Laid out by the same rules: the
|
|
|
|
|
|
// book is scrolled there for a moment and measured. Turning moves two pages.
|
|
|
|
|
|
|
|
|
|
|
|
const SPREAD_MIN = 900; // px of reader width below which one page stays
|
|
|
|
|
|
let spreadWanted = get(KEY_SPREAD, '0') === '1';
|
|
|
|
|
|
let spreadShown = false;
|
|
|
|
|
|
let pane = null; // #reader-spread-page
|
|
|
|
|
|
let paneChapter = null; // the chapter its copy is of
|
|
|
|
|
|
let paneDirty = true; // the book changed (marks, search hits, read-aloud) since
|
|
|
|
|
|
|
|
|
|
|
|
function spreadFits() {
|
|
|
|
|
|
const row = document.getElementById('reader-body-row');
|
|
|
|
|
|
return spreadWanted && paging() && !!row && row.clientWidth >= SPREAD_MIN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show or hide the right page; true if that changed the page's width.
|
|
|
|
|
|
function showSpread(show) {
|
|
|
|
|
|
const overlay = document.getElementById('reader-overlay');
|
|
|
|
|
|
if (show === spreadShown || !overlay) return false;
|
|
|
|
|
|
if (show && !pane) {
|
|
|
|
|
|
pane = document.createElement('div');
|
|
|
|
|
|
pane.id = 'reader-spread-page';
|
|
|
|
|
|
pane.className = 'reader-content';
|
|
|
|
|
|
pane.setAttribute('aria-hidden', 'true');
|
|
|
|
|
|
pane.addEventListener('click', onPaneClick, true);
|
|
|
|
|
|
pane.addEventListener('wheel', onWheel, {passive: false});
|
|
|
|
|
|
pane.addEventListener('touchstart', onTouchStart, {passive: true});
|
|
|
|
|
|
pane.addEventListener('touchend', onTouchEnd, {passive: true});
|
|
|
|
|
|
pane.addEventListener('pointerdown', e => { pointer = e.pointerType || 'mouse'; }, true);
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
c.parentNode.insertBefore(pane, c.nextSibling);
|
|
|
|
|
|
}
|
|
|
|
|
|
spreadShown = show;
|
|
|
|
|
|
overlay.classList.toggle('reader-spread', show);
|
|
|
|
|
|
if (!show) hideCover(rightCover);
|
|
|
|
|
|
history = [];
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function syncPane(c) {
|
|
|
|
|
|
const ch = currentChapter(c);
|
|
|
|
|
|
if (paneDirty || paneChapter !== ch) {
|
|
|
|
|
|
pane.textContent = '';
|
|
|
|
|
|
if (ch) {
|
|
|
|
|
|
const copy = ch.cloneNode(true);
|
|
|
|
|
|
// No second element with the book's ids (getElementById must find the book's).
|
|
|
|
|
|
copy.removeAttribute('id');
|
|
|
|
|
|
copy.querySelectorAll('[id]').forEach(e => e.removeAttribute('id'));
|
|
|
|
|
|
pane.appendChild(copy);
|
|
|
|
|
|
}
|
|
|
|
|
|
paneChapter = ch;
|
|
|
|
|
|
paneDirty = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Font, size, line height, width: what the reader settings put on the book's page.
|
|
|
|
|
|
pane.style.cssText = c.style.cssText;
|
|
|
|
|
|
const cs = getComputedStyle(c);
|
|
|
|
|
|
pane.style.padding = cs.paddingTop + ' ' + cs.paddingRight + ' ' + cs.paddingBottom + ' ' + cs.paddingLeft;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The page after the one shown, on the right — `left` is the shown page's layout.
|
|
|
|
|
|
function renderRightPage(c, box, bg, left) {
|
|
|
|
|
|
if (!rightCover) rightCover = makeCover();
|
|
|
|
|
|
syncPane(c);
|
|
|
|
|
|
const pbox = pane.getBoundingClientRect();
|
|
|
|
|
|
const ch = currentChapter(c);
|
|
|
|
|
|
// The chapter ends on the left page: the right one stays empty (a new chapter starts a new spread).
|
|
|
|
|
|
if (!ch || ch.getBoundingClientRect().bottom <= left.limit + 1) {
|
|
|
|
|
|
placeCover(rightCover, pane, pbox, bg, null, false, true);
|
|
|
|
|
|
pane.dataset.top = '';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const top0 = c.scrollTop;
|
|
|
|
|
|
let step = left.limit - box.top - TOP_GAP;
|
|
|
|
|
|
if (step < 40) step = c.clientHeight - TOP_GAP;
|
|
|
|
|
|
ownScroll(c, top0 + step);
|
|
|
|
|
|
const rightTop = c.scrollTop;
|
|
|
|
|
|
const right = layoutPage(c, rightCover);
|
|
|
|
|
|
// The right page's layout in the book's coordinates: the panes share their top.
|
|
|
|
|
|
c.scrollTop = top0;
|
|
|
|
|
|
expectedTop = c.scrollTop;
|
|
|
|
|
|
pane.scrollTop = rightTop;
|
|
|
|
|
|
pane.dataset.top = String(rightTop);
|
|
|
|
|
|
placeCover(rightCover, pane, pbox, bg, right, true, false);
|
|
|
|
|
|
// The notes above were filled before the book scrolled back: still right.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// A tap on the right page: a reference opens its note (the book's own
|
|
|
|
|
|
// reference, found by its place among the chapter's); else as on the left.
|
|
|
|
|
|
function onPaneClick(e) {
|
|
|
|
|
|
const ref = e.target.closest('[data-footnote-ref]');
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (ref && c && currentChapter(c)) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
const copies = pane.querySelectorAll('[data-footnote-ref]');
|
|
|
|
|
|
const originals = currentChapter(c).querySelectorAll('[data-footnote-ref]');
|
|
|
|
|
|
const i = Array.prototype.indexOf.call(copies, ref);
|
|
|
|
|
|
if (originals[i]) openNote(originals[i], ref);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
onClick(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- chapter window ----------------------------------------------------------------
|
|
|
|
|
|
// Only the current chapter is laid out (#reader-content.nui-window, app.css):
|
|
|
|
|
|
// layout (font changes, paging) shrinks from the whole book to one chapter.
|
|
|
|
|
|
|
|
|
|
|
|
const chapterList = c => c.querySelectorAll(':scope > [data-epub-src]');
|
|
|
|
|
|
const currentChapter = c => c.querySelector(':scope > .nui-ch-current');
|
|
|
|
|
|
|
|
|
|
|
|
function chapterOf(node) {
|
|
|
|
|
|
const el = node && node.nodeType !== 1 ? node.parentElement : node;
|
|
|
|
|
|
return el && el.closest ? el.closest('#reader-content > [data-epub-src]') : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function siblingChapter(ch, dir) {
|
|
|
|
|
|
let sib = dir > 0 ? ch.nextElementSibling : ch.previousElementSibling;
|
|
|
|
|
|
while (sib && !sib.hasAttribute('data-epub-src')) sib = dir > 0 ? sib.nextElementSibling : sib.previousElementSibling;
|
|
|
|
|
|
return sib;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Make `ch` the visible chapter. where: 'top' scrolls to its start,
|
|
|
|
|
|
// otherwise the caller positions (a jump that follows).
|
|
|
|
|
|
function showChapter(ch, where) {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!c || !ch) return false;
|
|
|
|
|
|
const cur = currentChapter(c);
|
|
|
|
|
|
if (cur === ch) return false;
|
|
|
|
|
|
if (typeof _suppressScrollJumpDetect === 'function') _suppressScrollJumpDetect();
|
|
|
|
|
|
if (cur) cur.classList.remove('nui-ch-current');
|
|
|
|
|
|
ch.classList.add('nui-ch-current');
|
|
|
|
|
|
history = [];
|
|
|
|
|
|
fractionCache = null;
|
|
|
|
|
|
if (where === 'top') ownScroll(c, 0);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Before anything scrolls to an element: bring its chapter in.
|
|
|
|
|
|
function reveal(node) {
|
|
|
|
|
|
if (!on) return false;
|
|
|
|
|
|
const ch = chapterOf(node);
|
|
|
|
|
|
return ch ? showChapter(ch, null) : false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ensureChapter(c) {
|
|
|
|
|
|
if (on && c && !isPdf() && !currentChapter(c)) {
|
|
|
|
|
|
const first = chapterList(c)[0];
|
|
|
|
|
|
if (first) showChapter(first, 'top');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Page tops of the (visible) chapter, walked like reading forward — so going
|
|
|
|
|
|
// back into a chapter lands exactly on its last page and back from there.
|
|
|
|
|
|
function chapterPageTops(c) {
|
|
|
|
|
|
const tops = [];
|
|
|
|
|
|
ownScroll(c, 0);
|
|
|
|
|
|
for (let i = 0; i < 2000; i++) {
|
|
|
|
|
|
tops.push(c.scrollTop);
|
|
|
|
|
|
if (atEnd(c)) break;
|
|
|
|
|
|
const box = c.getBoundingClientRect();
|
|
|
|
|
|
let step = layoutPage(c).limit - box.top - TOP_GAP;
|
|
|
|
|
|
if (step < 40) step = c.clientHeight - TOP_GAP;
|
|
|
|
|
|
ownScroll(c, c.scrollTop + step);
|
|
|
|
|
|
if (c.scrollTop <= tops[tops.length - 1]) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
return tops;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Share of the book's text read (44-books.js): the scroll position only knows
|
|
|
|
|
|
// the visible chapter.
|
|
|
|
|
|
let fractionCache = null;
|
|
|
|
|
|
function globalFraction(c) {
|
|
|
|
|
|
if (!on || isPdf() || !c) return null;
|
|
|
|
|
|
const cur = currentChapter(c);
|
|
|
|
|
|
const key = c.scrollTop + '|' + c.clientHeight + '|' + (cur ? cur.id : '');
|
|
|
|
|
|
if (fractionCache && fractionCache.key === key) return fractionCache.value;
|
|
|
|
|
|
const value = _fractionFromAnchor(c, getPositionAnchor(c));
|
|
|
|
|
|
fractionCache = {key, value};
|
|
|
|
|
|
return value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function jumpToFraction(fraction) {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!c) return;
|
|
|
|
|
|
_suppressScrollJumpDetect();
|
|
|
|
|
|
restoreFromAnchor(c, _anchorForFraction(c, fraction));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- turning pages ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
let expectedTop = null; // scrollTop our own last move produced
|
|
|
|
|
|
let history = []; // page tops while reading forward, for exact "back"
|
|
|
|
|
|
|
|
|
|
|
|
function ownScroll(c, top) {
|
|
|
|
|
|
const max = c.scrollHeight - c.clientHeight;
|
|
|
|
|
|
top = Math.max(0, Math.min(max, Math.round(top)));
|
|
|
|
|
|
expectedTop = top;
|
|
|
|
|
|
// Pages turned quickly add up to a long way: still no accidental fling.
|
|
|
|
|
|
if (top !== c.scrollTop) _suppressScrollJumpDetect();
|
|
|
|
|
|
c.scrollTop = top;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function atEnd(c) {
|
|
|
|
|
|
const ch = on && currentChapter(c);
|
|
|
|
|
|
if (ch) return ch.getBoundingClientRect().bottom <= layoutPage(c).limit + 1;
|
|
|
|
|
|
return c.scrollTop >= c.scrollHeight - c.clientHeight - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Turning a page is reading (docs/progress-sync.md), however it came.
|
|
|
|
|
|
function turned() {
|
|
|
|
|
|
if (window.__dioraReaderInteraction) window.__dioraReaderInteraction();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// One page forward: 'page', 'chapter' (the next one begins) or 'end' (of the book).
|
|
|
|
|
|
function stepForward(c) {
|
|
|
|
|
|
if (atEnd(c)) {
|
|
|
|
|
|
const nextCh = on && currentChapter(c) && siblingChapter(currentChapter(c), 1);
|
|
|
|
|
|
if (!nextCh) return 'end';
|
|
|
|
|
|
showChapter(nextCh, 'top');
|
|
|
|
|
|
return 'chapter';
|
|
|
|
|
|
}
|
|
|
|
|
|
const box = c.getBoundingClientRect();
|
|
|
|
|
|
let step = layoutPage(c).limit - box.top - TOP_GAP;
|
|
|
|
|
|
if (step < 40) step = c.clientHeight - TOP_GAP; // e.g. an image taller than the page
|
|
|
|
|
|
history.push(c.scrollTop);
|
|
|
|
|
|
if (history.length > 200) history.shift();
|
|
|
|
|
|
ownScroll(c, c.scrollTop + step);
|
|
|
|
|
|
return 'page';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function next() {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!c) return;
|
|
|
|
|
|
turned();
|
|
|
|
|
|
const r = stepForward(c);
|
|
|
|
|
|
if (r === 'end') { toast('Ende des Buchs'); return; }
|
|
|
|
|
|
// A spread: the right page becomes the left one's predecessor — one more.
|
|
|
|
|
|
if (r === 'page' && spreadShown) stepForward(c);
|
|
|
|
|
|
afterTurn();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function prev() {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!c) return;
|
|
|
|
|
|
turned();
|
|
|
|
|
|
if (c.scrollTop <= 0) {
|
|
|
|
|
|
const prevCh = on && currentChapter(c) && siblingChapter(currentChapter(c), -1);
|
|
|
|
|
|
if (!prevCh) return;
|
|
|
|
|
|
showChapter(prevCh, 'top');
|
|
|
|
|
|
const tops = chapterPageTops(c);
|
|
|
|
|
|
// The chapter's last page — in a spread, the left page of its last spread.
|
|
|
|
|
|
let i = tops.length - 1;
|
|
|
|
|
|
if (spreadShown) i -= i % 2;
|
|
|
|
|
|
const last = tops[i];
|
|
|
|
|
|
history = tops.slice(0, i);
|
|
|
|
|
|
ownScroll(c, last);
|
|
|
|
|
|
afterTurn();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let back = history.pop();
|
|
|
|
|
|
if (spreadShown && back !== undefined) {
|
|
|
|
|
|
const before = history.pop();
|
|
|
|
|
|
if (before !== undefined) back = before;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (back !== undefined && back < c.scrollTop) {
|
|
|
|
|
|
ownScroll(c, back);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
history = [];
|
|
|
|
|
|
ownScroll(c, c.scrollTop - (spreadShown ? 2 : 1) * (c.clientHeight - TOP_GAP));
|
|
|
|
|
|
if (c.scrollTop > 0) alignTop(c, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
afterTurn();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function afterTurn() {
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
updateMasks();
|
|
|
|
|
|
if (window.DioraUI && DioraUI.updateFooter) DioraUI.updateFooter();
|
|
|
|
|
|
});
|
|
|
|
|
|
turnHooks.forEach(fn => { try { fn(); } catch (e) {} });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function toast(text) {
|
|
|
|
|
|
const t = document.createElement('div');
|
|
|
|
|
|
t.className = 'reader-toast';
|
|
|
|
|
|
t.textContent = text;
|
|
|
|
|
|
document.body.appendChild(t);
|
|
|
|
|
|
setTimeout(() => t.remove(), 1600);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Line the page up again after the layout changed under it (bars, font, size).
|
|
|
|
|
|
function relayout(delay) {
|
|
|
|
|
|
if (!paging()) { updateMasks(); return; }
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!c || !paging()) return;
|
|
|
|
|
|
if (c.scrollTop > 0) alignTop(c, true);
|
|
|
|
|
|
updateMasks();
|
|
|
|
|
|
if (window.DioraUI && DioraUI.updateFooter) DioraUI.updateFooter();
|
|
|
|
|
|
}, delay || 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Something else moved the text (TOC, search, bookmark, restore on open,
|
|
|
|
|
|
// font change): line it up as a page again and forget the back history.
|
|
|
|
|
|
let realignTimer = null;
|
|
|
|
|
|
function onScroll() {
|
|
|
|
|
|
if (!paging()) return;
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (expectedTop !== null && Math.abs(c.scrollTop - expectedTop) < 2) return;
|
|
|
|
|
|
clearTimeout(realignTimer);
|
|
|
|
|
|
realignTimer = setTimeout(() => {
|
|
|
|
|
|
expectedTop = null;
|
|
|
|
|
|
history = [];
|
|
|
|
|
|
if (c.scrollTop > 0) alignTop(c, true);
|
|
|
|
|
|
updateMasks();
|
|
|
|
|
|
if (window.DioraUI && DioraUI.updateFooter) DioraUI.updateFooter();
|
|
|
|
|
|
}, 120);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- input: taps and clicks, swipes, keys, the mouse wheel -----------------------
|
|
|
|
|
|
|
|
|
|
|
|
let touch = null;
|
|
|
|
|
|
let pointer = 'touch'; // what the last press came from: a mouse turns only at the edges
|
|
|
|
|
|
|
|
|
|
|
|
function interactive(target) {
|
|
|
|
|
|
return target.closest('a, button, input, select, label, mark.epub-highlight, [data-footnote-ref], .footnote-popover');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onClick(e) {
|
|
|
|
|
|
if (!paging() || interactive(e.target)) return;
|
|
|
|
|
|
if (typeof markerModeActive !== 'undefined' && markerModeActive) return;
|
|
|
|
|
|
const sel = window.getSelection();
|
|
|
|
|
|
if (sel && !sel.isCollapsed) return;
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
const box = c.getBoundingClientRect();
|
|
|
|
|
|
const right = spreadShown ? pane : c;
|
|
|
|
|
|
const end = right.getBoundingClientRect().right;
|
|
|
|
|
|
if (pointer === 'mouse') {
|
|
|
|
|
|
// A mouse: only beside the text, so clicking into it (to select a word)
|
|
|
|
|
|
// leaves the page where it is.
|
|
|
|
|
|
if (e.clientX < columnOf(c).left) prev();
|
|
|
|
|
|
else if (e.clientX > columnOf(right).left + columnOf(right).width) next();
|
|
|
|
|
|
else return; // on the text: the reader toggles its bars
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fingers: a third on each side (of both pages in a spread).
|
|
|
|
|
|
const x = (e.clientX - box.left) / (end - box.left);
|
|
|
|
|
|
if (x < 0.3) prev();
|
|
|
|
|
|
else if (x > 0.7) next();
|
|
|
|
|
|
else return; // middle: the reader toggles its bars
|
|
|
|
|
|
}
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onTouchStart(e) {
|
|
|
|
|
|
if (!paging() || e.touches.length !== 1) { touch = null; return; }
|
|
|
|
|
|
touch = {x: e.touches[0].clientX, y: e.touches[0].clientY, t: Date.now()};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onTouchEnd(e) {
|
|
|
|
|
|
if (!touch || !paging()) return;
|
|
|
|
|
|
const t = e.changedTouches[0];
|
|
|
|
|
|
const dx = t.clientX - touch.x, dy = t.clientY - touch.y;
|
|
|
|
|
|
const quick = Date.now() - touch.t < 600;
|
|
|
|
|
|
touch = null;
|
|
|
|
|
|
const sel = window.getSelection();
|
|
|
|
|
|
if (!quick || (sel && !sel.isCollapsed)) return;
|
|
|
|
|
|
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5) { dx < 0 ? next() : prev(); }
|
|
|
|
|
|
else if (Math.abs(dy) > 60 && Math.abs(dy) > Math.abs(dx) * 1.5) { dy < 0 ? next() : prev(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Keys that scroll in the scrolling reader turn pages here (j/k/f/b/d/u are
|
|
|
|
|
|
// its vim keys, 26-sidebar.js).
|
|
|
|
|
|
const KEYS = {ArrowRight: 1, ArrowDown: 1, PageDown: 1, ' ': 1, j: 1, f: 1, d: 1,
|
|
|
|
|
|
ArrowLeft: -1, ArrowUp: -1, PageUp: -1, k: -1, b: -1, u: -1};
|
|
|
|
|
|
function onKey(e) {
|
|
|
|
|
|
if (!paging() || e.defaultPrevented || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
|
|
|
|
let dir = KEYS[e.key];
|
|
|
|
|
|
if (!dir) return;
|
|
|
|
|
|
const t = e.target;
|
|
|
|
|
|
if (t && t.closest && t.closest('input, textarea, select, [contenteditable="true"], .note-bottom-sheet')) return;
|
|
|
|
|
|
// Space on a button presses it.
|
|
|
|
|
|
if (e.key === ' ' && t && t.closest && t.closest('button, a')) return;
|
|
|
|
|
|
// The search bar's arrows go from match to match.
|
|
|
|
|
|
if (typeof readerSearchOpen !== 'undefined' && readerSearchOpen && /^Arrow(Up|Down)$/.test(e.key)) return;
|
|
|
|
|
|
if (document.getElementById('nui-note')) return; // the apps' note sheet is open
|
|
|
|
|
|
if (e.key === ' ' && e.shiftKey) dir = -1;
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
dir > 0 ? next() : prev();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// One turn per wheel gesture: a trackpad's flick (with its long tail) or a
|
|
|
|
|
|
// wheel notch; a wheel spun on turns on every fourth of a second.
|
|
|
|
|
|
let wheelSum = 0, wheelTurnAt = 0, wheelQuiet = 0, wheelLocked = false;
|
|
|
|
|
|
function onWheel(e) {
|
|
|
|
|
|
if (!paging() || e.ctrlKey) return;
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const dy = e.deltaMode === 1 ? e.deltaY * 30 : e.deltaY;
|
|
|
|
|
|
clearTimeout(wheelQuiet);
|
|
|
|
|
|
wheelQuiet = setTimeout(() => { wheelLocked = false; wheelSum = 0; }, 180);
|
|
|
|
|
|
if (wheelLocked && !(Math.abs(dy) >= 50 && Date.now() - wheelTurnAt > 250)) return;
|
|
|
|
|
|
wheelSum += dy;
|
|
|
|
|
|
if (Math.abs(wheelSum) < 40) return;
|
|
|
|
|
|
wheelLocked = true;
|
|
|
|
|
|
wheelTurnAt = Date.now();
|
|
|
|
|
|
const dir = wheelSum > 0 ? 1 : -1;
|
|
|
|
|
|
wheelSum = 0;
|
|
|
|
|
|
dir > 0 ? next() : prev();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- footer: chapter, page and share of the book ------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function footerText(c) {
|
|
|
|
|
|
if (!paging() || !c) return null;
|
|
|
|
|
|
const pageH = Math.max(1, c.clientHeight - TOP_GAP);
|
|
|
|
|
|
// Pages vary in height (notes at the foot), so page numbers within the
|
|
|
|
|
|
// chapter are an estimate — rounded, so a short page doesn't repeat one.
|
|
|
|
|
|
const cur = currentChapter(c);
|
|
|
|
|
|
const textEnd = cur ? cur.offsetTop + cur.offsetHeight : c.scrollHeight;
|
|
|
|
|
|
const current = Math.round(c.scrollTop / pageH) + 1;
|
|
|
|
|
|
// A spread with text on the right page too: both its numbers.
|
|
|
|
|
|
const two = spreadShown && pane && pane.dataset.top;
|
|
|
|
|
|
const total = Math.max(current + (two ? 1 : 0), Math.ceil(Math.max(0, textEnd - TOP_GAP) / pageH));
|
|
|
|
|
|
const chapters = chapterList(c);
|
|
|
|
|
|
const idx = Array.prototype.indexOf.call(chapters, cur);
|
|
|
|
|
|
const f = globalFraction(c);
|
|
|
|
|
|
return {
|
Nachrichten und Blogs im Buch-Reader, Doppelklick für Vollbild, Hörbücher wie die Bücher-Bibliothek
65-article.js (DioraArticle): Artikel öffnen im selben Reader wie Bücher – Schrift, Farben,
Seiten/Doppelseite mit Fußnoten am Seitenfuß, Suche, Vorlesen, Vollbild. Lesezeichen,
Markierungen und Notizspalte bleiben Büchern vorbehalten. Merken, Projekt und Original
stehen in der Kopfzeile (Apps: im Lesemenü). feeds.js gibt sein eigenes Overlay auf;
Fortschritt wie bisher (bis zum unteren Bildschirmrand gelesen, ab 90 % gelesen).
Einstellungen der Artikel gelten für alle Artikel zusammen.
62-reader.js: Doppelklick (Maus) auf den Text schaltet Vollbild an/aus, die Leisten gehen mit.
audiobooks.js: die Hörbücher sind gebaut wie die Bücher – Kopf mit Suche, Sortierung und +,
Chips Angefangen/Ungehört/Gehört und Ordner, „Weiterhören“ wie „Weiterlesen“, Kacheln aus
denselben Bausteinen (53-library.js gibt seine Styles auch an #audiobook-shelf), Cover quadratisch.
63-pages.js: bei nur einem Kapitel zeigt die Fußzeile nur die Seiten.
sw CACHE v158
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:15:00 +01:00
|
|
|
|
// One chapter (an article, a single-file book): just its pages.
|
|
|
|
|
|
left: (chapters.length > 1 ? 'Kap. ' + (idx + 1) + '/' + chapters.length + ' · ' : '') +
|
|
|
|
|
|
'S. ' + current + (two ? '–' + (current + 1) : '') + '/' + total,
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
|
right: f === null ? '' : _formatBookPercent(f) + ' %',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The browser has no footer of its own (the apps' native-ui.js has one and
|
|
|
|
|
|
// asks footerText): a line under the page while reading in pages.
|
|
|
|
|
|
let footer = null;
|
|
|
|
|
|
function renderFooter() {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
const own = !document.getElementById('nui-reader-footer');
|
|
|
|
|
|
const text = own && c ? footerText(c) : null;
|
|
|
|
|
|
if (!text) { if (footer) footer.style.display = 'none'; return; }
|
|
|
|
|
|
if (!footer) {
|
|
|
|
|
|
const overlay = document.getElementById('reader-overlay');
|
|
|
|
|
|
if (!overlay) return;
|
|
|
|
|
|
footer = document.createElement('div');
|
|
|
|
|
|
footer.id = 'reader-page-footer';
|
|
|
|
|
|
footer.innerHTML = '<span class="rpf-left"></span><span class="rpf-right"></span>';
|
|
|
|
|
|
overlay.appendChild(footer);
|
|
|
|
|
|
}
|
|
|
|
|
|
footer.style.display = '';
|
|
|
|
|
|
const l = footer.querySelector('.rpf-left');
|
|
|
|
|
|
const r = footer.querySelector('.rpf-right');
|
|
|
|
|
|
// Rewriting unchanged text still repaints (a flicker on e-ink).
|
|
|
|
|
|
if (l.textContent !== text.left) l.textContent = text.left;
|
|
|
|
|
|
if (r.textContent !== text.right) r.textContent = text.right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- switching -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function apply() {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
const bookOpen = !!c && readerOpen() && !isPdf();
|
|
|
|
|
|
// The place, taken while the old layout is still there.
|
|
|
|
|
|
const anchor = bookOpen ? getPositionAnchor(c) : '';
|
|
|
|
|
|
if (c) c.classList.toggle('nui-window', on && !isPdf());
|
|
|
|
|
|
history = [];
|
|
|
|
|
|
fractionCache = null;
|
|
|
|
|
|
if (bookOpen) {
|
|
|
|
|
|
_suppressScrollJumpDetect();
|
|
|
|
|
|
if (anchor) restoreFromAnchor(c, anchor);
|
|
|
|
|
|
ensureChapter(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!on) { hideMasks(); showSpread(false); }
|
|
|
|
|
|
if (bookOpen && on) relayout(50);
|
|
|
|
|
|
renderFooter();
|
|
|
|
|
|
if (window.DioraUI && DioraUI.updateFooter) DioraUI.updateFooter();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function update() {
|
|
|
|
|
|
const was = on;
|
|
|
|
|
|
on = wanted || forced;
|
|
|
|
|
|
if (was !== on) apply();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function setOn(value) {
|
|
|
|
|
|
wanted = !!value;
|
|
|
|
|
|
set(KEY_ON, wanted ? '1' : '0');
|
|
|
|
|
|
update();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function force(value) {
|
|
|
|
|
|
forced = !!value;
|
|
|
|
|
|
update();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Nachrichten und Blogs im Buch-Reader, Doppelklick für Vollbild, Hörbücher wie die Bücher-Bibliothek
65-article.js (DioraArticle): Artikel öffnen im selben Reader wie Bücher – Schrift, Farben,
Seiten/Doppelseite mit Fußnoten am Seitenfuß, Suche, Vorlesen, Vollbild. Lesezeichen,
Markierungen und Notizspalte bleiben Büchern vorbehalten. Merken, Projekt und Original
stehen in der Kopfzeile (Apps: im Lesemenü). feeds.js gibt sein eigenes Overlay auf;
Fortschritt wie bisher (bis zum unteren Bildschirmrand gelesen, ab 90 % gelesen).
Einstellungen der Artikel gelten für alle Artikel zusammen.
62-reader.js: Doppelklick (Maus) auf den Text schaltet Vollbild an/aus, die Leisten gehen mit.
audiobooks.js: die Hörbücher sind gebaut wie die Bücher – Kopf mit Suche, Sortierung und +,
Chips Angefangen/Ungehört/Gehört und Ordner, „Weiterhören“ wie „Weiterlesen“, Kacheln aus
denselben Bausteinen (53-library.js gibt seine Styles auch an #audiobook-shelf), Cover quadratisch.
63-pages.js: bei nur einem Kapitel zeigt die Fußzeile nur die Seiten.
sw CACHE v158
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:15:00 +01:00
|
|
|
|
// A book or an article (65-article.js) is in the reader now.
|
|
|
|
|
|
function opened() {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!c) return;
|
|
|
|
|
|
c.classList.toggle('nui-window', on && !isPdf());
|
|
|
|
|
|
history = [];
|
|
|
|
|
|
fractionCache = null;
|
|
|
|
|
|
paneDirty = true;
|
|
|
|
|
|
ensureChapter(c); // a new book has no saved position to reveal
|
|
|
|
|
|
if (paging()) relayout(250);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
|
function setSpread(value) {
|
|
|
|
|
|
spreadWanted = !!value;
|
|
|
|
|
|
set(KEY_SPREAD, spreadWanted ? '1' : '0');
|
|
|
|
|
|
updateMasks();
|
|
|
|
|
|
relayout(50);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function togglePageNotes() {
|
|
|
|
|
|
pageNotes = !pageNotes;
|
|
|
|
|
|
set(KEY_NOTES, pageNotes ? '1' : '0');
|
|
|
|
|
|
updateMasks();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- hooks into the reader ----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// Whatever scrolls to an element (search, read-aloud, notes) brings its
|
|
|
|
|
|
// chapter in first, and jumps instead of gliding.
|
|
|
|
|
|
function patchScrolling() {
|
|
|
|
|
|
['scrollBy', 'scrollTo', 'scrollIntoView'].forEach(name => {
|
|
|
|
|
|
const orig = Element.prototype[name];
|
|
|
|
|
|
if (!orig) return;
|
|
|
|
|
|
Element.prototype[name] = function () {
|
|
|
|
|
|
const args = Array.prototype.slice.call(arguments);
|
|
|
|
|
|
if (on) {
|
|
|
|
|
|
if (name === 'scrollIntoView') reveal(this);
|
|
|
|
|
|
if (paging() && args.length === 1 && args[0] && typeof args[0] === 'object') {
|
|
|
|
|
|
args[0] = Object.assign({}, args[0], {behavior: 'auto'});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return orig.apply(this, args);
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function wrap(name, make) {
|
|
|
|
|
|
const orig = window[name];
|
|
|
|
|
|
if (typeof orig === 'function') window[name] = make(orig);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function wrapReader() {
|
|
|
|
|
|
wrap('openBook', orig => async function () {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (c) c.classList.toggle('nui-window', on);
|
|
|
|
|
|
fractionCache = null;
|
|
|
|
|
|
paneDirty = true;
|
|
|
|
|
|
const r = await orig.apply(this, arguments);
|
Nachrichten und Blogs im Buch-Reader, Doppelklick für Vollbild, Hörbücher wie die Bücher-Bibliothek
65-article.js (DioraArticle): Artikel öffnen im selben Reader wie Bücher – Schrift, Farben,
Seiten/Doppelseite mit Fußnoten am Seitenfuß, Suche, Vorlesen, Vollbild. Lesezeichen,
Markierungen und Notizspalte bleiben Büchern vorbehalten. Merken, Projekt und Original
stehen in der Kopfzeile (Apps: im Lesemenü). feeds.js gibt sein eigenes Overlay auf;
Fortschritt wie bisher (bis zum unteren Bildschirmrand gelesen, ab 90 % gelesen).
Einstellungen der Artikel gelten für alle Artikel zusammen.
62-reader.js: Doppelklick (Maus) auf den Text schaltet Vollbild an/aus, die Leisten gehen mit.
audiobooks.js: die Hörbücher sind gebaut wie die Bücher – Kopf mit Suche, Sortierung und +,
Chips Angefangen/Ungehört/Gehört und Ordner, „Weiterhören“ wie „Weiterlesen“, Kacheln aus
denselben Bausteinen (53-library.js gibt seine Styles auch an #audiobook-shelf), Cover quadratisch.
63-pages.js: bei nur einem Kapitel zeigt die Fußzeile nur die Seiten.
sw CACHE v158
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:15:00 +01:00
|
|
|
|
opened();
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
wrap('restoreFromAnchor', orig => function (contentEl, anchor) {
|
|
|
|
|
|
let first = false;
|
|
|
|
|
|
if (on && anchor && contentEl) {
|
|
|
|
|
|
const parts = _anchorParts(anchor);
|
|
|
|
|
|
const list = contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR);
|
|
|
|
|
|
const block = parts[0] >= 0 && parts[0] < list.length ? list[parts[0]] : null;
|
|
|
|
|
|
if (block) {
|
|
|
|
|
|
reveal(block);
|
|
|
|
|
|
// The chapter's very beginning: its first page, with the margin above
|
|
|
|
|
|
// the heading — not the heading pushed up to the edge.
|
|
|
|
|
|
const ch = chapterOf(block);
|
|
|
|
|
|
first = !!ch && parts[1] < 0.01 && ch.querySelector(EPUB_BLOCK_SELECTOR) === block;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const r = orig.apply(this, arguments);
|
|
|
|
|
|
if (first && r !== false && contentEl === content()) contentEl.scrollTop = 0;
|
|
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
wrap('jumpToTocEntry', orig => function (href) {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (on && c && typeof href === 'string' && href.indexOf('#pdf-page-') !== 0) {
|
|
|
|
|
|
const i = href.indexOf('#');
|
|
|
|
|
|
const frag = i >= 0 ? href.slice(i + 1) : '';
|
|
|
|
|
|
const file = i >= 0 ? href.slice(0, i) : href;
|
|
|
|
|
|
let t = frag ? c.querySelector('#' + CSS.escape(frag)) : null;
|
|
|
|
|
|
if (!t && file) t = Array.prototype.find.call(chapterList(c), el => el.getAttribute('data-epub-src') === file);
|
|
|
|
|
|
if (t) reveal(t);
|
|
|
|
|
|
}
|
|
|
|
|
|
return orig.apply(this, arguments);
|
|
|
|
|
|
});
|
|
|
|
|
|
wrap('jumpToBookmark', orig => function (id) {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
const bm = on && typeof currentBookmarks !== 'undefined' && currentBookmarks.find(b => b.id === id);
|
|
|
|
|
|
if (c && bm && bm.anchor && bm.anchor.indexOf('pdf-page-') !== 0) {
|
|
|
|
|
|
const ch = Array.prototype.find.call(chapterList(c), el => el.getAttribute('data-epub-src') === bm.anchor);
|
|
|
|
|
|
if (ch) reveal(ch);
|
|
|
|
|
|
}
|
|
|
|
|
|
return orig.apply(this, arguments);
|
|
|
|
|
|
});
|
|
|
|
|
|
wrap('jumpToAnnotation', orig => function (id) {
|
|
|
|
|
|
const h = on && typeof currentHighlights !== 'undefined' && currentHighlights.find(x => x.id === id);
|
|
|
|
|
|
if (h && typeof resolveAnchorRange === 'function') {
|
|
|
|
|
|
try { const r = resolveAnchorRange(h); if (r) reveal(r.startContainer); } catch (e) {}
|
|
|
|
|
|
}
|
|
|
|
|
|
return orig.apply(this, arguments);
|
|
|
|
|
|
});
|
|
|
|
|
|
wrap('_ttsHighlightSentence', orig => function (block) {
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (on && c) {
|
|
|
|
|
|
const ch = chapterOf(block);
|
|
|
|
|
|
if (ch && ch !== currentChapter(c)) showChapter(ch, 'top');
|
|
|
|
|
|
}
|
|
|
|
|
|
return orig.apply(this, arguments);
|
|
|
|
|
|
});
|
|
|
|
|
|
// Read-aloud follows the text page by page instead of scrolling.
|
|
|
|
|
|
wrap('_ttsScrollIntoView', orig => function (el) {
|
|
|
|
|
|
if (!paging()) return orig.apply(this, arguments);
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
let guard = 50;
|
|
|
|
|
|
let moved = false;
|
|
|
|
|
|
// Page by page (also in a spread: the voice's page goes left, the next one beside it).
|
|
|
|
|
|
while (guard-- > 0 && el.getBoundingClientRect().bottom > layoutPage(c).limit && !atEnd(c)) moved = stepForward(c) === 'page' || moved;
|
|
|
|
|
|
if (moved) afterTurn();
|
|
|
|
|
|
if (el.getBoundingClientRect().top < c.getBoundingClientRect().top) {
|
|
|
|
|
|
ownScroll(c, c.scrollTop - (c.getBoundingClientRect().top + TOP_GAP - el.getBoundingClientRect().top));
|
|
|
|
|
|
history = [];
|
|
|
|
|
|
afterTurn();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
wrap('applyReaderSettings', orig => function () {
|
|
|
|
|
|
const r = orig.apply(this, arguments);
|
|
|
|
|
|
if (paging()) relayout(60);
|
|
|
|
|
|
return r;
|
|
|
|
|
|
});
|
|
|
|
|
|
wrap('closeReader', orig => function () {
|
|
|
|
|
|
hideMasks();
|
|
|
|
|
|
if (footer) footer.style.display = 'none';
|
|
|
|
|
|
return orig.apply(this, arguments);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.DioraPages = {
|
|
|
|
|
|
isOn: () => on,
|
|
|
|
|
|
isForced: () => forced,
|
|
|
|
|
|
paging,
|
|
|
|
|
|
setOn,
|
|
|
|
|
|
force,
|
|
|
|
|
|
pageNotes: () => pageNotes,
|
|
|
|
|
|
togglePageNotes,
|
|
|
|
|
|
spread: () => spreadWanted,
|
|
|
|
|
|
spreadShown: () => spreadShown,
|
|
|
|
|
|
setSpread,
|
|
|
|
|
|
next,
|
|
|
|
|
|
prev,
|
|
|
|
|
|
reveal,
|
|
|
|
|
|
currentChapter: () => (content() ? currentChapter(content()) : null),
|
|
|
|
|
|
globalFraction,
|
|
|
|
|
|
jumpToFraction,
|
|
|
|
|
|
footerText,
|
|
|
|
|
|
relayout,
|
Nachrichten und Blogs im Buch-Reader, Doppelklick für Vollbild, Hörbücher wie die Bücher-Bibliothek
65-article.js (DioraArticle): Artikel öffnen im selben Reader wie Bücher – Schrift, Farben,
Seiten/Doppelseite mit Fußnoten am Seitenfuß, Suche, Vorlesen, Vollbild. Lesezeichen,
Markierungen und Notizspalte bleiben Büchern vorbehalten. Merken, Projekt und Original
stehen in der Kopfzeile (Apps: im Lesemenü). feeds.js gibt sein eigenes Overlay auf;
Fortschritt wie bisher (bis zum unteren Bildschirmrand gelesen, ab 90 % gelesen).
Einstellungen der Artikel gelten für alle Artikel zusammen.
62-reader.js: Doppelklick (Maus) auf den Text schaltet Vollbild an/aus, die Leisten gehen mit.
audiobooks.js: die Hörbücher sind gebaut wie die Bücher – Kopf mit Suche, Sortierung und +,
Chips Angefangen/Ungehört/Gehört und Ordner, „Weiterhören“ wie „Weiterlesen“, Kacheln aus
denselben Bausteinen (53-library.js gibt seine Styles auch an #audiobook-shelf), Cover quadratisch.
63-pages.js: bei nur einem Kapitel zeigt die Fußzeile nur die Seiten.
sw CACHE v158
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:15:00 +01:00
|
|
|
|
opened,
|
Lesen in Seiten auch im Browser und auf dem Desktop, mit Fußnoten unten auf der Seite und Doppelseite
63-pages.js (DioraPages): die Seitenansicht des E-Reader-Modus der Android-Apps, jetzt für alle –
Seitenumbrüche vor angeschnittenen Zeilen, ein Kapitel auf einmal, Fußnoten am Seitenfuß wie im
gedruckten Buch, Blättern per Tippen/Klick am Rand, Wischen, Pfeil-/Bild-/Leertaste und Mausrad.
Neu: Doppelseite (zwei Seiten nebeneinander ab 900 px Breite). Einstellung „Scrollen | Seiten“
(+ „Fußnoten unten“, „Doppelseite“) im Schrift-&-Layout-Panel, je Gerät.
Fußnoten: Erkennung und Auflösung aus diora-android native-ui.js übernommen (40-epub.js,
78-footnotes.js) – die Notiz wird im richtigen Kapitel gesucht (IDs wiederholen sich je Datei),
auch das Popover zeigt jetzt genau die Notiz. Vorbereitete Bücher werden einmal neu aufbereitet.
Behoben gegenüber der App-Fassung: ein Verweis in der ersten Zeile einer Seite (hochgestellt)
zählte zur Vorseite, und ein Verweis, der samt Notiz nicht mehr passte, blieb ohne Notiz stehen –
jetzt wandert er mit ihr auf die nächste Seite. Schnelles Blättern löst „Weit gescrollt“ nicht mehr aus.
sw CACHE v157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 19:41:27 +01:00
|
|
|
|
onTurn: fn => turnHooks.push(fn),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
patchScrolling();
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
wrapReader();
|
|
|
|
|
|
const c = content();
|
|
|
|
|
|
if (!c) return;
|
|
|
|
|
|
c.classList.toggle('nui-window', on);
|
|
|
|
|
|
c.addEventListener('pointerdown', e => { pointer = e.pointerType || 'mouse'; }, true);
|
|
|
|
|
|
c.addEventListener('click', onClick, true);
|
|
|
|
|
|
c.addEventListener('scroll', onScroll, {passive: true});
|
|
|
|
|
|
c.addEventListener('touchstart', onTouchStart, {passive: true});
|
|
|
|
|
|
c.addEventListener('touchend', onTouchEnd, {passive: true});
|
|
|
|
|
|
c.addEventListener('wheel', onWheel, {passive: false});
|
|
|
|
|
|
window.addEventListener('keydown', onKey, true);
|
|
|
|
|
|
// The right page of a spread is a copy: made again after the book changed
|
|
|
|
|
|
// (a marking, search hits, the read-aloud mark).
|
|
|
|
|
|
new MutationObserver(() => { paneDirty = true; })
|
|
|
|
|
|
.observe(c, {childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class', 'data-color']});
|
|
|
|
|
|
// Bars shown or hidden, panels opened, the window resized: the page's
|
|
|
|
|
|
// height changed. Look again once the layout (and its transition) settled.
|
|
|
|
|
|
const settle = () => {
|
|
|
|
|
|
if (!paging()) return;
|
|
|
|
|
|
requestAnimationFrame(updateMasks);
|
|
|
|
|
|
setTimeout(updateMasks, 80);
|
|
|
|
|
|
relayout(400);
|
|
|
|
|
|
};
|
|
|
|
|
|
new MutationObserver(settle).observe(document.body, {attributes: true, attributeFilter: ['class']});
|
|
|
|
|
|
if (window.ResizeObserver) new ResizeObserver(settle).observe(c); // not in old WebViews
|
|
|
|
|
|
window.addEventListener('resize', () => relayout(150));
|
|
|
|
|
|
});
|
|
|
|
|
|
})();
|