From e8b525a3de9457c8e33a647448fbf131a3dad473 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Mon, 26 Oct 2026 20:15:00 +0100 Subject: [PATCH] =?UTF-8?q?Nachrichten=20und=20Blogs=20im=20Buch-Reader,?= =?UTF-8?q?=20Doppelklick=20f=C3=BCr=20Vollbild,=20H=C3=B6rb=C3=BCcher=20w?= =?UTF-8?q?ie=20die=20B=C3=BCcher-Bibliothek?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- static/css/app.css | 11 ++ static/js/app/53-library.js | 13 +- static/js/app/62-reader.js | 45 +++++ static/js/app/63-pages.js | 22 ++- static/js/app/64-reader-settings.js | 9 +- static/js/app/65-article.js | 173 ++++++++++++++++++++ static/js/app/72-highlights.js | 3 +- static/js/audiobooks.js | 226 +++++++++++++++++--------- static/js/feeds.js | 244 ++++++++++++---------------- static/js/sw.js | 3 +- templates/radio/player.html | 1 + 11 files changed, 521 insertions(+), 229 deletions(-) create mode 100644 static/js/app/65-article.js diff --git a/static/css/app.css b/static/css/app.css index 4bd3dd9..6cb0976 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1901,6 +1901,17 @@ html.nui-tts-on body.reader-immersive .reader-overlay { bottom: var(--bar-h); } #reader-overlay.reader-spread #reader-spread-page { display:block; overflow:hidden; box-shadow:inset 1px 0 0 var(--border); user-select:none; -webkit-user-select:none; cursor:default; } #reader-spread-page::after { content:""; display:block; height:100vh; } +/* An article in the reader (65-article.js): what only books have stays out. */ +#reader-overlay.reader-article-mode .reader-progress-wrap, +#reader-overlay.reader-article-mode #reader-bookmark-btn, +#reader-overlay.reader-article-mode #reader-bm-list-btn, +#reader-overlay.reader-article-mode #reader-toc-btn, +#reader-overlay.reader-article-mode #reader-margin-btn, +#reader-overlay.reader-article-mode #reader-reset-pos-btn, +#reader-overlay.reader-article-mode .marker-mode-btn, +#reader-overlay.reader-article-mode #reader-margin, +#reader-overlay.reader-article-mode .nui-toc { display:none !important; } +.reader-article-actions { display:inline-flex; gap:10px; align-items:center; } /* The browser's line under the page (the apps have their own footer). */ #reader-page-footer { display:flex; justify-content:space-between; gap:12px; flex-shrink:0; padding:4px 16px 6px; font-size:12px; color:var(--muted,#888); font-variant-numeric:tabular-nums; } diff --git a/static/js/app/53-library.js b/static/js/app/53-library.js index 3758245..c2fe639 100644 --- a/static/js/app/53-library.js +++ b/static/js/app/53-library.js @@ -933,7 +933,7 @@ function _libAddMenu() { (function () { const style = document.createElement('style'); - style.textContent = [ + const rules = [ 'html.lib-v2 #tab-books .book-drop-zone, html.lib-v2 #tab-books .book-list-filter,', 'html.lib-v2 #book-upload-area > button.btn, html.lib-v2 #book-upload-area > .nui-add-row { display: none !important; }', '#book-list .lib-head { display: flex; align-items: center; gap: 6px; margin: 4px 0 8px; }', @@ -1026,6 +1026,15 @@ function _libAddMenu() { '.cloud-chosen:empty { display: none; }', '.cloud-entry .cloud-check { width: 18px; height: 18px; margin: 0 8px 0 2px; flex: none; }', 'body.eink #book-list .lib-cover { box-shadow: none; border: 1px solid #000; }', - ].join('\n'); + ]; + // The audiobooks' shelf (audiobooks.js) is built of the same parts: the same rules for it, + // its covers square like an audiobook's. + const shelf = rules.filter(r => r.indexOf('#book-list') >= 0).map(r => r.replace(/#book-list/g, '#audiobook-shelf')); + style.textContent = rules.concat(shelf, [ + '#audiobook-shelf .lib-cover { aspect-ratio: 1; }', + '#audiobook-shelf .lib-hero-cover { height: auto; aspect-ratio: 1; width: 120px; align-self: flex-start; }', + '@media (min-width: 721px) { #audiobook-shelf .lib-hero-cover { width: 160px; height: auto; } }', + '#audiobook-shelf button.lib-play { border: none; font: inherit; font-size: 14px; font-weight: 600; cursor: pointer; }', + ]).join('\n'); document.head.appendChild(style); })(); diff --git a/static/js/app/62-reader.js b/static/js/app/62-reader.js index 453fafe..72d7a0a 100644 --- a/static/js/app/62-reader.js +++ b/static/js/app/62-reader.js @@ -29,6 +29,49 @@ function _immHandleTap(e) { } } +// A double click on the text (with a mouse): full screen, the bars out of the +// way; again (or Esc): back as it was. Fingers keep their double tap; beside +// the text a click turns the page (63-pages.js), so only on the text itself. +let _readerFsHidBars = false; + +function _readerDblClick(e) { + if (!matchMedia('(hover: hover) and (pointer: fine)').matches) return; + if (!e.target.closest('[data-epub-src], .pdf-page-wrapper')) return; + if (e.target.closest('button, a, input, select, textarea, label, [data-footnote-ref], mark.epub-highlight')) return; + // The double click also picked a word: that's not a marking. + const sel = window.getSelection(); + if (sel) sel.removeAllRanges(); + if (typeof dismissHighlightPopover === 'function') dismissHighlightPopover(); + if (document.fullscreenElement) { + if (document.exitFullscreen) document.exitFullscreen().catch(() => {}); + return; + } + const el = document.documentElement; + if (!el.requestFullscreen) return; + el.requestFullscreen().then(() => { + if (_immBarsVisible) { + _immBarsVisible = false; + document.body.classList.add('reader-immersive'); + _readerFsHidBars = true; + } + }).catch(() => {}); +} + +document.addEventListener('fullscreenchange', () => { + if (document.fullscreenElement || !_readerFsHidBars) return; + _readerFsHidBars = false; + const overlay = $('reader-overlay'); + if (overlay && overlay.style.display !== 'none' && !_immBarsVisible) { + _immBarsVisible = true; + document.body.classList.remove('reader-immersive'); + } +}); + +document.addEventListener('DOMContentLoaded', () => { + const row = $('reader-body-row'); + if (row) row.addEventListener('dblclick', _readerDblClick); +}); + function enterReaderImmersiveMode() { _immBarsVisible = true; document.body.classList.remove('reader-immersive'); @@ -549,6 +592,8 @@ function closeReader() { // Still opening: just leave it (nothing to save yet). if (cancelBookOpening()) return; if (typeof _issueReaderClosed === 'function') _issueReaderClosed(); + // An article (65-article.js): where it was read to, before the reader empties. + if (typeof _articleReaderClosed === 'function') _articleReaderClosed(); exitReaderImmersiveMode(); stopReadAloud(); // Save progress BEFORE hiding — scrollHeight/clientHeight return 0 once display:none diff --git a/static/js/app/63-pages.js b/static/js/app/63-pages.js index d5547e0..af5d5be 100644 --- a/static/js/app/63-pages.js +++ b/static/js/app/63-pages.js @@ -763,7 +763,9 @@ const idx = Array.prototype.indexOf.call(chapters, cur); const f = globalFraction(c); return { - left: 'Kap. ' + (idx + 1) + '/' + chapters.length + ' · S. ' + current + (two ? '–' + (current + 1) : '') + '/' + total, + // 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, right: f === null ? '' : _formatBookPercent(f) + ' %', }; } @@ -830,6 +832,18 @@ update(); } + // 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); + } + function setSpread(value) { spreadWanted = !!value; set(KEY_SPREAD, spreadWanted ? '1' : '0'); @@ -876,10 +890,7 @@ fractionCache = null; paneDirty = true; const r = await orig.apply(this, arguments); - history = []; - if (c && isPdf()) c.classList.remove('nui-window'); - ensureChapter(content()); // a new book has no saved position to reveal - if (paging()) relayout(250); + opened(); return r; }); wrap('restoreFromAnchor', orig => function (contentEl, anchor) { @@ -982,6 +993,7 @@ jumpToFraction, footerText, relayout, + opened, onTurn: fn => turnHooks.push(fn), }; diff --git a/static/js/app/64-reader-settings.js b/static/js/app/64-reader-settings.js index 128280c..f50111e 100644 --- a/static/js/app/64-reader-settings.js +++ b/static/js/app/64-reader-settings.js @@ -15,7 +15,11 @@ function _readerTypeDefaults() { ? _READER_TYPE_DEFAULTS_WIDE : _READER_TYPE_DEFAULTS_NARROW; } +// Where the settings shown are kept: the book's id, or 'article' (all articles, 65-article.js). +let _readerSettingsKey = null; + function loadReaderSettings(bookId) { + _readerSettingsKey = bookId; // Reset to defaults, then apply per-book overrides Object.assign(readerSettings, { theme: 'app', fontFamily: 'serif', noBold: false, pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false }, @@ -40,8 +44,9 @@ function loadReaderSettings(bookId) { } function saveReaderSettings() { - if (currentBookId) { - localStorage.setItem(`diora_reader_settings_${currentBookId}`, JSON.stringify({ ...readerSettings, v: 3 })); + const key = currentBookId || _readerSettingsKey; + if (key) { + localStorage.setItem(`diora_reader_settings_${key}`, JSON.stringify({ ...readerSettings, v: 3 })); } } diff --git a/static/js/app/65-article.js b/static/js/app/65-article.js new file mode 100644 index 0000000..1458159 --- /dev/null +++ b/static/js/app/65-article.js @@ -0,0 +1,173 @@ +'use strict'; + +// --------------------------------------------------------------------------- +// Articles in the book reader (DioraArticle) — blog posts and news (feeds.js) +// read like books: the same overlay, typography and colours (⚙, kept for all +// articles together), pages or scrolling (63-pages.js) with the notes at the +// page's foot, search, read-aloud, full screen. Bookmarks, markings and the +// notes margin stay with books (an article has nowhere to keep them). +// +// The article is one "chapter" in #reader-content; currentBookId stays null, +// so nothing of a book's position or annotations is saved. Where it was read +// to goes back to its owner (onProgress), as do clicks on its own buttons. +// --------------------------------------------------------------------------- +(function () { + let session = null; // {title, html, startFraction, onProgress, onClick, onClose, actions} + let progressTimer = 0; + + const overlay = () => document.getElementById('reader-overlay'); + const content = () => document.getElementById('reader-content'); + + // Same-page links: the notes of a blog post ("¹" → "#fn1") become references + // like in books; other jumps inside the post must not change the app's + // address (#books, #feeds …): kept as internal links (the apps follow them). + function markLinks(root) { + root.querySelectorAll('a[href^="#"]').forEach(a => { + const href = a.getAttribute('href'); + a.removeAttribute('href'); + if (href.length < 2) return; + if (!_isNoteBacklink(a) && _looksLikeFootnoteLink(a, href)) { + a.dataset.footnoteRef = href.slice(1); + a.dataset.footnoteHref = href; + a.style.cursor = 'pointer'; + } else { + a.dataset.internalHref = href; + } + }); + // Everything else opens outside. + root.querySelectorAll('a[href]').forEach(a => { + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + }); + } + + // How far it is read: down to the bottom of what is on screen (0-1). + function fraction() { + const c = content(); + const ch = c && c.querySelector(':scope > [data-epub-src]'); + if (!ch || !ch.offsetHeight) return 0; + return Math.max(0, Math.min(1, (c.scrollTop + c.clientHeight - ch.offsetTop) / ch.offsetHeight)); + } + + function report() { + if (session && session.onProgress) session.onProgress(fraction()); + } + + function onScroll() { + clearTimeout(progressTimer); + progressTimer = setTimeout(report, 1200); + } + + function onClick(e) { + if (session && session.onClick) session.onClick(e); + } + + // The article's own buttons (star, project, original): in the browser's + // header; the apps put them into their reader menu (native-ui.js asks actions()). + function renderActions() { + const bar = document.querySelector('#reader-overlay .reader-header-actions'); + if (!bar) return; + let box = bar.querySelector('.reader-article-actions'); + if (!box) { + box = document.createElement('span'); + box.className = 'reader-article-actions'; + bar.insertBefore(box, bar.firstChild); + } + box.textContent = ''; + actions().forEach(a => { + const b = document.createElement('button'); + b.type = 'button'; + b.className = 'btn-icon' + (a.active ? ' active' : ''); + b.title = a.label; + b.textContent = a.icon; + b.addEventListener('click', e => { e.stopPropagation(); a.action(); }); + box.appendChild(b); + }); + } + + function actions() { + return session && session.actions ? session.actions().filter(Boolean) : []; + } + + async function open(o) { + const ov = overlay(); + const c = content(); + if (!ov || !c) return; + if (session) { + report(); + stopReadAloud(); // the next article: the voice was reading the last one + } + else if (ov.style.display !== 'none') closeReader(); // a book was open + session = o; + loadReaderSettings('article'); + _charIndex = null; + _readPos = null; + currentBookId = null; + ov.classList.add('reader-article-mode'); + $('reader-title').textContent = o.title || ''; + c.textContent = ''; + const ch = document.createElement('div'); + ch.setAttribute('data-epub-src', 'article'); + ch.id = 'epub-chapter-0'; + ch.className = 'fd-artview'; + ch.innerHTML = o.html; + markLinks(ch); + c.appendChild(ch); + ov.style.display = ''; + applyReaderSettings(false); + enterReaderImmersiveMode(); + renderActions(); + c.scrollTop = 0; + if (window.DioraPages) DioraPages.opened(); + if (window.DioraUI && DioraUI.updateFooter) DioraUI.updateFooter(); + const f = o.startFraction || 0; + if (f > 0.02 && f < 0.9) { + // Back where reading stopped (on any device). + await new Promise(r => requestAnimationFrame(r)); + if (session !== o) return; + _suppressScrollJumpDetect(); + c.scrollTop = Math.max(0, f * ch.offsetHeight + ch.offsetTop - c.clientHeight); + } else { + setTimeout(() => { if (session === o) report(); }, 1500); // short enough to fit: read at once + } + } + + // Replaces what is shown (the overview of a story arrived) without moving. + function update(html) { + const ch = content() && content().querySelector(':scope > [data-epub-src="article"]'); + if (!ch || !session) return; + session.html = html; + ch.innerHTML = html; + markLinks(ch); + } + + // closeReader (62-reader.js) calls this before it empties the reader. + window._articleReaderClosed = function () { + if (!session) return; + clearTimeout(progressTimer); + report(); + const s = session; + session = null; + overlay().classList.remove('reader-article-mode'); + const box = document.querySelector('#reader-overlay .reader-article-actions'); + if (box) box.remove(); + if (s.onClose) setTimeout(s.onClose, 0); + }; + + window.DioraArticle = { + open, + update, + isOpen: () => !!session, + close: () => { if (session) closeReader(); }, + actions, + refreshActions: renderActions, + fraction, + }; + + document.addEventListener('DOMContentLoaded', () => { + const c = content(); + if (!c) return; + c.addEventListener('scroll', () => { if (session) onScroll(); }, {passive: true}); + c.addEventListener('click', e => { if (session) onClick(e); }); + }); +})(); diff --git a/static/js/app/72-highlights.js b/static/js/app/72-highlights.js index 4e950f7..cd684f7 100644 --- a/static/js/app/72-highlights.js +++ b/static/js/app/72-highlights.js @@ -219,7 +219,8 @@ function handleReaderSelection(e) { return; } - if (!markerModeActive) return; + // Markings belong to books (an article has nowhere to keep them, 65-article.js). + if (!markerModeActive || !currentBookId) 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 // used only as an internal jump target (sanitizeEpubHtml diff --git a/static/js/audiobooks.js b/static/js/audiobooks.js index 63d07ea..2092386 100644 --- a/static/js/audiobooks.js +++ b/static/js/audiobooks.js @@ -31,6 +31,10 @@ folder: null, // the folder chip chosen (null: all, '': without a folder) folderOrder: [], // the podcasts' folder order (/podcasts/folders/) series: null, // the series shown (its key), inside the folder + q: '', // the search in the shelf's head + searching: false, // the search field open (narrow screens show a ⌕ first) + sort: cached('sort') || 'recent', + status: 'all', // the state chip: all, started, unheard, heard }; const shelf = document.createElement('div'); @@ -175,45 +179,6 @@ return st.detail; } - // ---- the shelf in "Bücher" ------------------------------------------------------------ - - function renderShelf() { - const h = host(); - const books = st.books || []; - const state = h ? podcastState() : null; - const pods = state && state.feeds ? state.feeds.filter(f => f.kind !== 'audiobook') : []; - if (!h) { shelf.innerHTML = ''; return; } - if (!books.length && !pods.length) { - shelf.innerHTML = '

Hörbücher und Hörspiele sind hier Podcasts, die wie ein Buch gehört ' + - 'werden: Kapitel in Reihenfolge, Lesezeichen. Erst einen Podcast abonnieren (Podcasts → Suche), dann hier markieren.

'; - return; - } - let html = ''; - const go = books.filter(b => b.next && b.last_played_at)[0]; - if (go) { - html += '
' + - cover(go.artwork_url, go.title, 'ab-cover ab-cover-s') + - '
Weiterhören
' + - '
' + esc(go.title) + '
' + - '
' + esc(progressText(go)) + '
' + - '
' + - '
'; - } - html += '

Hörbücher

' + - (pods.length ? '' : '') + '
'; - if (books.length) { - html += gridHtml(books); - } else { - html += '

Podcasts, die eigentlich Hörbücher oder Hörspiele sind, lassen sich hier ' + - 'wie ein Buch hören: in Kapitelreihenfolge, mit Lesezeichen. „+ Hörbuch“ wählt einen aus.

'; - } - if (st.picking) html += pickHtml(pods); - shelf.innerHTML = html; - drawChips(books); - const input = shelf.querySelector('.ab-pick-q'); - if (input && document.activeElement !== input && st.pickFocus) { input.focus(); st.pickFocus = false; } - } - // ---- organising: folders (set per book) and series (found in the titles) ---------------- // "Das Kapital – Erster Band", "Harry Potter: Teil 3", "Die drei ??? – Folge 12": @@ -270,28 +235,131 @@ x.sort.localeCompare(y.sort, 'de')); } + // ---- the shelf, built like the books' library (53-library.js: its lib-* parts, and its + // styles, which hold for #audiobook-shelf too): a head with search, sort and +, chips for + // the state and the folders, "Weiterhören" on top, the books as cards. --------------------- + + const SORTS = [['recent', 'Zuletzt'], ['title', 'Titel'], ['author', 'Autor'], + ['long', 'Längste zuerst'], ['short', 'Kürzeste zuerst']]; + const heard = b => !b.next; + const started = b => !!b.next && (b.heard_seconds > 0 || b.done > 0 || !!b.last_played_at); + const unheard = b => !!b.next && !started(b); + const STATES = {started, unheard, heard}; + + function renderShelf() { + const h = host(); + const books = st.books || []; + const state = h ? podcastState() : null; + const pods = state && state.feeds ? state.feeds.filter(f => f.kind !== 'audiobook') : []; + if (!h) { shelf.innerHTML = ''; return; } + const narrow = window.matchMedia && matchMedia('(max-width: 720px)').matches; + let html = '

Hörbücher

' + + (!narrow || st.searching || st.q + ? '' + : '') + + '' + + (pods.length ? '' : '') + + '
'; + if (st.picking) html += pickHtml(pods); + if (!books.length) { + html += '

' + (pods.length + ? 'Podcasts, die eigentlich Hörbücher oder Hörspiele sind, lassen sich hier wie ein Buch hören: ' + + 'in Kapitelreihenfolge, mit Lesezeichen. Mit + einen auswählen.' + : 'Hörbücher und Hörspiele sind hier Podcasts, die wie ein Buch gehört werden: Kapitel in Reihenfolge, ' + + 'Lesezeichen. Erst einen Podcast abonnieren (Podcasts → Suche), dann hier mit + markieren.') + '

'; + } else { + html += listHtml(books); + } + const q = shelf.querySelector('.lib-q'); + const focused = q && document.activeElement === q; + shelf.innerHTML = html; + drawChips(books); + if (focused || (st.searching && !st.q)) { + const n = shelf.querySelector('.lib-q'); + if (n) { n.focus(); n.setSelectionRange(n.value.length, n.value.length); } + } + const input = shelf.querySelector('.ab-pick-q'); + if (input && document.activeElement !== input && st.pickFocus) { input.focus(); st.pickFocus = false; } + } + + function shownBooks(books) { + const q = (st.q || '').trim().toLowerCase(); + return books.filter(b => + (st.folder == null ? true : st.folder === '' ? !foldersOf(b).length : foldersOf(b).indexOf(st.folder) >= 0) && + (!STATES[st.status] || STATES[st.status](b)) && + (!q || (b.title + ' ' + (b.author || '')).toLowerCase().indexOf(q) >= 0)); + } + + // The list's order (entries() comes "Zuletzt": the last heard first). + function sorted(list) { + const books = e => (e.kind === 'series' ? e.books : [e.b]); + const length = e => books(e).reduce((n, b) => n + (b.total_seconds || 0), 0); + const author = e => books(e)[0].author || ''; + const by = { + title: (x, y) => x.sort.localeCompare(y.sort, 'de'), + author: (x, y) => author(x).localeCompare(author(y), 'de') || x.sort.localeCompare(y.sort, 'de'), + long: (x, y) => length(y) - length(x), + short: (x, y) => length(x) - length(y), + }[st.sort]; + return by ? list.slice().sort(by) : list; + } + + function listHtml(books) { + const here = shownBooks(books); + // Series as stacks, unless searching or filtering by state (as with the books). + const stack = !st.q && !STATES[st.status]; + const list = sorted(stack ? entries(here) : here.map(b => ({kind: 'book', b, sort: b.title}))); + if (st.series != null) { + const g = list.filter(e => e.kind === 'series' && e.key === st.series)[0]; + if (g) { + return '
' + + '' + esc(g.name) + '
' + + '
' + g.books.map(b => bookCard(b, b._volume)).join('') + '
'; + } + st.series = null; + } + let html = ''; + const go = !st.q && st.folder == null && !STATES[st.status] && books.filter(b => b.next && b.last_played_at)[0]; + if (go) html += continueHtml(go); + if (!list.length) return html + '

Nichts gefunden.

'; + return html + '
' + list.map(e => e.kind === 'series' ? seriesCard(e) : bookCard(e.b)).join('') + '
'; + } + + // "Weiterhören": the book last listened to, large — like "Weiterlesen". + function continueHtml(b) { + return '
' + + cover(b.artwork_url, b.title, 'lib-hero-cover', b.author) + + 'Weiterhören' + + '' + esc(b.title) + '' + + (b.author ? '' + esc(b.author) + '' : '') + + '' + + '' + esc(progressText(b)) + '' + + '
'; + } + function bookCard(b, label) { - return ''; + return '
' + + '' + cover(b.artwork_url, b.title, 'lib-cover', b.author) + + (heard(b) ? '✓' : '') + '' + + '' + + '' + esc(label || b.title) + '' + + '' + esc(label ? progressText(b) : (b.author || progressText(b))) + '
'; } function seriesCard(g) { const total = g.books.reduce((n, b) => n + (b.total_seconds || 0), 0); - const heard = g.books.reduce((n, b) => n + (b.heard_seconds || 0), 0); + const heardSecs = g.books.reduce((n, b) => n + (b.heard_seconds || 0), 0); const going = g.books.filter(b => b.next && b.last_played_at)[0]; - const done = g.books.filter(b => !b.next).length; + const done = g.books.filter(heard).length; const meta = g.books.length + ' Bände' + (going ? ' · bei ' + going._volume : done === g.books.length ? ' · fertig gehört' : done ? ' · ' + done + ' gehört' : ''); - return ''; + return '
' + + '' + cover(g.books[0].artwork_url, g.name, 'lib-cover', g.books[0].author) + '' + + '' + + '' + esc(g.name) + '' + + '' + esc(meta) + '
'; } const foldersOf = b => (b.folders && b.folders.length ? b.folders : (b.folder ? [b.folder] : [])); @@ -311,32 +379,24 @@ return {folders: names.map(n => ({name: n, count: counts.get(n)})), none: names.length ? none : 0}; } - function gridHtml(books) { - const here = books.filter(b => st.folder == null ? true - : st.folder === '' ? !foldersOf(b).length : foldersOf(b).indexOf(st.folder) >= 0); - const list = entries(here); - if (st.series != null) { - const g = list.filter(e => e.kind === 'series' && e.key === st.series)[0]; - if (g) { - return '
' + - '' + esc(g.name) + '
' + - '
' + g.books.map(b => bookCard(b, b._volume)).join('') + '
'; - } - st.series = null; - } - return '
' + - list.map(e => e.kind === 'series' ? seriesCard(e) : bookCard(e.b)).join('') + '
'; - } - function drawChips(books) { - const el = shelf.querySelector('.ab-chips'); + const el = shelf.querySelector('.lib-chips'); if (!el || !window.DioraFolders) return; + if (!books.length) { el.innerHTML = ''; return; } const c = folderChips(books); - if (!c.folders.length) { el.innerHTML = ''; return; } window.DioraFolders.chips(el, { + lead: [ + {id: 'started', label: 'Angefangen', count: books.filter(started).length, on: st.status === 'started'}, + {id: 'unheard', label: 'Ungehört', count: books.filter(unheard).length, on: st.status === 'unheard'}, + {id: 'heard', label: 'Gehört', count: books.filter(heard).length, on: st.status === 'heard'}, + ], folders: c.folders, active: st.folder, none: c.none, - onPick(name) { st.folder = name; renderShelf(); }, - onManage: manageFolders, + onPick(p) { + if (p && p.lead) st.status = st.status === p.lead ? 'all' : p.lead; + else st.folder = p; + renderShelf(); + }, + onManage: c.folders.length ? manageFolders : null, }); } @@ -442,7 +502,7 @@ const d = st.detail && st.detail.book.feed_id === st.open ? st.detail : null; const fromList = (st.books || []).filter(b => b.feed_id === st.open)[0]; const b = d ? d.book : fromList; - if (!b) { view.innerHTML = '

Wird geladen …

'; return; } + if (!b) { view.innerHTML = '

Wird geladen …

'; return; } const now = nowInBook(); const marks = d ? d.bookmarks : []; const dlMap = d ? downloads() : null; @@ -452,7 +512,7 @@ return ch ? 'Kapitel ' + (ch.index + 1) : ''; }; view.innerHTML = - '
' + + '
' + '
' + '