From 7d99e58286b9098debe04f0cd28874d90a3d7564 Mon Sep 17 00:00:00 2001 From: marwin Date: Mon, 17 Aug 2026 10:58:53 +0200 Subject: [PATCH 001/176] =?UTF-8?q?B=C3=BCcher:=20Ordner=20(eine=20Ebene)?= =?UTF-8?q?=20+=20"Zuletzt=20gelesen"-Leiste=20(SW=20v36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ordnername lebt im bereits verschlüsselten meta-Blob (wie Titel/Autor), daher keine Schema-Änderung nötig — nur ein schlanker Endpoint zum Aktualisieren von meta_ct/meta_iv ohne die Buchdaten neu hochzuladen. Die Bücheransicht zeigt jetzt immer oben die letzten 7 gelesenen Titel als Shortcut-Leiste, darunter Ordner-Kacheln (root) bzw. den Inhalt eines geöffneten Ordners (kein Verschachteln). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011j4LcoeddwFt6Rw8Wyaknj --- books/urls.py | 1 + books/views.py | 29 +++++++++ static/css/app.css | 83 ++++++++++++++++++++++++ static/js/app.js | 158 ++++++++++++++++++++++++++++++++++++++------- static/js/sw.js | 2 +- 5 files changed, 250 insertions(+), 23 deletions(-) diff --git a/books/urls.py b/books/urls.py index ddcb20a..0d3b782 100644 --- a/books/urls.py +++ b/books/urls.py @@ -7,6 +7,7 @@ urlpatterns = [ path('/data/', views.get_book_data, name='get_book_data'), path('/delete/', views.delete_book, name='delete_book'), path('/read/', views.set_book_read, name='set_book_read'), + path('/meta/', views.update_book_meta, name='update_book_meta'), path('/replace-data/', views.replace_book_data, name='replace_book_data'), path('/rekey/', views.rekey_book, name='rekey_book'), path('/progress/', views.save_progress, name='save_book_progress'), diff --git a/books/views.py b/books/views.py index c9e21f2..15d1445 100644 --- a/books/views.py +++ b/books/views.py @@ -89,6 +89,35 @@ def set_book_read(request, pk): return JsonResponse({'ok': True, 'is_read': book.is_read}) +@csrf_exempt +@require_http_methods(['POST']) +def update_book_meta(request, pk): + """Update only the encrypted metadata blob (e.g. to assign a folder) without touching book bytes.""" + err = _require_auth(request) + if err: + return err + + try: + book = EBook.objects.get(pk=pk, user=request.user) + except EBook.DoesNotExist: + return JsonResponse({'error': 'not found'}, status=404) + + try: + body = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({'error': 'invalid JSON'}, status=400) + + meta_ct = body.get('meta_ct', '') + meta_iv = body.get('meta_iv', '') + if not meta_ct or not meta_iv: + return JsonResponse({'error': 'meta_ct, meta_iv required'}, status=400) + + book.meta_ct = meta_ct + book.meta_iv = meta_iv + book.save(update_fields=['meta_ct', 'meta_iv']) + return JsonResponse({'ok': True}) + + @csrf_exempt @require_http_methods(['POST']) def upload_book(request): diff --git a/static/css/app.css b/static/css/app.css index 3f953dc..3ed7abd 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1602,6 +1602,89 @@ body.dnd-mode .timer-display { cursor: pointer; } +/* --- Recently-read shelf (always pinned atop the books view) --- */ +.book-recent-shelf { + margin-bottom: 14px; +} +.book-recent-title { + font-size: 13px; + font-weight: 600; + margin: 0 0 6px; + color: var(--muted, #888); +} +.book-recent-row { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 2px; +} +.book-recent-item { + flex: 0 0 auto; + display: flex; + flex-direction: column; + gap: 2px; + max-width: 160px; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: none; + text-align: left; + cursor: pointer; +} +.book-recent-item-title { + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.book-recent-item-author { + font-size: 11px; + color: var(--muted, #888); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* --- Book folders (single level) --- */ +.book-folder-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; + margin-bottom: 10px; +} +.book-folder-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 12px 8px; + border: 1px solid var(--border); + border-radius: var(--radius); + background: none; + cursor: pointer; + text-align: center; +} +.book-folder-tile-icon { + font-size: 22px; +} +.book-folder-tile-name { + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} +.book-folder-tile-count { + font-size: 11px; + color: var(--muted, #888); +} +.book-folder-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + /* --- PDF pages --- */ .pdf-page-wrapper { margin: 0 auto 1rem; diff --git a/static/js/app.js b/static/js/app.js index 3ab4dfb..b7157a7 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3249,11 +3249,11 @@ async function loadBookList() { try { const metaBuf = await decryptBytes(key, b.meta_iv, b.meta_ct); const meta = JSON.parse(new TextDecoder().decode(metaBuf)); - bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub'}; - decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); + bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || ''}; + decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } catch (e) { - bookMetaCache[b.id] = {title: `Book #${b.id}`, author: '', type: 'epub'}; - decrypted.push({id: b.id, title: `Book #${b.id}`, author: '', type: 'epub', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); + bookMetaCache[b.id] = {title: `Book #${b.id}`, author: '', type: 'epub', filename: '', folder: ''}; + decrypted.push({id: b.id, title: `Book #${b.id}`, author: '', type: 'epub', folder: '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } } // If local cache is further ahead than the server, push it and use it for display. @@ -3292,7 +3292,7 @@ async function loadBookList() { if (b.last_read) return 1; return (b.uploaded_at || '').localeCompare(a.uploaded_at || ''); }); - for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub'}; + for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || ''}; const uploadArea = $('book-upload-area'); if (uploadArea) uploadArea.style.display = 'none'; renderBookList(cachedBooks); @@ -3415,23 +3415,23 @@ async function toggleBookRead(bookId, currentlyRead) { } } -function renderBookList(books) { - const listEl = $('book-list'); - if (!listEl) return; - _lastBookListData = books; - const visible = _bookShowRead ? books : books.filter(b => !b.is_read); - if (!visible.length) { - listEl.innerHTML = books.length - ? '

Keine ungelesenen Bücher. „Gelesene Bücher anzeigen“ aktivieren, um alle zu sehen.

' - : ''; - return; - } - let html = ''; - for (const b of visible) { - const pct = Math.round((b.scroll_fraction || 0) * 100); - const keyWarning = b.keyOk === false ? '⚠ wrong key' : ''; - const broken = _brokenBooks.has(b.id); - html += `
+// Folder assigned to a book lives inside its encrypted meta blob (like title/author), so the +// server never sees plaintext folder names — no schema change needed, and it rides along for +// free in /api/sync/. One level only: folders don't nest. +let _currentBookFolder = null; // null = root view (folder tiles + unfiled books) + +function _recentlyReadBooks(books) { + return books + .filter(b => b.last_read) + .sort((a, b) => b.last_read.localeCompare(a.last_read)) + .slice(0, 7); +} + +function _renderBookItemHtml(b) { + const pct = Math.round((b.scroll_fraction || 0) * 100); + const keyWarning = b.keyOk === false ? '⚠ wrong key' : ''; + const broken = _brokenBooks.has(b.id); + return `
${escapeHtml(b.title)}${keyWarning}${b.is_read ? ' ✓ gelesen' : ''} ${escapeHtml(b.author)} @@ -3440,13 +3440,127 @@ function renderBookList(books) {
${broken ? `` : ''} ${b.has_highlights ? `` : ''} +
`; +} + +function _openBookFolder(name) { + _currentBookFolder = name; + renderBookList(_lastBookListData); +} + +function renderBookList(books) { + const listEl = $('book-list'); + if (!listEl) return; + _lastBookListData = books; + + if (!books.length) { + listEl.innerHTML = ''; + return; } + + let html = ''; + + // Always-visible "recently read" shelf, independent of folder navigation and the + // read/unread filter below — it's a shortcut back into whatever was open last. + const recent = _recentlyReadBooks(books); + if (recent.length) { + html += '
' + + '

Zuletzt gelesen

' + + '
' + + recent.map(b => ``).join('') + + '
'; + } + + const visible = _bookShowRead ? books : books.filter(b => !b.is_read); + if (!visible.length) { + html += books.length + ? '

Keine ungelesenen Bücher. „Gelesene Bücher anzeigen“ aktivieren, um alle zu sehen.

' + : ''; + listEl.innerHTML = html; + return; + } + + const grouped = new Map(); + for (const b of visible) { + const key = b.folder || ''; + if (!grouped.has(key)) grouped.set(key, []); + grouped.get(key).push(b); + } + // Folder emptied out (last book moved/deleted, or read-filter hid it) — fall back to root. + if (_currentBookFolder !== null && !grouped.has(_currentBookFolder)) { + _currentBookFolder = null; + } + + if (_currentBookFolder === null) { + const folderNames = [...grouped.keys()].filter(k => k !== '').sort((a, b) => a.localeCompare(b, 'de')); + if (folderNames.length) { + html += '
' + + folderNames.map(name => ``).join('') + + '
'; + } + html += (grouped.get('') || []).map(_renderBookItemHtml).join(''); + } else { + html += `
+ + ${escapeHtml(_currentBookFolder)} +
`; + html += grouped.get(_currentBookFolder).map(_renderBookItemHtml).join(''); + } + listEl.innerHTML = html; + listEl.querySelectorAll('.book-folder-tile').forEach(tile => { + tile.addEventListener('click', () => _openBookFolder(tile.dataset.folder)); + }); +} + +async function assignBookFolder(bookId) { + const book = _lastBookListData.find(b => b.id === bookId); + if (!book) return; + const folders = [...new Set(_lastBookListData.map(b => b.folder).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'de')); + const hint = folders.length ? ` Vorhandene Ordner: ${folders.join(', ')}.` : ''; + const result = await customPrompt(`Ordner für „${book.title}“ (leer = kein Ordner).${hint}`, book.folder || ''); + if (result === null) return; + const folder = result.trim(); + if (folder === (book.folder || '')) return; + + try { + const key = await getOrCreateEncKey(); + const cached = bookMetaCache[bookId] || {}; + const metaJson = new TextEncoder().encode(JSON.stringify({ + title: cached.title || book.title, + author: cached.author || book.author, + filename: cached.filename || '', + type: cached.type || book.type || 'epub', + folder, + })); + const metaEnc = await encryptBytes(key, metaJson); + const res = await fetch(`/books/${bookId}/meta/`, { + method: 'POST', + headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, + body: JSON.stringify({meta_ct: metaEnc.ciphertext, meta_iv: metaEnc.iv}), + }); + const data = await res.json(); + if (!data.ok) throw new Error('Server error'); + book.folder = folder; + bookMetaCache[bookId] = {...cached, folder}; + _saveBookMeta(_lastBookListData); + if (folder) _currentBookFolder = folder; + renderBookList(_lastBookListData); + } catch (e) { + await customAlert('Konnte Ordner nicht ändern: ' + e.message); + } } function bookFileSelected(input) { diff --git a/static/js/sw.js b/static/js/sw.js index 900716b..31d3d26 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v35'; +const CACHE = 'diora-v36'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From 3e301b6f61f00c8770e922a46e6ca9badb915745 Mon Sep 17 00:00:00 2001 From: marwin Date: Mon, 17 Aug 2026 11:27:16 +0200 Subject: [PATCH 002/176] =?UTF-8?q?B=C3=BCcher:=20Regal-Metadaten-Lookup?= =?UTF-8?q?=20(DNB=20=E2=86=92=20Open=20Library)=20+=20Drei-Punkte-Men?= =?UTF-8?q?=C3=BC=20(SW=20v37)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manuell per neuem "Metadaten"-Menüpunkt ausgelöst (nie automatisch): der Server schlägt die aus dem EPUB extrahierte ISBN zuerst bei der DNB (SRU, DDC-Sachgruppe), dann bei Open Library nach und gibt nur ein kurzes Label zurück, ohne etwas zu speichern — das Label landet als Badge nur im ohnehin verschlüsselten Meta-Blob des Clients. Das ist eine bewusste, eng begrenzte Ausnahme vom "Server sieht nie Buchinhalte"-Prinzip: die Verschlüsselung dient vor allem der Absicherung gegen Piraterie-Vorwürfe, nicht striktem Zero-Knowledge gegenüber dem eigenen Server. Da pro Buch jetzt Reparieren/Herunterladen/Ordner/Metadaten/Lesestatus/Löschen zusammenkommen, wandern alle Aktionen außer "Open" in ein Drei-Punkte-Menü. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011j4LcoeddwFt6Rw8Wyaknj --- books/urls.py | 1 + books/views.py | 131 +++++++++++++++++++++++++++++++++++++++ diora/settings.py | 1 + static/css/app.css | 56 +++++++++++++++++ static/js/app.js | 151 +++++++++++++++++++++++++++++++++++---------- static/js/sw.js | 2 +- 6 files changed, 307 insertions(+), 35 deletions(-) diff --git a/books/urls.py b/books/urls.py index 0d3b782..e17e99f 100644 --- a/books/urls.py +++ b/books/urls.py @@ -4,6 +4,7 @@ from . import views urlpatterns = [ path('', views.book_list, name='book_list'), path('upload/', views.upload_book, name='upload_book'), + path('metadata-lookup/', views.lookup_book_metadata, name='lookup_book_metadata'), path('/data/', views.get_book_data, name='get_book_data'), path('/delete/', views.delete_book, name='delete_book'), path('/read/', views.set_book_read, name='set_book_read'), diff --git a/books/views.py b/books/views.py index 15d1445..f400531 100644 --- a/books/views.py +++ b/books/views.py @@ -1,7 +1,9 @@ import base64 import json import re +import xml.etree.ElementTree as ET +import requests from django.conf import settings from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt @@ -16,6 +18,135 @@ def _require_auth(request): return None +# --------------------------------------------------------------------------- +# Library-shelf metadata lookup (DNB, falling back to Open Library) +# +# Only ever called on explicit user request (the "Metadaten" book action), never +# automatically — the server briefly sees the plaintext ISBN for this one proxied +# request, which is a deliberate, narrow exception to the "server never sees book +# content" rule (see CLAUDE.md), made because the encryption's real purpose here is +# to keep the operator from being able to see what's on the platform (piracy +# liability), not strict user privacy — an ISBN lookup against public library +# catalogs doesn't undermine that. Nothing from this lookup is persisted server-side; +# the resulting label is stored only in the client's encrypted meta blob. +# --------------------------------------------------------------------------- + +# DNB "Sachgruppen der Deutschen Nationalbibliografie" / DDC divisions (hundreds -> tens), +# used to turn a raw DDC notation like "833.92" into a short shelf label. +_DDC_DIVISIONS = { + '000': 'Allgemeines, Informatik', '010': 'Bibliografien', '020': 'Bibliotheks- und Informationswissenschaft', + '030': 'Enzyklopädien', '050': 'Zeitschriften, fortlaufende Sammelwerke', '060': 'Organisationen, Museumswissenschaft', + '070': 'Nachrichtenmedien, Journalismus, Verlagswesen', '090': 'Handschriften, seltene Bücher', + '100': 'Philosophie', '130': 'Parapsychologie, Okkultismus', '150': 'Psychologie', + '200': 'Religion', '230': 'Christentum, Christliche Theologie', '290': 'Andere Religionen', + '300': 'Sozialwissenschaften, Soziologie', '310': 'Statistiken', '320': 'Politikwissenschaft', + '330': 'Wirtschaft', '340': 'Recht', '350': 'Öffentliche Verwaltung, Militärwissenschaft', + '360': 'Soziale Probleme, Sozialdienste, Versicherungen', '370': 'Erziehung, Schul- und Bildungswesen', + '380': 'Handel, Kommunikation, Verkehr', '390': 'Gebräuche, Etikette, Folklore', + '400': 'Sprache, Linguistik', '420': 'Englisch', '430': 'Deutsch, Germanische Sprachen', + '440': 'Französisch, Romanische Sprachen', '450': 'Italienisch, Rumänisch, Rätoromanisch', + '460': 'Spanisch, Portugiesisch', '470': 'Latein, Italische Sprachen', '480': 'Griechisch', '490': 'Andere Sprachen', + '500': 'Naturwissenschaften', '510': 'Mathematik', '520': 'Astronomie', '530': 'Physik', '540': 'Chemie', + '550': 'Geowissenschaften', '560': 'Paläontologie', '570': 'Biowissenschaften, Biologie', + '580': 'Pflanzen (Botanik)', '590': 'Tiere (Zoologie)', + '600': 'Technik', '610': 'Medizin, Gesundheit', '620': 'Ingenieurwissenschaften', '630': 'Landwirtschaft', + '640': 'Hauswirtschaft', '650': 'Management', '660': 'Chemische Technik', '670': 'Industrielle Fertigung', + '680': 'Fertigung für spezielle Zwecke', '690': 'Hausbau, Bauhandwerk', + '700': 'Künste', '710': 'Landschaftsgestaltung, Raumplanung', '720': 'Architektur', + '730': 'Plastik, Keramik, Metallkunst', '740': 'Zeichnung, angewandte Kunst', '750': 'Malerei', + '760': 'Grafik, Druckgrafik, Fotografie', '780': 'Musik', '790': 'Freizeit, Darstellende Kunst, Sport', + '800': 'Literatur', '810': 'Amerikanische Literatur', '820': 'Englische Literatur', + '830': 'Deutsche Literatur', '840': 'Französische Literatur', '850': 'Italienische Literatur', + '860': 'Spanische, Portugiesische Literatur', '870': 'Lateinische Literatur', '880': 'Griechische Literatur', + '890': 'Literaturen in anderen Sprachen', + '900': 'Geschichte', '910': 'Geografie, Reisen', '920': 'Biografie, Genealogie', + '930': 'Geschichte des Altertums', '940': 'Geschichte Europas', '950': 'Geschichte Asiens', + '960': 'Geschichte Afrikas', '970': 'Geschichte Nordamerikas', '980': 'Geschichte Südamerikas', + '990': 'Geschichte der übrigen Welt', +} + + +def _ddc_label(ddc_raw): + """Coarsen a DDC notation ('833.92') down to its nearest known division/class label.""" + digits = re.sub(r'\D', '', ddc_raw or '') + if len(digits) < 3: + return None + tens = digits[:2] + '0' + if tens in _DDC_DIVISIONS: + return _DDC_DIVISIONS[tens] + return _DDC_DIVISIONS.get(digits[0] + '00') + + +_MARC_NS = '{http://www.loc.gov/MARC21/slim}' + + +def _lookup_dnb_shelf(isbn): + url = 'https://services.dnb.de/sru/dnb' + params = { + 'version': '1.1', + 'operation': 'searchRetrieve', + 'query': f'dnb.num={isbn}', + 'recordSchema': 'MARC21-xml', + 'maximumRecords': '1', + } + resp = requests.get(url, params=params, timeout=getattr(settings, 'BOOK_METADATA_TIMEOUT', 6)) + resp.raise_for_status() + root = ET.fromstring(resp.content) + for datafield in root.iter(f'{_MARC_NS}datafield'): + if datafield.get('tag') != '082': + continue + for subfield in datafield.findall(f'{_MARC_NS}subfield'): + if subfield.get('code') == 'a' and subfield.text: + label = _ddc_label(subfield.text) + if label: + return label + return None + + +def _lookup_openlibrary_shelf(isbn): + url = 'https://openlibrary.org/api/books' + params = {'bibkeys': f'ISBN:{isbn}', 'jscmd': 'data', 'format': 'json'} + resp = requests.get(url, params=params, timeout=getattr(settings, 'BOOK_METADATA_TIMEOUT', 6)) + resp.raise_for_status() + data = resp.json().get(f'ISBN:{isbn}', {}) + ddc = (data.get('classifications') or {}).get('dewey_decimal_class') or [] + if ddc: + label = _ddc_label(ddc[0]) + if label: + return label + subjects = data.get('subjects') or [] + if subjects: + return subjects[0].get('name') + return None + + +@require_http_methods(['GET']) +def lookup_book_metadata(request): + err = _require_auth(request) + if err: + return err + + isbn = re.sub(r'[^0-9Xx]', '', request.GET.get('isbn', '')) + if len(isbn) not in (10, 13): + return JsonResponse({'error': 'invalid ISBN'}, status=400) + + label = None + source = None + try: + label = _lookup_dnb_shelf(isbn) + source = 'dnb' if label else None + except Exception: + pass + if not label: + try: + label = _lookup_openlibrary_shelf(isbn) + source = 'openlibrary' if label else None + except Exception: + pass + + return JsonResponse({'label': label, 'source': source}) + + def _anchor_parts(anchor): """Split a position anchor 'blockIndex:innerFraction' into (block, inner). diff --git a/diora/settings.py b/diora/settings.py index f1d0571..53f6905 100644 --- a/diora/settings.py +++ b/diora/settings.py @@ -108,6 +108,7 @@ BOOKMARKS_MAX_BYTES = 100 * 1024 # 100 KB VOLUME_DEFAULT = 204 # out of 255 ITUNES_TIMEOUT = 6 # seconds +BOOK_METADATA_TIMEOUT = 6 # seconds (DNB / Open Library shelf lookup) PODCAST_INBOX_PAGE_SIZE = 200 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/static/css/app.css b/static/css/app.css index 3ed7abd..a2f0264 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1583,8 +1583,14 @@ body.dnd-mode .timer-display { .book-progress { font-size: 12px; } +.book-item-meta-row { + display: flex; + align-items: center; + gap: 8px; +} .book-item-actions { display: flex; + align-items: center; gap: 6px; flex-shrink: 0; } @@ -1592,6 +1598,56 @@ body.dnd-mode .timer-display { font-size: 12px; font-weight: normal; } +.book-shelf-badge { + display: inline-block; + font-size: 11px; + padding: 1px 7px; + border: 1px solid var(--border); + border-radius: 10px; + color: var(--muted, #888); + white-space: nowrap; +} + +/* --- Per-book "⋮" menu (everything except Open) --- */ +.book-item-menu { + position: relative; +} +.book-item-menu-list { + display: none; + flex-direction: column; + gap: 2px; + position: absolute; + right: 0; + top: calc(100% + 4px); + z-index: 20; + min-width: 220px; + background: var(--surface, #111); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 4px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); +} +.book-item-menu-list.open { + display: flex; +} +.book-menu-item { + background: none; + border: none; + color: var(--fg); + text-align: left; + padding: 8px 10px; + border-radius: var(--radius); + cursor: pointer; + font: inherit; + font-size: 13px; + white-space: nowrap; +} +.book-menu-item:hover { + background: var(--bg-row, rgba(255, 255, 255, 0.08)); +} +.book-menu-item--danger { + color: var(--accent, #e63946); +} .book-list-filter { display: flex; align-items: center; diff --git a/static/js/app.js b/static/js/app.js index b7157a7..dacfef4 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3249,11 +3249,11 @@ async function loadBookList() { try { const metaBuf = await decryptBytes(key, b.meta_iv, b.meta_ct); const meta = JSON.parse(new TextDecoder().decode(metaBuf)); - bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || ''}; - decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); + bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || ''}; + decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } catch (e) { - bookMetaCache[b.id] = {title: `Book #${b.id}`, author: '', type: 'epub', filename: '', folder: ''}; - decrypted.push({id: b.id, title: `Book #${b.id}`, author: '', type: 'epub', folder: '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); + bookMetaCache[b.id] = {title: `Book #${b.id}`, author: '', type: 'epub', filename: '', folder: '', isbn: '', shelfTag: ''}; + decrypted.push({id: b.id, title: `Book #${b.id}`, author: '', type: 'epub', folder: '', isbn: '', shelfTag: '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } } // If local cache is further ahead than the server, push it and use it for display. @@ -3292,7 +3292,7 @@ async function loadBookList() { if (b.last_read) return 1; return (b.uploaded_at || '').localeCompare(a.uploaded_at || ''); }); - for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || ''}; + for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || '', isbn: b.isbn || '', shelfTag: b.shelfTag || ''}; const uploadArea = $('book-upload-area'); if (uploadArea) uploadArea.style.display = 'none'; renderBookList(cachedBooks); @@ -3322,6 +3322,15 @@ function markBookBroken(bookId) { } } +function _extractIsbnFromOpf(opfDoc) { + const identifiers = opfDoc.querySelectorAll('metadata > identifier, metadata > *|identifier'); + for (const el of identifiers) { + const digits = (el.textContent || '').replace(/[^0-9Xx]/g, ''); + if (digits.length === 10 || digits.length === 13) return digits; + } + return ''; +} + function repairBook(bookId) { const input = document.createElement('input'); input.type = 'file'; @@ -3338,7 +3347,7 @@ function repairBook(bookId) { const isPdf = /\.pdf$/i.test(file.name); const type = isPdf ? 'pdf' : 'epub'; - let title = file.name.replace(/\.(epub|pdf)$/i, ''), author = ''; + let title = file.name.replace(/\.(epub|pdf)$/i, ''), author = '', isbn = ''; try { if (isPdf) { const pdfDoc = await pdfjsLib.getDocument({data: new Uint8Array(buf.slice(0))}).promise; @@ -3356,11 +3365,20 @@ function repairBook(bookId) { await zip.file(opfPath).async('text'), 'application/xml'); title = opfDoc.querySelector('metadata > title, metadata > *|title')?.textContent?.trim() || title; author = opfDoc.querySelector('metadata > creator, metadata > *|creator')?.textContent?.trim() || ''; + isbn = _extractIsbnFromOpf(opfDoc); } } } catch (e) { /* keep filename as title */ } - const metaJson = new TextEncoder().encode(JSON.stringify({title, author, filename: file.name, type})); + // Preserve folder/shelfTag assigned before this repair — replacing the book's + // data shouldn't silently wipe organization the user already set up. + const prevMeta = bookMetaCache[bookId] || {}; + const metaJson = new TextEncoder().encode(JSON.stringify({ + title, author, filename: file.name, type, + isbn: isbn || prevMeta.isbn || '', + folder: prevMeta.folder || '', + shelfTag: prevMeta.shelfTag || '', + })); const [metaEnc, dataEnc] = await Promise.all([ encryptBytes(key, metaJson), encryptBytes(key, buf), @@ -3435,19 +3453,43 @@ function _renderBookItemHtml(b) {
${escapeHtml(b.title)}${keyWarning}${b.is_read ? ' ✓ gelesen' : ''} ${escapeHtml(b.author)} - ${pct > 0 ? `${pct}% read` : ''} + + ${pct > 0 ? `${pct}% read` : ''} + ${b.shelfTag ? `${escapeHtml(b.shelfTag)}` : ''} +
- ${broken ? `` : ''} - ${b.has_highlights ? `` : ''} - - - +
+ +
+ ${broken ? `` : ''} + ${b.has_highlights ? `` : ''} + + + + +
+
`; } +function toggleBookMenu(bookId) { + const menu = document.getElementById(`book-menu-${bookId}`); + if (!menu) return; + const willOpen = !menu.classList.contains('open'); + document.querySelectorAll('.book-item-menu-list.open').forEach(m => m.classList.remove('open')); + if (willOpen) menu.classList.add('open'); +} + +// Close any open book menu on an outside click, or right after an item inside it is +// clicked (the toggle button manages its own open/close state above, so it's excluded). +document.addEventListener('click', (e) => { + if (e.target.closest('.book-menu-toggle')) return; + document.querySelectorAll('.book-item-menu-list.open').forEach(m => m.classList.remove('open')); +}); + function _openBookFolder(name) { _currentBookFolder = name; renderBookList(_lastBookListData); @@ -3525,6 +3567,38 @@ function renderBookList(books) { }); } +// Shared by assignBookFolder/lookupBookMetadata: re-encrypts the meta blob with the given +// field overrides applied on top of whatever is already cached, and pushes it to the server. +async function _updateBookMeta(bookId, overrides) { + const book = _lastBookListData.find(b => b.id === bookId); + if (!book) return false; + const cached = bookMetaCache[bookId] || {}; + const merged = { + title: cached.title || book.title, + author: cached.author || book.author, + filename: cached.filename || '', + type: cached.type || book.type || 'epub', + isbn: cached.isbn || book.isbn || '', + folder: cached.folder || book.folder || '', + shelfTag: cached.shelfTag || book.shelfTag || '', + ...overrides, + }; + const key = await getOrCreateEncKey(); + const metaJson = new TextEncoder().encode(JSON.stringify(merged)); + const metaEnc = await encryptBytes(key, metaJson); + const res = await fetch(`/books/${bookId}/meta/`, { + method: 'POST', + headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, + body: JSON.stringify({meta_ct: metaEnc.ciphertext, meta_iv: metaEnc.iv}), + }); + const data = await res.json(); + if (!data.ok) throw new Error('Server error'); + Object.assign(book, overrides); + bookMetaCache[bookId] = merged; + _saveBookMeta(_lastBookListData); + return true; +} + async function assignBookFolder(bookId) { const book = _lastBookListData.find(b => b.id === bookId); if (!book) return; @@ -3536,26 +3610,7 @@ async function assignBookFolder(bookId) { if (folder === (book.folder || '')) return; try { - const key = await getOrCreateEncKey(); - const cached = bookMetaCache[bookId] || {}; - const metaJson = new TextEncoder().encode(JSON.stringify({ - title: cached.title || book.title, - author: cached.author || book.author, - filename: cached.filename || '', - type: cached.type || book.type || 'epub', - folder, - })); - const metaEnc = await encryptBytes(key, metaJson); - const res = await fetch(`/books/${bookId}/meta/`, { - method: 'POST', - headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, - body: JSON.stringify({meta_ct: metaEnc.ciphertext, meta_iv: metaEnc.iv}), - }); - const data = await res.json(); - if (!data.ok) throw new Error('Server error'); - book.folder = folder; - bookMetaCache[bookId] = {...cached, folder}; - _saveBookMeta(_lastBookListData); + await _updateBookMeta(bookId, {folder}); if (folder) _currentBookFolder = folder; renderBookList(_lastBookListData); } catch (e) { @@ -3563,6 +3618,32 @@ async function assignBookFolder(bookId) { } } +// Manually triggered (never automatic) — looks up which library shelf/DDC category this +// book falls under via the server-side DNB→Open Library proxy, purely as an organizational +// hint (shown as a badge). See books/views.py:lookup_book_metadata for why this one call is +// allowed to briefly touch the server with a plaintext ISBN. +async function lookupBookMetadata(bookId) { + const book = _lastBookListData.find(b => b.id === bookId); + if (!book) return; + const isbn = (bookMetaCache[bookId] || {}).isbn || book.isbn || ''; + if (!isbn) { + await customAlert('Keine ISBN in den Metadaten dieses Buchs gefunden (nur bei neueren Uploads automatisch erkannt).'); + return; + } + try { + const res = await fetch(`/books/metadata-lookup/?isbn=${encodeURIComponent(isbn)}`); + const data = await res.json(); + if (!data.label) { + await customAlert('Keine Regal-Kategorie gefunden (weder bei der DNB noch bei Open Library).'); + return; + } + await _updateBookMeta(bookId, {shelfTag: data.label}); + renderBookList(_lastBookListData); + } catch (e) { + await customAlert('Metadaten-Abruf fehlgeschlagen: ' + e.message); + } +} + function bookFileSelected(input) { const file = input.files[0]; if (!file) return; @@ -3644,6 +3725,7 @@ async function uploadEbook(file) { let title = file.name.replace(/\.(epub|pdf)$/i, ''); let author = ''; + let isbn = ''; const type = isPdf ? 'pdf' : 'epub'; if (isPdf) { @@ -3664,12 +3746,13 @@ async function uploadEbook(file) { const opfDoc = new DOMParser().parseFromString(opfText, 'application/xml'); title = opfDoc.querySelector('metadata > title, metadata > *|title')?.textContent?.trim() || title; author = opfDoc.querySelector('metadata > creator, metadata > *|creator')?.textContent?.trim() || ''; + isbn = _extractIsbnFromOpf(opfDoc); } } catch (e) { /* use filename as title */ } } const key = await getOrCreateEncKey(); - const metaJson = new TextEncoder().encode(JSON.stringify({title, author, filename: file.name, type})); + const metaJson = new TextEncoder().encode(JSON.stringify({title, author, filename: file.name, type, isbn, folder: '', shelfTag: ''})); const [metaEnc, dataEnc] = await Promise.all([ encryptBytes(key, metaJson), encryptBytes(key, buf), diff --git a/static/js/sw.js b/static/js/sw.js index 31d3d26..5017b17 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v36'; +const CACHE = 'diora-v37'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From e6b3ee620ecf850166747939f163cbf55eb384cf Mon Sep 17 00:00:00 2001 From: marwin Date: Mon, 17 Aug 2026 12:12:17 +0200 Subject: [PATCH 003/176] =?UTF-8?q?B=C3=BCcher:=20Titel=20in=20"Zuletzt=20?= =?UTF-8?q?gelesen"/Ordner-Kacheln=20unsichtbar=20(Button-Farbvererbung);?= =?UTF-8?q?=20Regal-Lookup=20jetzt=20auch=20f=C3=BCr=20Altbestand=20(SW=20?= =?UTF-8?q?v38)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit
- -
+ +
${broken ? `` : ''} ${b.has_highlights ? `` : ''} @@ -3475,8 +3475,11 @@ function _renderBookItemHtml(b) {
`; } -function toggleBookMenu(bookId) { - const menu = document.getElementById(`book-menu-${bookId}`); +function toggleBookMenu(toggleBtn) { + // Looked up relative to the clicked button (not by id) because the same book — and + // thus the same rendered menu — can appear twice at once: once in the "Zuletzt + // gelesen" section and again further down in the folder/unfiled list. + const menu = toggleBtn.closest('.book-item-menu')?.querySelector('.book-item-menu-list'); if (!menu) return; const willOpen = !menu.classList.contains('open'); document.querySelectorAll('.book-item-menu-list.open').forEach(m => m.classList.remove('open')); @@ -3507,18 +3510,15 @@ function renderBookList(books) { let html = ''; - // Always-visible "recently read" shelf, independent of folder navigation and the - // read/unread filter below — it's a shortcut back into whatever was open last. + // Always-visible "recently read" section, independent of folder navigation and the + // read/unread filter below — it's a shortcut back into whatever was open last. Same + // vertical list-item look as the rest of the list (not a card shelf). const recent = _recentlyReadBooks(books); if (recent.length) { - html += '
' + html += '
' + '

Zuletzt gelesen

' - + '
' - + recent.map(b => ``).join('') - + '
'; + + recent.map(_renderBookItemHtml).join('') + + '
'; } const visible = _bookShowRead ? books : books.filter(b => !b.is_read); diff --git a/static/js/sw.js b/static/js/sw.js index a445d84..990752a 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v39'; +const CACHE = 'diora-v40'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From b205625e21d4f40922c661a747dbe50e3a9b2013 Mon Sep 17 00:00:00 2001 From: marwin Date: Fri, 28 Aug 2026 08:26:22 +0000 Subject: [PATCH 006/176] =?UTF-8?q?B=C3=BCcher:=20Cloud-Import=20aus=20Web?= =?UTF-8?q?DAV/Nextcloud=20(SW=20v41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud, ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen. Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser: uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext. Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen: - assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global durchlässt) - _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der Guard per DNS-Rebinding umgehbar - Redirects werden abgelehnt statt verfolgt - identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12 gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen. Co-Authored-By: Claude Opus 5 --- .env.example | 5 + accounts/migrations/0005_webdavsource.py | 33 ++ accounts/models.py | 52 +++ accounts/tests.py | 441 ++++++++++++++++++++++- accounts/urls.py | 3 + accounts/views.py | 76 +++- accounts/webdav.py | 380 +++++++++++++++++++ books/urls.py | 5 +- books/webdav.py | 89 +++++ diora/settings.py | 8 + static/css/app.css | 155 ++++++++ static/js/app.js | 188 +++++++++- static/js/sw.js | 2 +- templates/accounts/settings.html | 69 ++++ templates/radio/player.html | 10 + 15 files changed, 1498 insertions(+), 18 deletions(-) create mode 100644 accounts/migrations/0005_webdavsource.py create mode 100644 accounts/webdav.py create mode 100644 books/webdav.py diff --git a/.env.example b/.env.example index d869df1..33cc233 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,8 @@ DEBUG=True AMAZON_AFFILIATE_TAG=diora-20 LASTFM_API_KEY= LASTFM_API_SECRET= + +# Cloud import (WebDAV/Nextcloud): allow connections to LAN/loopback addresses. +# Leave False on any instance with open registration — it is what stops a user +# from probing the internal network through the import proxy. +WEBDAV_ALLOW_PRIVATE_HOSTS=False diff --git a/accounts/migrations/0005_webdavsource.py b/accounts/migrations/0005_webdavsource.py new file mode 100644 index 0000000..910650e --- /dev/null +++ b/accounts/migrations/0005_webdavsource.py @@ -0,0 +1,33 @@ +# Generated by Django 6.1 on 2026-08-28 08:01 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_apitoken'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='WebDAVSource', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', models.CharField(max_length=100)), + ('base_url', models.URLField(max_length=500)), + ('username', models.CharField(blank=True, max_length=200)), + ('password', models.CharField(blank=True, max_length=500)), + ('root_path', models.CharField(blank=True, default='', max_length=500)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('last_used_at', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webdav_sources', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['created_at'], + }, + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 616a150..38e8229 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,4 +1,5 @@ import secrets +from urllib.parse import urlsplit from django.db import models from django.contrib.auth.models import User @@ -37,6 +38,57 @@ def save_user_profile(sender, instance, **kwargs): instance.profile.save() +class WebDAVSource(models.Model): + """A user-supplied WebDAV endpoint that ebooks can be imported from. + + Deliberately generic rather than Nextcloud-specific — any WebDAV server + (Nextcloud, ownCloud, Synology, rclone serve, …) works, Nextcloud just gets + a URL-shorthand in `normalized_base_url`. + + The password is stored in the clear because the server has to replay it on + every PROPFIND/GET (same trade-off as `lastfm_session_key` above). The UI + therefore tells users to create a revocable *app password* rather than + handing over their account password. + """ + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='webdav_sources') + label = models.CharField(max_length=100) + base_url = models.URLField(max_length=500) + username = models.CharField(max_length=200, blank=True) + password = models.CharField(max_length=500, blank=True) + root_path = models.CharField(max_length=500, blank=True, default='') + created_at = models.DateTimeField(auto_now_add=True) + last_used_at = models.DateTimeField(null=True, blank=True) + + class Meta: + ordering = ['created_at'] + + def __str__(self): + return f"WebDAVSource({self.label}, user={self.user_id})" + + def normalized_base_url(self) -> str: + """Collection root to resolve browse paths against, always ending in '/'. + + A bare host ('https://cloud.example.com') is expanded to the Nextcloud + files endpoint, since that is the URL users actually have at hand; an + URL that already points into a DAV tree is left alone so non-Nextcloud + servers stay usable. + """ + url = self.base_url.strip() + if not url.endswith('/'): + url += '/' + # Match against the path only — a host literally named "webdav.…" or + # "dav.…" must not be mistaken for a URL that already points into a + # DAV tree. + path = urlsplit(url).path.lower() + is_dav = any(marker in path for marker in ('/remote.php/', '/dav/', '/webdav')) + if not is_dav and self.username: + url += f'remote.php/dav/files/{self.username}/' + root = self.root_path.strip().strip('/') + if root: + url += root + '/' + return url + + def _generate_token(): return secrets.token_hex(32) diff --git a/accounts/tests.py b/accounts/tests.py index 49d830c..4d4e05a 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,9 +1,13 @@ import json +import socket +from types import SimpleNamespace +from unittest.mock import patch from django.contrib.auth.models import User -from django.test import TestCase +from django.test import TestCase, override_settings -from .models import ApiToken +from .models import ApiToken, WebDAVSource +from .webdav import WebDAVError, assert_safe_url, fetch_file, list_directory, safe_rel_path from books.models import EBook, EBookProgress, EBookHighlights @@ -89,3 +93,436 @@ class SyncSnapshotTests(TestCase): # No data_ct/data_iv leaked into the snapshot (book bytes stay lazy-fetched) self.assertNotIn('data_ct', data['books'][0]) + + +# --------------------------------------------------------------------------- +# WebDAV cloud import +# --------------------------------------------------------------------------- + +PROPFIND_RESPONSE = b''' + + + /remote.php/dav/files/alice/Books/ + + + + /remote.php/dav/files/alice/Books/Sci-Fi/ + + + + /remote.php/dav/files/alice/Books/Der%20Steppenwolf.epub + 4096 + + + /remote.php/dav/files/alice/Books/notes.txt + 12 + +''' + + +class _FakeSocket: + def __init__(self, peer): + self._peer = peer + + def getpeername(self): + return (self._peer, 443) + + +class _FakeResponse: + def __init__(self, status_code=207, content=b'', headers=None, peer=None): + self.status_code = status_code + self.content = content + self.headers = headers or {} + self.closed = False + # Mirrors requests' response.raw._connection.sock, which is what + # _assert_peer_is_safe introspects. None means "nothing to check". + if peer is None: + self.raw = None + else: + self.raw = SimpleNamespace(_connection=SimpleNamespace(sock=_FakeSocket(peer))) + + def iter_content(self, chunk_size=None): + yield self.content + + def close(self): + self.closed = True + + +class NormalizedBaseUrlTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + + def _source(self, **kwargs): + kwargs.setdefault('label', 'cloud') + kwargs.setdefault('username', 'alice') + return WebDAVSource(user=self.user, **kwargs) + + def test_bare_host_expands_to_nextcloud_files_endpoint(self): + source = self._source(base_url='https://cloud.example.com') + self.assertEqual( + source.normalized_base_url(), + 'https://cloud.example.com/remote.php/dav/files/alice/', + ) + + def test_root_path_is_appended(self): + source = self._source(base_url='https://cloud.example.com', root_path='/Buecher/') + self.assertEqual( + source.normalized_base_url(), + 'https://cloud.example.com/remote.php/dav/files/alice/Buecher/', + ) + + def test_explicit_dav_url_is_left_alone(self): + source = self._source(base_url='https://dav.example.com/webdav') + self.assertEqual(source.normalized_base_url(), 'https://dav.example.com/webdav/') + + def test_generic_server_without_username_is_not_rewritten(self): + source = self._source(base_url='https://files.example.com/share', username='') + self.assertEqual(source.normalized_base_url(), 'https://files.example.com/share/') + + +class SafeRelPathTests(TestCase): + def test_traversal_is_rejected(self): + with self.assertRaises(WebDAVError): + safe_rel_path('Books/../../etc/passwd') + + def test_leading_slashes_and_dots_are_stripped(self): + self.assertEqual(safe_rel_path('/Books/./Sci-Fi/'), 'Books/Sci-Fi') + + def test_empty_path_is_root(self): + self.assertEqual(safe_rel_path(''), '') + self.assertEqual(safe_rel_path('/'), '') + + +class AssertSafeUrlTests(TestCase): + def _resolve_to(self, ip): + return [(2, 1, 6, '', (ip, 443))] + + def test_non_http_scheme_rejected(self): + with self.assertRaises(WebDAVError): + assert_safe_url('file:///etc/passwd') + + def test_private_address_rejected(self): + with patch('accounts.webdav.socket.getaddrinfo', return_value=self._resolve_to('172.18.0.4')): + with self.assertRaises(WebDAVError): + assert_safe_url('https://internal.example.com/dav/') + + def test_loopback_rejected(self): + with patch('accounts.webdav.socket.getaddrinfo', return_value=self._resolve_to('127.0.0.1')): + with self.assertRaises(WebDAVError): + assert_safe_url('http://localhost:11000/remote.php/dav/') + + def test_link_local_metadata_endpoint_rejected(self): + with patch('accounts.webdav.socket.getaddrinfo', return_value=self._resolve_to('169.254.169.254')): + with self.assertRaises(WebDAVError): + assert_safe_url('http://metadata.example.com/') + + def test_public_address_allowed(self): + with patch('accounts.webdav.socket.getaddrinfo', return_value=self._resolve_to('85.214.6.118')): + assert_safe_url('https://nc.example.com/remote.php/dav/') + + def test_unresolvable_host_rejected(self): + with patch('accounts.webdav.socket.getaddrinfo', side_effect=socket.gaierror): + with self.assertRaises(WebDAVError): + assert_safe_url('https://nope.example.com/') + + @override_settings(WEBDAV_ALLOW_PRIVATE_HOSTS=True) + def test_private_allowed_when_opted_in(self): + assert_safe_url('http://192.168.1.10/dav/') + + +class ListDirectoryTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.source = WebDAVSource.objects.create( + user=self.user, label='cloud', base_url='https://cloud.example.com', + username='alice', password='app-pw', root_path='Books', + ) + + def _list(self, response=None): + with patch('accounts.webdav.assert_safe_url'), \ + patch('accounts.webdav.requests.request', + return_value=response or _FakeResponse(content=PROPFIND_RESPONSE)): + return list_directory(self.source) + + def test_entries_are_parsed_and_sorted_dirs_first(self): + entries = self._list() + self.assertEqual([e['name'] for e in entries], + ['Sci-Fi', 'Der Steppenwolf.epub', 'notes.txt']) + + def test_collection_itself_is_excluded(self): + self.assertNotIn('Books', [e['name'] for e in self._list()]) + + def test_book_flag_and_size(self): + by_name = {e['name']: e for e in self._list()} + self.assertTrue(by_name['Der Steppenwolf.epub']['is_book']) + self.assertEqual(by_name['Der Steppenwolf.epub']['size'], 4096) + self.assertFalse(by_name['notes.txt']['is_book']) + self.assertTrue(by_name['Sci-Fi']['is_dir']) + + def test_redirect_is_refused_rather_than_followed(self): + redirect_response = _FakeResponse(status_code=302, headers={'Location': 'http://127.0.0.1/'}) + with self.assertRaises(WebDAVError): + self._list(redirect_response) + + def test_bad_credentials_surface_clearly(self): + with self.assertRaises(WebDAVError) as ctx: + self._list(_FakeResponse(status_code=401)) + self.assertIn('App-Passwort', str(ctx.exception)) + + +class FetchFileTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.source = WebDAVSource.objects.create( + user=self.user, label='cloud', base_url='https://cloud.example.com', username='alice', + ) + + def test_non_book_extension_refused_before_any_request(self): + with patch('accounts.webdav.requests.request') as mock_request: + with self.assertRaises(WebDAVError): + fetch_file(self.source, 'secrets.env', 1024) + mock_request.assert_not_called() + + def test_declared_oversize_refused(self): + response = _FakeResponse(status_code=200, headers={'Content-Length': '99999'}) + with patch('accounts.webdav.assert_safe_url'), \ + patch('accounts.webdav.requests.request', return_value=response): + with self.assertRaises(WebDAVError): + fetch_file(self.source, 'big.epub', 1024) + + def test_streamed_oversize_refused_even_without_content_length(self): + response = _FakeResponse(status_code=200, content=b'x' * 5000) + with patch('accounts.webdav.assert_safe_url'), \ + patch('accounts.webdav.requests.request', return_value=response): + with self.assertRaises(WebDAVError): + fetch_file(self.source, 'sneaky.epub', 1024) + + def test_successful_fetch_returns_bytes(self): + response = _FakeResponse(status_code=200, content=b'EPUB-BYTES') + with patch('accounts.webdav.assert_safe_url'), \ + patch('accounts.webdav.requests.request', return_value=response): + self.assertEqual(fetch_file(self.source, 'ok.epub', 1024), b'EPUB-BYTES') + + +class CloudImportViewTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.other = User.objects.create_user(username='bob', password='pw12345678') + self.source = WebDAVSource.objects.create( + user=self.user, label='cloud', base_url='https://cloud.example.com', username='alice', + ) + self.foreign = WebDAVSource.objects.create( + user=self.other, label='bobs', base_url='https://other.example.com', username='bob', + ) + + def test_endpoints_require_authentication(self): + self.assertEqual(self.client.get('/books/cloud/sources/').status_code, 401) + self.assertEqual(self.client.get(f'/books/cloud/{self.source.pk}/browse/').status_code, 401) + self.assertEqual(self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=a.epub').status_code, 401) + + def test_sources_are_scoped_to_the_owner(self): + self.client.force_login(self.user) + labels = [s['label'] for s in self.client.get('/books/cloud/sources/').json()['sources']] + self.assertEqual(labels, ['cloud']) + + def test_foreign_source_is_not_browsable(self): + self.client.force_login(self.user) + resp = self.client.get(f'/books/cloud/{self.foreign.pk}/browse/') + self.assertEqual(resp.status_code, 404) + + def test_browse_returns_entries(self): + self.client.force_login(self.user) + entries = [{'name': 'Dune.epub', 'path': 'Dune.epub', 'is_dir': False, + 'size': 10, 'modified': '', 'is_book': True}] + with patch('books.webdav.list_directory', return_value=entries): + resp = self.client.get(f'/books/cloud/{self.source.pk}/browse/') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.json()['entries'], entries) + + def test_browse_reports_upstream_failure_as_502(self): + self.client.force_login(self.user) + with patch('books.webdav.list_directory', side_effect=WebDAVError('kaputt')): + resp = self.client.get(f'/books/cloud/{self.source.pk}/browse/') + self.assertEqual(resp.status_code, 502) + self.assertEqual(resp.json()['error'], 'kaputt') + + def test_bad_user_input_is_400_not_502(self): + self.client.force_login(self.user) + resp = self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=../../etc/passwd') + self.assertEqual(resp.status_code, 400) + resp = self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=notes.txt') + self.assertEqual(resp.status_code, 400) + + def test_fetch_streams_bytes_without_storing_them(self): + self.client.force_login(self.user) + with patch('books.webdav.fetch_file', return_value=b'EPUB-BYTES'): + resp = self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=Dune.epub') + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.content, b'EPUB-BYTES') + # The proxy is a pass-through: nothing is persisted server-side. + self.assertEqual(EBook.objects.count(), 0) + + +BILLION_LAUGHS = b''' + + + + + +]> +&lol4;''' + +# A server that answers with the 404 propstat first — legal per RFC 4918, and +# what made directories disappear from the listing before _select_prop existed. +PROPSTAT_404_FIRST = b''' + + + /remote.php/dav/files/alice/Books/ + + HTTP/1.1 200 OK + + + /remote.php/dav/files/alice/Books/Sci-Fi/ + + HTTP/1.1 404 Not Found + + HTTP/1.1 200 OK + +''' + +RELATIVE_HREF_RESPONSE = b''' + + Dune.epub + 7 + HTTP/1.1 200 OK + +''' + + +class WebDAVHardeningTests(TestCase): + """Regressions for the SSRF / resource-exhaustion review findings.""" + + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.source = WebDAVSource.objects.create( + user=self.user, label='cloud', base_url='https://cloud.example.com', + username='alice', password='app-pw', root_path='Books', + ) + + def _list(self, response): + with patch('accounts.webdav.assert_safe_url'), \ + patch('accounts.webdav.requests.request', return_value=response): + return list_directory(self.source) + + # --- DNS rebinding ----------------------------------------------------- + + def test_peer_address_is_rechecked_after_connecting(self): + """A resolver that answers public-then-private must not leak a body. + + assert_safe_url passes (it is given a public answer), but the socket + actually landed on a Docker-internal address. + """ + response = _FakeResponse(content=PROPFIND_RESPONSE, peer='172.18.0.5') + with self.assertRaises(WebDAVError): + self._list(response) + self.assertTrue(response.closed) + + def test_public_peer_is_accepted(self): + response = _FakeResponse(content=PROPFIND_RESPONSE, peer='85.214.6.118') + self.assertTrue(self._list(response)) + + @override_settings(WEBDAV_ALLOW_PRIVATE_HOSTS=True) + def test_peer_check_respects_the_opt_out(self): + response = _FakeResponse(content=PROPFIND_RESPONSE, peer='192.168.1.10') + self.assertTrue(self._list(response)) + + # --- Resource exhaustion ---------------------------------------------- + + def test_entity_expansion_is_refused(self): + with self.assertRaises(WebDAVError): + self._list(_FakeResponse(content=BILLION_LAUGHS)) + + def test_oversized_listing_is_refused(self): + oversized = _FakeResponse(headers={'Content-Length': str(9 * 1024 * 1024)}) + with self.assertRaises(WebDAVError): + self._list(oversized) + self.assertTrue(oversized.closed) + + # --- Information disclosure ------------------------------------------- + + def test_rejection_message_leaks_neither_ip_nor_resolvability(self): + private = [(2, 1, 6, '', ('172.18.0.5', 443))] + with patch('accounts.webdav.socket.getaddrinfo', return_value=private): + with self.assertRaises(WebDAVError) as private_ctx: + assert_safe_url('http://forgejo-db/') + with patch('accounts.webdav.socket.getaddrinfo', side_effect=socket.gaierror): + with self.assertRaises(WebDAVError) as missing_ctx: + assert_safe_url('http://forgejo-db/') + + self.assertNotIn('172.18.0.5', str(private_ctx.exception)) + # Same wording either way, so the endpoint cannot be used to tell an + # existing internal host from a nonexistent one. + self.assertEqual(str(private_ctx.exception), str(missing_ctx.exception)) + + # --- Malformed input --------------------------------------------------- + + def test_invalid_port_is_reported_not_crashed(self): + with self.assertRaises(WebDAVError): + assert_safe_url('https://example.com:99999/dav/') + + def test_ipv6_transition_ranges_are_rejected(self): + for address in ('64:ff9b::7f00:1', '::127.0.0.1'): + with patch('accounts.webdav.socket.getaddrinfo', + return_value=[(10, 1, 6, '', (address, 443, 0, 0))]): + with self.assertRaises(WebDAVError, msg=address): + assert_safe_url('https://nat64.example.com/') + + # --- PROPFIND parsing -------------------------------------------------- + + def test_directory_survives_a_404_propstat_listed_first(self): + entries = self._list(_FakeResponse(content=PROPSTAT_404_FIRST)) + by_name = {e['name']: e for e in entries} + self.assertTrue(by_name['Sci-Fi']['is_dir']) + + def test_relative_hrefs_are_resolved(self): + entries = self._list(_FakeResponse(content=RELATIVE_HREF_RESPONSE)) + self.assertEqual([e['name'] for e in entries], ['Dune.epub']) + + # --- URL normalisation ------------------------------------------------- + + def test_host_named_webdav_still_gets_the_nextcloud_path(self): + source = WebDAVSource(user=self.user, label='x', + base_url='https://webdav.example.com', username='alice') + self.assertEqual( + source.normalized_base_url(), + 'https://webdav.example.com/remote.php/dav/files/alice/', + ) + + +class WebDAVSourceLimitTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.client.force_login(self.user) + + @override_settings(WEBDAV_MAX_SOURCES_PER_USER=2) + def test_sources_are_capped_per_user(self): + with patch('accounts.views._probe_source', return_value=(20, 'ok')): + for i in range(3): + self.client.post('/accounts/webdav/add/', + {'label': f'c{i}', 'base_url': 'https://example.com'}) + self.assertEqual(self.user.webdav_sources.count(), 2) + + +class CloudFetchCachingTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.source = WebDAVSource.objects.create( + user=self.user, label='cloud', base_url='https://cloud.example.com', username='alice', + ) + + def test_plaintext_bytes_are_not_cacheable(self): + self.client.force_login(self.user) + with patch('books.webdav.fetch_file', return_value=b'EPUB-BYTES'): + resp = self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=Dune.epub') + self.assertEqual(resp['Cache-Control'], 'no-store') diff --git a/accounts/urls.py b/accounts/urls.py index 82524c1..4bee71f 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -16,4 +16,7 @@ urlpatterns = [ path('check-password/', views.check_password, name='check_password'), path('change-password/', views.change_password, name='change_password'), path('api-token/regenerate/', views.regenerate_api_token, name='regenerate_api_token'), + path('webdav/add/', views.webdav_add, name='webdav_add'), + path('webdav//test/', views.webdav_test, name='webdav_test'), + path('webdav//delete/', views.webdav_delete, name='webdav_delete'), ] diff --git a/accounts/views.py b/accounts/views.py index 0e71d7a..519c073 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -1,6 +1,7 @@ import base64 import json from django.conf import settings +from django.contrib import messages from django.contrib.auth import authenticate, login, get_user_model, update_session_auth_hash from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordChangeForm @@ -11,7 +12,8 @@ from django.views.decorators.http import require_http_methods from radio import lastfm as lastfm_module -from .models import ApiToken +from .models import ApiToken, WebDAVSource +from .webdav import WebDAVError, list_directory User = get_user_model() @@ -75,6 +77,7 @@ def settings_view(request): 'has_lastfm': profile.has_lastfm(), 'password_form': PasswordChangeForm(request.user), 'api_token': getattr(request.user, 'api_token', None), + 'webdav_sources': request.user.webdav_sources.all(), } return render(request, 'accounts/settings.html', context) @@ -213,6 +216,7 @@ def change_password(request): 'password_form': form, 'password_form_open': True, 'api_token': getattr(request.user, 'api_token', None), + 'webdav_sources': request.user.webdav_sources.all(), }) @@ -236,3 +240,73 @@ def regenerate_api_token(request): token, _ = ApiToken.objects.get_or_create(user=request.user) token.regenerate() return redirect('settings') + + +# --------------------------------------------------------------------------- +# WebDAV sources (Nextcloud & friends) — used by the ebook cloud import +# --------------------------------------------------------------------------- + +def _probe_source(source): + """Report a source's reachability as a (level, message) pair for messages.""" + try: + entries = list_directory(source) + except WebDAVError as exc: + return messages.WARNING, f'„{source.label}“ gespeichert, aber nicht erreichbar: {exc}' + books = sum(1 for e in entries if e['is_book']) + folders = sum(1 for e in entries if e['is_dir']) + return messages.SUCCESS, ( + f'„{source.label}“ verbunden — {books} Buch/Bücher und {folders} Ordner im Startverzeichnis.' + ) + + +@login_required +@require_http_methods(['POST']) +def webdav_add(request): + label = request.POST.get('label', '').strip() + base_url = request.POST.get('base_url', '').strip() + + if not label or not base_url: + messages.error(request, 'Name und Server-URL sind erforderlich.') + return redirect('settings') + + # Each source costs a synchronous probe on add and is reachable from the + # import endpoints, so cap how many one account can pile up. + max_sources = getattr(settings, 'WEBDAV_MAX_SOURCES_PER_USER', 10) + if request.user.webdav_sources.count() >= max_sources: + messages.error(request, f'Maximal {max_sources} Cloud-Verbindungen pro Konto.') + return redirect('settings') + + source = WebDAVSource.objects.create( + user=request.user, + label=label[:100], + base_url=base_url[:500], + username=request.POST.get('username', '').strip()[:200], + password=request.POST.get('password', '')[:500], + root_path=request.POST.get('root_path', '').strip()[:500], + ) + + level, message = _probe_source(source) + messages.add_message(request, level, message) + return redirect('settings') + + +@login_required +@require_http_methods(['POST']) +def webdav_test(request, pk): + source = WebDAVSource.objects.filter(pk=pk, user=request.user).first() + if not source: + messages.error(request, 'Verbindung nicht gefunden.') + return redirect('settings') + + level, message = _probe_source(source) + messages.add_message(request, level, message) + return redirect('settings') + + +@login_required +@require_http_methods(['POST']) +def webdav_delete(request, pk): + deleted, _ = WebDAVSource.objects.filter(pk=pk, user=request.user).delete() + if deleted: + messages.success(request, 'Verbindung entfernt.') + return redirect('settings') diff --git a/accounts/webdav.py b/accounts/webdav.py new file mode 100644 index 0000000..3c0c5c6 --- /dev/null +++ b/accounts/webdav.py @@ -0,0 +1,380 @@ +"""Minimal, hardened WebDAV client behind the ebook cloud import. + +Only PROPFIND (Depth 1) and GET are implemented — enough to browse a remote +folder and pull a single file out of it. + +Every request here is made by the server to a URL the *user* supplied, which +makes this an SSRF surface: registration is open (accounts/views.py:23) and +diora normally runs in a Docker network next to other services. Three things +guard it, and all three are load-bearing: + + * `assert_safe_url` refuses hosts that resolve to non-public addresses, + * `_assert_peer_is_safe` re-checks the address we *actually* connected to, + because requests resolves the hostname a second time and a hostile resolver + can answer differently across the two lookups (DNS rebinding), + * redirects are refused rather than followed, so a public host cannot bounce + us onto a private one. + +Residual risk worth knowing about: a rebinding attacker can still cause a +single request to be *sent* to an internal address; the peer check runs before +the body is read, so nothing comes back to them. Fully closing that needs +connect-time DNS pinning, which requests does not expose without reaching into +urllib3. + +Response bodies are read through `_read_capped` and parsed only after any DTD +is refused — the remote server is attacker-chosen, so an unbounded read or a +billion-laughs document would otherwise be a cheap way to OOM the container. +""" + +import ipaddress +import socket +import xml.etree.ElementTree as ET +from urllib.parse import quote, unquote, urljoin, urlsplit + +import requests +from django.conf import settings + +DAV_NS = '{DAV:}' + +BOOK_EXTENSIONS = ('.epub', '.pdf') + +# A directory listing is XML; anything this large is not a real one. +MAX_LISTING_BYTES = 8 * 1024 * 1024 + + +class WebDAVError(Exception): + """Failure with a message that is safe to show the user directly.""" + + +class WebDAVInputError(WebDAVError): + """The user's own URL/path/credentials are at fault, not the remote server. + + Separated so views can answer 400 instead of 502 — a rejected traversal + attempt is not an upstream outage. + """ + + +def _timeout(): + return getattr(settings, 'WEBDAV_TIMEOUT', 15) + + +def _allow_private(): + return getattr(settings, 'WEBDAV_ALLOW_PRIVATE_HOSTS', False) + + +# --------------------------------------------------------------------------- +# SSRF guard +# --------------------------------------------------------------------------- + +def _unreachable(host): + """One message for every "we won't talk to this" case. + + Deliberately does not say whether the name failed to resolve or resolved to + a private address, and never echoes the resolved IP: telling those apart + would turn this endpoint into a scanner for the Docker network's service + names and addresses. + """ + return WebDAVInputError( + f'„{host}“ ist nicht öffentlich erreichbar. Aus Sicherheitsgründen sind nur ' + 'öffentlich auflösbare Server erlaubt.' + ) + + +def _is_public(raw_address): + """True only for addresses we are willing to open a connection to.""" + try: + # Scope IDs ('fe80::1%eth0') are not part of the address itself. + ip = ipaddress.ip_address(raw_address.split('%')[0]) + except ValueError: + return False + if not ip.is_global: + return False + # is_global misses two IPv6 transition ranges that can carry an embedded + # private IPv4 address: the well-known NAT64 prefix (RFC 6052) and the + # deprecated IPv4-compatible range. + if ip.version == 6: + for unsafe in ('64:ff9b::/96', '::/96'): + if ip in ipaddress.ip_network(unsafe): + return False + return True + + +def assert_safe_url(url): + """Reject URLs that don't resolve to a public address. + + Self-hosted setups that legitimately want a LAN target can opt out with + WEBDAV_ALLOW_PRIVATE_HOSTS=True, which is why this is a setting and not a + hard rule. + """ + parts = urlsplit(url) + if parts.scheme not in ('http', 'https'): + raise WebDAVInputError('Nur http:// und https:// werden unterstützt.') + + host = parts.hostname + if not host: + raise WebDAVInputError('Die URL enthält keinen Hostnamen.') + + try: + port = parts.port or (443 if parts.scheme == 'https' else 80) + except ValueError: + # urlsplit validates the port lazily, on attribute access. + raise WebDAVInputError('Die URL enthält einen ungültigen Port.') + + if _allow_private(): + return + + try: + infos = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP) + except socket.gaierror: + raise _unreachable(host) + + for info in infos: + if not _is_public(info[4][0]): + raise _unreachable(host) + + +def _assert_peer_is_safe(response): + """Re-check the address the socket actually connected to. + + `assert_safe_url` validates DNS before the request, but requests resolves + the name again when it opens the connection. A resolver answering + public-then-private across those two lookups would otherwise hand us an + internal service's response body. + """ + if _allow_private(): + return + sock = getattr(getattr(response.raw, '_connection', None), 'sock', None) + if sock is None: + return # nothing to introspect (mocked, or already released) + try: + peer = sock.getpeername()[0] + except OSError: + return + if not _is_public(peer): + raise WebDAVInputError('Der Server ist nicht öffentlich erreichbar.') + + +# --------------------------------------------------------------------------- +# Path helpers +# --------------------------------------------------------------------------- + +def safe_rel_path(raw): + """Normalise a client-supplied path and keep it inside the source root. + + Whatever survives here is percent-encoded by `_build_url` with `quote()`'s + default `safe='/'`, and that is what stops double-encoded traversal + ('%252e%252e%252f') and smuggled absolute URLs: '%' and ':' both get + escaped, so they land as literal filename characters. Widening that safe + set would reopen those paths. + """ + raw = (raw or '').strip().strip('/') + if not raw: + return '' + segments = [] + for segment in raw.split('/'): + if not segment or segment == '.': + continue + if segment == '..': + raise WebDAVInputError('Ungültiger Pfad.') + segments.append(segment) + return '/'.join(segments) + + +def _build_url(source, rel_path): + base = source.normalized_base_url() + if not rel_path: + return base + return base + quote(rel_path) + + +def _request(method, url, source, **kwargs): + assert_safe_url(url) + auth = (source.username, source.password) if source.username else None + try: + # Always streamed: it keeps the connection open long enough to inspect + # the peer, and stops requests from buffering an unbounded body before + # we get the chance to cap it. + response = requests.request( + method, url, auth=auth, timeout=_timeout(), + allow_redirects=False, stream=True, **kwargs + ) + except requests.Timeout: + raise WebDAVError('Zeitüberschreitung beim Server.') + except requests.RequestException as exc: + raise WebDAVError(f'Verbindung fehlgeschlagen: {exc.__class__.__name__}') + + try: + _assert_peer_is_safe(response) + + if response.status_code in (301, 302, 303, 307, 308): + raise WebDAVError( + 'Der Server hat eine Weiterleitung geschickt. ' + 'Bitte trage die Ziel-URL direkt ein.' + ) + if response.status_code in (401, 403): + raise WebDAVInputError('Anmeldung abgelehnt — Benutzername oder App-Passwort prüfen.') + if response.status_code == 404: + raise WebDAVError('Pfad auf dem Server nicht gefunden.') + if response.status_code >= 400: + raise WebDAVError(f'Server antwortete mit HTTP {response.status_code}.') + except Exception: + response.close() + raise + + return response + + +def _read_capped(response, max_bytes, oversize_error): + """Read a streamed body, aborting as soon as it exceeds `max_bytes`.""" + declared = response.headers.get('Content-Length') + if declared and declared.isdigit() and int(declared) > max_bytes: + response.close() + raise oversize_error() + + chunks = [] + total = 0 + try: + for chunk in response.iter_content(chunk_size=64 * 1024): + total += len(chunk) + if total > max_bytes: + raise oversize_error() + chunks.append(chunk) + except requests.RequestException: + raise WebDAVError('Übertragung abgebrochen.') + finally: + response.close() + + return b''.join(chunks) + + +# --------------------------------------------------------------------------- +# PROPFIND / GET +# --------------------------------------------------------------------------- + +_PROPFIND_BODY = ( + '' + '' + '' + '' +) + + +def _parse_listing_xml(payload): + """Parse a multistatus body, refusing any document that carries a DTD. + + The remote server is chosen by the user, so a billion-laughs document is + within an attacker's reach, and ElementTree *does* expand internal entities + (verified on 3.12: a 3-level document expands). Installing an expat + EntityDeclHandler is not an option either — `XMLParser.parser` no longer + exists in 3.12, so setting it silently does nothing. + + That leaves rejecting the DTD before parsing. It costs nothing: a real + multistatus never has one, and a filename containing this text would arrive + escaped as `<!DOCTYPE`, so these bytes can only ever be markup. + """ + lowered = payload.lower() + if b' block that actually applies. + + RFC 4918 lets a server split its answer across several blocks — + typically a 200 for the properties it found and a 404 for the ones it did + not, in no guaranteed order. Taking the first one blindly mislabels + directories (whose getcontentlength is the missing property) as files, and + the client then filters them out of the listing entirely. + """ + fallback = None + for propstat in node.findall(f'{DAV_NS}propstat'): + prop = propstat.find(f'{DAV_NS}prop') + if prop is None: + continue + if ' 200 ' in (propstat.findtext(f'{DAV_NS}status') or ''): + return prop + if fallback is None: + fallback = prop + return fallback + + +def list_directory(source, rel_path=''): + """PROPFIND Depth 1, returned as entries relative to the source root.""" + rel_path = safe_rel_path(rel_path) + url = _build_url(source, rel_path) + if not url.endswith('/'): + url += '/' + + response = _request( + 'PROPFIND', url, source, + headers={'Depth': '1', 'Content-Type': 'application/xml; charset=utf-8'}, + data=_PROPFIND_BODY.encode('utf-8'), + ) + payload = _read_capped( + response, MAX_LISTING_BYTES, + lambda: WebDAVError('Verzeichnisliste ist unerwartet groß.'), + ) + + root = _parse_listing_xml(payload) + base_path = unquote(urlsplit(url).path) + entries = [] + + for node in root.findall(f'{DAV_NS}response'): + href = node.findtext(f'{DAV_NS}href') or '' + # hrefs come as absolute URLs, absolute paths, or relative references. + href_path = unquote(urlsplit(urljoin(url, href)).path) + if not href_path.startswith(base_path): + continue + remainder = href_path[len(base_path):].strip('/') + if not remainder: + continue # the collection we asked about + if '/' in remainder: + continue # Depth 1 should not return these, but be strict anyway + + prop = _select_prop(node) + is_dir = ( + prop is not None + and prop.find(f'{DAV_NS}resourcetype/{DAV_NS}collection') is not None + ) + size_text = prop.findtext(f'{DAV_NS}getcontentlength') if prop is not None else None + try: + size = int(size_text) if size_text else 0 + except ValueError: + size = 0 + + entries.append({ + 'name': remainder, + 'path': f'{rel_path}/{remainder}' if rel_path else remainder, + 'is_dir': is_dir, + 'size': size, + 'modified': (prop.findtext(f'{DAV_NS}getlastmodified') or '') if prop is not None else '', + 'is_book': (not is_dir) and remainder.lower().endswith(BOOK_EXTENSIONS), + }) + + entries.sort(key=lambda e: (not e['is_dir'], e['name'].lower())) + return entries + + +def fetch_file(source, rel_path, max_bytes): + """Download one file, refusing anything larger than `max_bytes`. + + Reads into memory rather than to disk: the caller hands the bytes straight + back to the browser, which encrypts them. Nothing readable is persisted + server-side. + """ + rel_path = safe_rel_path(rel_path) + if not rel_path: + raise WebDAVInputError('Kein Pfad angegeben.') + if not rel_path.lower().endswith(BOOK_EXTENSIONS): + raise WebDAVInputError('Nur .epub- und .pdf-Dateien können importiert werden.') + + response = _request('GET', _build_url(source, rel_path), source) + return _read_capped( + response, max_bytes, + lambda: WebDAVInputError(f'Datei ist zu groß (max. {max_bytes // 1024 // 1024} MB).'), + ) diff --git a/books/urls.py b/books/urls.py index e17e99f..a9d738b 100644 --- a/books/urls.py +++ b/books/urls.py @@ -1,9 +1,12 @@ from django.urls import path -from . import views +from . import views, webdav urlpatterns = [ path('', views.book_list, name='book_list'), path('upload/', views.upload_book, name='upload_book'), + path('cloud/sources/', webdav.cloud_sources, name='cloud_sources'), + path('cloud//browse/', webdav.cloud_browse, name='cloud_browse'), + path('cloud//fetch/', webdav.cloud_fetch, name='cloud_fetch'), path('metadata-lookup/', views.lookup_book_metadata, name='lookup_book_metadata'), path('/data/', views.get_book_data, name='get_book_data'), path('/delete/', views.delete_book, name='delete_book'), diff --git a/books/webdav.py b/books/webdav.py new file mode 100644 index 0000000..11ed64f --- /dev/null +++ b/books/webdav.py @@ -0,0 +1,89 @@ +"""Cloud import endpoints for the ebook library. + +These let the browser browse a user-configured WebDAV server and pull one book +out of it. The fetch is proxied through the server rather than done in the +browser because the WebDAV host is a different origin and will not send CORS +headers for it. + +That proxy is a deliberate, narrow relaxation of the same rule the ISBN lookup +in books/views.py bends: the server sees the file's bytes in memory for the +duration of one request, but never persists them. The book is encrypted in the +browser and only then POSTed to /books/upload/ as ciphertext, exactly like a +local upload — nothing readable is ever stored server-side. +""" + +from django.conf import settings +from django.http import HttpResponse, JsonResponse +from django.utils import timezone +from django.views.decorators.http import require_http_methods + +from accounts.models import WebDAVSource +from accounts.webdav import WebDAVError, WebDAVInputError, list_directory, fetch_file +from .views import _require_auth + + +def _get_source(request, pk): + return WebDAVSource.objects.filter(pk=pk, user=request.user).first() + + +@require_http_methods(['GET']) +def cloud_sources(request): + unauthorized = _require_auth(request) + if unauthorized: + return unauthorized + + sources = request.user.webdav_sources.all() + return JsonResponse({'sources': [ + {'id': s.id, 'label': s.label, 'host': s.base_url} + for s in sources + ]}) + + +@require_http_methods(['GET']) +def cloud_browse(request, pk): + unauthorized = _require_auth(request) + if unauthorized: + return unauthorized + + source = _get_source(request, pk) + if not source: + return JsonResponse({'error': 'unknown source'}, status=404) + + path = request.GET.get('path', '') + try: + entries = list_directory(source, path) + except WebDAVInputError as exc: + return JsonResponse({'error': str(exc)}, status=400) + except WebDAVError as exc: + return JsonResponse({'error': str(exc)}, status=502) + + source.last_used_at = timezone.now() + source.save(update_fields=['last_used_at']) + + return JsonResponse({'ok': True, 'path': path, 'entries': entries}) + + +@require_http_methods(['GET']) +def cloud_fetch(request, pk): + unauthorized = _require_auth(request) + if unauthorized: + return unauthorized + + source = _get_source(request, pk) + if not source: + return JsonResponse({'error': 'unknown source'}, status=404) + + path = request.GET.get('path', '') + try: + payload = fetch_file(source, path, settings.EBOOK_MAX_BYTES) + except WebDAVInputError as exc: + return JsonResponse({'error': str(exc)}, status=400) + except WebDAVError as exc: + return JsonResponse({'error': str(exc)}, status=502) + + response = HttpResponse(payload, content_type='application/octet-stream') + # These are the book's plaintext bytes, on their way to be encrypted in the + # browser. Keeping them out of the HTTP disk cache (and any reverse proxy + # in front) is what makes "nothing readable is stored" true end to end. + response['Cache-Control'] = 'no-store' + return response diff --git a/diora/settings.py b/diora/settings.py index 53f6905..b8c8b21 100644 --- a/diora/settings.py +++ b/diora/settings.py @@ -109,6 +109,8 @@ BOOKMARKS_MAX_BYTES = 100 * 1024 # 100 KB VOLUME_DEFAULT = 204 # out of 255 ITUNES_TIMEOUT = 6 # seconds BOOK_METADATA_TIMEOUT = 6 # seconds (DNB / Open Library shelf lookup) +WEBDAV_TIMEOUT = 15 # seconds (cloud import browse/fetch) +WEBDAV_MAX_SOURCES_PER_USER = 10 PODCAST_INBOX_PAGE_SIZE = 200 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' @@ -125,4 +127,10 @@ LASTFM_API_SECRET = os.environ.get('LASTFM_API_SECRET', '') AMAZON_AFFILIATE_TAG = os.environ.get('AMAZON_AFFILIATE_TAG', 'diora-20') AMAZON_AFFILIATE_ENABLED = os.environ.get('AMAZON_AFFILIATE_ENABLED', 'True') == 'True' +# WebDAV cloud import (ebooks). Users supply the target URL, so by default the +# server refuses to talk to non-public addresses — otherwise any registered +# account could probe the internal network through it. Set to True only when +# every account on this instance is trusted and the WebDAV server is on the LAN. +WEBDAV_ALLOW_PRIVATE_HOSTS = os.environ.get('WEBDAV_ALLOW_PRIVATE_HOSTS', 'False') == 'True' + BUILD_TIME = os.environ.get('BUILD_TIME', '') diff --git a/static/css/app.css b/static/css/app.css index af1ecf6..b451796 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -710,6 +710,161 @@ a:hover { padding: 1.5rem 0; } +/* --- WebDAV cloud connections (settings) --- */ + +.webdav-list { + list-style: none; + margin: 0 0 1rem; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.webdav-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + flex-wrap: wrap; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.6rem 0.75rem; +} + +.webdav-item-info { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; + word-break: break-all; +} + +.webdav-item-actions { + display: flex; + gap: 0.5rem; +} + +.webdav-form { + display: flex; + flex-direction: column; + gap: 0.6rem; + border-top: 1px solid var(--border); + padding-top: 1rem; +} + +.webdav-field { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.85rem; + color: var(--text-muted); +} + +.webdav-field input { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-family: var(--font); + font-size: 0.9rem; + padding: 0.45rem 0.7rem; + outline: none; + width: 100%; +} + +.webdav-field input:focus { + border-color: var(--accent); +} + +.webdav-form .btn { + align-self: flex-start; +} + +/* --- Cloud import browser (books tab) --- */ + +.cloud-browser { + display: flex; + flex-direction: column; + gap: 0.5rem; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 0.75rem; + margin-bottom: 1rem; +} + +.cloud-browser-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + flex-wrap: wrap; +} + +.cloud-browser-head select { + background: var(--bg-alt); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-family: var(--font); + font-size: 0.85rem; + padding: 0.3rem 0.5rem; + outline: none; +} + +.cloud-path { + font-size: 0.8rem; + color: var(--text-muted); + word-break: break-all; +} + +.cloud-entries { + list-style: none; + margin: 0; + padding: 0; + max-height: 320px; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.cloud-entry { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.4rem 0.25rem; + border-bottom: 1px solid var(--border); +} + +.cloud-entry:last-child { + border-bottom: none; +} + +.cloud-entry-name { + background: none; + border: none; + color: inherit; + font-family: var(--font); + font-size: 0.9rem; + text-align: left; + padding: 0; + cursor: pointer; + word-break: break-all; + flex: 1; + min-width: 0; +} + +.cloud-entry-name:hover { + color: var(--accent); +} + +.cloud-entry-size { + font-size: 0.75rem; + color: var(--text-muted); + white-space: nowrap; +} + /* ========================================================= RESPONSIVE ========================================================= */ diff --git a/static/js/app.js b/static/js/app.js index 39f793a..263f25f 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3776,23 +3776,31 @@ async function deriveAndStoreKey() { async function uploadEbook(file) { const statusEl = $('book-upload-status'); - const isPdf = /\.pdf$/i.test(file.name); - const isEpub = /\.epub$/i.test(file.name); - if (!isPdf && !isEpub) { - if (statusEl) statusEl.textContent = 'Only .epub and .pdf files are supported.'; - return; - } if (file.size > DIORA_CONFIG.ebookMaxBytes) { if (statusEl) statusEl.textContent = `File too large (max ${DIORA_CONFIG.ebookMaxBytes / 1024 / 1024} MB).`; - return; + return false; + } + return _importEbookBuffer(await file.arrayBuffer(), file.name, statusEl); +} + +// Shared tail of every import path (local file, cloud): parse metadata, encrypt +// with the user's key and POST ciphertext. Plaintext never leaves the browser. +async function _importEbookBuffer(buf, filename, statusEl) { + const isPdf = /\.pdf$/i.test(filename); + const isEpub = /\.epub$/i.test(filename); + if (!isPdf && !isEpub) { + if (statusEl) statusEl.textContent = 'Only .epub and .pdf files are supported.'; + return false; + } + if (buf.byteLength > DIORA_CONFIG.ebookMaxBytes) { + if (statusEl) statusEl.textContent = `File too large (max ${DIORA_CONFIG.ebookMaxBytes / 1024 / 1024} MB).`; + return false; } if (statusEl) statusEl.textContent = 'Encrypting…'; try { - const buf = await file.arrayBuffer(); - - let title = file.name.replace(/\.(epub|pdf)$/i, ''); + let title = filename.replace(/\.(epub|pdf)$/i, ''); let author = ''; let isbn = ''; const type = isPdf ? 'pdf' : 'epub'; @@ -3821,7 +3829,7 @@ async function uploadEbook(file) { } const key = await getOrCreateEncKey(); - const metaJson = new TextEncoder().encode(JSON.stringify({title, author, filename: file.name, type, isbn, folder: '', shelfTag: ''})); + const metaJson = new TextEncoder().encode(JSON.stringify({title, author, filename, type, isbn, folder: '', shelfTag: ''})); const [metaEnc, dataEnc] = await Promise.all([ encryptBytes(key, metaJson), encryptBytes(key, buf), @@ -3843,11 +3851,165 @@ async function uploadEbook(file) { if (data.ok) { if (statusEl) statusEl.textContent = `✓ "${title}" uploaded`; loadBookList(); - } else { - if (statusEl) statusEl.textContent = 'Error: ' + (data.error || 'upload failed'); + return true; } + if (statusEl) statusEl.textContent = 'Error: ' + (data.error || 'upload failed'); + return false; } catch (e) { if (statusEl) statusEl.textContent = 'Upload failed: ' + e.message; + return false; + } +} + +// --------------------------------------------------------------------------- +// Cloud import — browse a user-configured WebDAV server and pull a book in. +// +// The download is proxied by the server because the remote host is a different +// origin and sends no CORS headers. It still lands here as raw bytes and goes +// through _importEbookBuffer like any local file, so what gets stored is +// ciphertext the server cannot read. +// --------------------------------------------------------------------------- + +let _cloudSources = []; +let _cloudSourceId = null; +let _cloudPath = ''; + +async function toggleCloudImport() { + const panel = $('cloud-browser'); + if (!panel) return; + if (panel.style.display !== 'none') { closeCloudImport(); return; } + + try { + const res = await fetch('/books/cloud/sources/'); + _cloudSources = (await res.json()).sources || []; + } catch (e) { + await customAlert('Cloud-Verbindungen konnten nicht geladen werden: ' + e.message); + return; + } + + if (!_cloudSources.length) { + await customAlert( + 'Noch keine Cloud-Verbindung eingerichtet. Unter Einstellungen → ' + + '„Cloud-Verbindungen für Bücher“ kannst du eine hinzufügen.' + ); + return; + } + + const select = $('cloud-source-select'); + if (select) { + select.innerHTML = ''; + for (const source of _cloudSources) { + const option = document.createElement('option'); + option.value = source.id; + option.textContent = source.label; + select.appendChild(option); + } + select.style.display = _cloudSources.length > 1 ? '' : 'none'; + } + + panel.style.display = ''; + _cloudSourceId = _cloudSources[0].id; + _cloudBrowse(''); +} + +function closeCloudImport() { + const panel = $('cloud-browser'); + if (panel) panel.style.display = 'none'; +} + +function _onCloudSourceChange(value) { + _cloudSourceId = parseInt(value, 10); + _cloudBrowse(''); +} + +function _cloudParentPath(path) { + const parts = path.split('/').filter(Boolean); + parts.pop(); + return parts.join('/'); +} + +function _formatBytes(n) { + if (!n) return ''; + if (n < 1024) return n + ' B'; + if (n < 1024 * 1024) return Math.round(n / 1024) + ' KB'; + return (n / 1024 / 1024).toFixed(1) + ' MB'; +} + +function _cloudEntryRow(label, onClick, sizeText) { + const li = document.createElement('li'); + li.className = 'cloud-entry'; + + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'cloud-entry-name'; + button.textContent = label; + button.addEventListener('click', onClick); + li.appendChild(button); + + if (sizeText) { + const size = document.createElement('span'); + size.className = 'cloud-entry-size'; + size.textContent = sizeText; + li.appendChild(size); + } + return li; +} + +async function _cloudBrowse(path) { + const listEl = $('cloud-entries'); + const pathEl = $('cloud-path'); + const statusEl = $('cloud-status'); + if (!listEl) return; + + if (statusEl) statusEl.textContent = 'Lade…'; + listEl.innerHTML = ''; + + let data; + try { + const res = await fetch(`/books/cloud/${_cloudSourceId}/browse/?path=${encodeURIComponent(path)}`); + data = await res.json(); + if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`); + } catch (e) { + if (statusEl) statusEl.textContent = 'Fehler: ' + e.message; + return; + } + + _cloudPath = data.path || ''; + if (pathEl) pathEl.textContent = '/' + _cloudPath; + if (statusEl) statusEl.textContent = ''; + + if (_cloudPath) { + listEl.appendChild(_cloudEntryRow('⬑ ..', () => _cloudBrowse(_cloudParentPath(_cloudPath)))); + } + + const entries = (data.entries || []).filter(e => e.is_dir || e.is_book); + if (!entries.length && statusEl) { + statusEl.textContent = 'Keine Bücher oder Ordner in diesem Verzeichnis.'; + } + + for (const entry of entries) { + listEl.appendChild(entry.is_dir + ? _cloudEntryRow('📁 ' + entry.name, () => _cloudBrowse(entry.path)) + : _cloudEntryRow('📖 ' + entry.name, () => _cloudImport(entry), _formatBytes(entry.size))); + } +} + +async function _cloudImport(entry) { + const statusEl = $('cloud-status'); + if (statusEl) statusEl.textContent = `Lade „${entry.name}“…`; + + try { + const res = await fetch(`/books/cloud/${_cloudSourceId}/fetch/?path=${encodeURIComponent(entry.path)}`); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.error || `HTTP ${res.status}`); + } + const buf = await res.arrayBuffer(); + if (await _importEbookBuffer(buf, entry.name, statusEl) && statusEl) { + statusEl.textContent = `✓ „${entry.name}“ importiert`; + } + } catch (e) { + if (statusEl) statusEl.textContent = 'Import fehlgeschlagen: ' + e.message; } } diff --git a/static/js/sw.js b/static/js/sw.js index 990752a..fa5dff0 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v40'; +const CACHE = 'diora-v41'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', diff --git a/templates/accounts/settings.html b/templates/accounts/settings.html index 2fffad2..69e373a 100644 --- a/templates/accounts/settings.html +++ b/templates/accounts/settings.html @@ -125,6 +125,75 @@ {% endif %} + + +
+

Cloud-Verbindungen für Bücher

+

+ Verbinde einen WebDAV-Server — Nextcloud, ownCloud, Synology oder was auch immer WebDAV + spricht — und importiere .epub- und .pdf-Dateien direkt daraus in + deine Bibliothek. Die Datei läuft dabei nur durch den Server hindurch; verschlüsselt wird sie + wie immer erst in deinem Browser, gespeichert wird ausschließlich der Geheimtext. +

+

+ Lege dafür bitte ein App-Passwort an (bei Nextcloud unter + Einstellungen → Sicherheit) statt dein Konto-Passwort einzutragen — es wird serverseitig + gespeichert und lässt sich jederzeit einzeln widerrufen. +

+ + {% if webdav_sources %} +
    + {% for source in webdav_sources %} +
  • +
    + {{ source.label }} + {{ source.base_url }}{% if source.root_path %} · /{{ source.root_path }}{% endif %} +
    +
    +
    + {% csrf_token %} + +
    +
    + {% csrf_token %} + +
    +
    +
  • + {% endfor %} +
+ {% endif %} + +
+ {% csrf_token %} + + + + + +

+ Bei Nextcloud/ownCloud genügt die Server-Adresse — der WebDAV-Pfad wird aus dem + Benutzernamen ergänzt. Andere Server brauchen die vollständige WebDAV-URL. +

+ +
+
{% endblock %} diff --git a/templates/radio/player.html b/templates/radio/player.html index 931db3e..5e0248e 100644 --- a/templates/radio/player.html +++ b/templates/radio/player.html @@ -319,6 +319,16 @@
+ +
+ + From 26f983bbaab384ef2864a3c41a132218c51f5056 Mon Sep 17 00:00:00 2001 From: marwin Date: Fri, 4 Sep 2026 12:30:58 +0200 Subject: [PATCH 014/176] =?UTF-8?q?Default-Lautst=C3=A4rke=20auf=2040%=20s?= =?UTF-8?q?enken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Betrifft alle Sender, insbesondere creamfresh radio. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Drq8o4HfFmwkXz2AgvaQ4p --- diora/settings.py | 2 +- templates/radio/player.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/diora/settings.py b/diora/settings.py index c417d52..ffbe165 100644 --- a/diora/settings.py +++ b/diora/settings.py @@ -123,7 +123,7 @@ BG_MAX_BYTES = 5 * 1024 * 1024 # 5 MB HIGHLIGHTS_MAX_BYTES = 700 * 1024 # 700 KB BOOKMARKS_MAX_BYTES = 100 * 1024 # 100 KB -VOLUME_DEFAULT = 204 # out of 255 +VOLUME_DEFAULT = 102 # out of 255 (40%) ITUNES_TIMEOUT = 6 # seconds BOOK_METADATA_TIMEOUT = 6 # seconds (DNB / Open Library shelf lookup) WEBDAV_TIMEOUT = 15 # seconds (cloud import browse/fetch) diff --git a/templates/radio/player.html b/templates/radio/player.html index 9681785..9ac3540 100644 --- a/templates/radio/player.html +++ b/templates/radio/player.html @@ -13,8 +13,8 @@ From 51fb6258b82208f80583ba9b8ad1602fb483130b Mon Sep 17 00:00:00 2001 From: marwin Date: Sat, 12 Sep 2026 15:05:08 +0200 Subject: [PATCH 015/176] Serif-Schriftart im Reader auf Source Serif 4 umstellen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selbst gehostet (Latin + Latin Extended, normal/italic, variable Weight 200-900) statt Google-Fonts-Request, damit der Reader offline über den Service-Worker-App-Shell-Cache weiter funktioniert. SW-Cache auf v44 gebumpt, damit bestehende Clients die neuen Font-Dateien nachladen. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Mxq5zi49Qs9FyQEQAeR8xc --- static/css/app.css | 38 +++++++++++++++++- .../fonts/SourceSerif4-italic-latin-ext.woff2 | Bin 0 -> 110324 bytes static/fonts/SourceSerif4-italic-latin.woff2 | Bin 0 -> 130188 bytes .../fonts/SourceSerif4-normal-latin-ext.woff2 | Bin 0 -> 100872 bytes static/fonts/SourceSerif4-normal-latin.woff2 | Bin 0 -> 122360 bytes static/js/app.js | 2 +- static/js/sw.js | 6 ++- 7 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 static/fonts/SourceSerif4-italic-latin-ext.woff2 create mode 100644 static/fonts/SourceSerif4-italic-latin.woff2 create mode 100644 static/fonts/SourceSerif4-normal-latin-ext.woff2 create mode 100644 static/fonts/SourceSerif4-normal-latin.woff2 diff --git a/static/css/app.css b/static/css/app.css index 94c8723..9418cda 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -2,6 +2,42 @@ diora — dark radio player theme ========================================================= */ +/* Self-hosted (no external request, works offline via the service worker's + app-shell cache) — variable font covering weight 200-900 in one file per + style/subset. Latin + Latin Extended only (covers German/English text). */ +@font-face { + font-family: 'Source Serif 4'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url('/static/fonts/SourceSerif4-normal-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Source Serif 4'; + font-style: normal; + font-weight: 200 900; + font-display: swap; + src: url('/static/fonts/SourceSerif4-normal-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Source Serif 4'; + font-style: italic; + font-weight: 200 900; + font-display: swap; + src: url('/static/fonts/SourceSerif4-italic-latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Source Serif 4'; + font-style: italic; + font-weight: 200 900; + font-display: swap; + src: url('/static/fonts/SourceSerif4-italic-latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + *, *::before, *::after { box-sizing: border-box; margin: 0; @@ -1942,7 +1978,7 @@ body.dnd-mode .timer-display { position: relative; padding: 24px 16px; line-height: 1.8; - font-family: Georgia, 'Times New Roman', serif; + font-family: 'Source Serif 4', Georgia, 'Times New Roman', serif; font-size: 16px; font-weight: 400; } diff --git a/static/fonts/SourceSerif4-italic-latin-ext.woff2 b/static/fonts/SourceSerif4-italic-latin-ext.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..61c767d8b6387a02cea67fb8a0abc94afa3f0c36 GIT binary patch literal 110324 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

9+ss!_F9(o$#$2XAYfBC1<(-C7p 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 #1ub?3vL6;3QgSuU|0a7^0N+L}wq| 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 z z4j(#kD5MXA6sGO3R&#v#&u1f&r3=~JFRQdj#;7p=lH8Y)MVdX9%Q9cBQEcPz7FRFo zGaMA5C3Za7w#{8~&e7t>;1aHmJ-^S*WdHqS%Zl9hj=+lNISuA^8Dn>2C%f`ihf+$* zN6%H{xmtt}65L_`%(y-|T?gkBDO;gK-OH|T#>sBy(739GQm%{wHrq=f1U4^*E0cWV zdFs2JM$wbKWsD}0(QX8FA?j{BqwC80TB>^orj0aMD)`n3MQfS+c;yo6B~#^f@$@56 z=|WFZ2+r>z(f0;xr|-Kvg=%KgV`*_1Y@=HI_-;oDH3I_!0|NsC6N^gtKXApRJ+L*) z-|7gwlV5594<*(oU|VkM4G=5K`V2tnwrra<4X#eU0t^?4i&U;wlgp2_finD~`}yZ& z(JM*oCaU>zy=<$l_^#`I^D}HLAU+6Hz&kr;}cT_BBf~(0{+M17A+?c$w;2 zE?vc+k?~!!UoO0Hd~mnr2`i-fF+pErxsaZXp04{T2+|J3^m;f1_6%!VwIx|Mv%7cF zsMitmpt{J*s!SunDAnYZTzp1vy z-F9vO0{Y(>3$N-p^F{D9oSTi49=Y zZI*t(pMPWRD41;MJPerYXT-A%jpZWh20}$iM1`JYROV79ie`dhj9}QC8iuAh%-*HC z$dbC(W1I+Y*h<))VFu?eT_;|{EQRqK#w{@!A#7?>2D~6SU$!Ql8ryh*!nvgkr^C_| zSbG-IL6PQJMZmh0C@S+LrFft68^iR1b6>MX*d(^?I7N-ZTC`RSmJct`uu7@X8fwYhP^l0B{cENX zDt4T90V{ABqwmYuVexU=PN&OtPi~pK>nG|%(3w&ZH7tBGa}&i7Q?8T^DW|>9g=OId zQ?F|2*7bl1AY^U0F)Z1!5_*3VSpTV=lf!EioRY1j*5H`{R(z#f)}e5du+47t-c|kh zFzol+-RA7Ol<L?jE1XE0xyTZp1_| z&5u)0S6LyU;_{ePkFS?UTD|aRYx1U;R8tlsVSjm>V8P%$R#KO$pK@TkkR?R zs)$Qt+aBhWHtkJH-O;0%D*M_ZdJ@?i6~4yr*Z62%!-tPTg)mUn!N(mqHWsafO%cUt`g(0 zUBteg%qA$ote7w(1S{Aw8toOM8SIhfMXonVfxs!CgDfG=2{I*?-mml>K*LN3_p+td zSgXcXazslUJOZNaI`0(~<=L$j2V>MWn7g7(8?KWx*8?Up=}kF@D?OX_$MLxv{Wi#^5MtBw;+@%>^q`At--5k9jjm!HA=jX)%&O#=uN_ zc1AXJeM^fB$6DFliGs(B#;OC?DPSDxF~Ogrnlk=b z9E2u3QEggXQ?uF~$fa*5VblCHXPytv%S@%}R?O+QmS0=r@>LqSIC4?q8*e=1*?@}5 zuheI9NO(M?FdZhgHOo`R*r_Zfvzour!L@XeUdx*RBdL?ZKJ|P9^!KosU#9fiiAN=4KbFYc|$%7 zSUo6jn^N22b?`lPEq%v)F|*=KGv=r;Ib)3V`K+EcH&=EM@uOQf%^B<~*7i==gO3(g zMP4pUm>LVVshLe39VBcb?uj#D;>Nw1rAIG^?4!=+IM}0gSKi;+hCoBv(M-6qDMfL_ zOy8^C7M~GM0v$(Yx5?f!T-StJQ`H*C%cB0=a(TbGUVQ(ZRe!qxkKhGq2E%w~{5;Yg z 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%0DyHT3b~H5+5Wm zg6qVPC94$l2{%k7Swfp$tcC|A3}tbx4o+*F7c4xF&fD_bop^5I8|N}^y3TaRmTKL2 zHOzrLyEs3m)8w<3RB0&DE9R0lqO^@XnP&V?HbV1JJN+;{Pkb`x`f0})+@NW_|9U^o zy7I<8=~d9+RQpmrt}8$hmO?&7YVs{{p)I(8nG0rmRqVCLxymJ7DM~4Xw zotgy_pyLymUku~#NPwB#)y~ik0b6eVnA?K4slJUIxnH!tv3216Xf8CcRdL9&R@Szn zZ0CV`C9%%~UZi2(=X~g3A++S67%6-uf+?YzPvq{xgQDFVFW0oSTkUa#-n!d7no*Lv zoxHYKuhKPn1Cz8gVK6sdW|OkRVL)sY+Qh&PL&H&`HBOD-4fIl?Pw71{@_B#LzQQZ; zJU=;j^kj-*MqTFaCB)L)p3+R^mhG)0AN<}>{SEhkF2wx;&f)EFrpj7Pf=y%WG3x%ifpV@%JSd#_2^g>3XBQL+@99slGyzC;~oQB!$!)CIM z2Q$kuXEBL^X-L52b(H=igt3xfK=@N?O^XtayiA?BP}3A^b0&d@z`L@jG(o*zY?By_ zc(dj~Cha}~%X+T2)I(tVYin{<*MbF1y(1a=RXVd`vVO!CCu?kOj8mmr);Czpn#UUP zP|L&{&NYWXeN94*siuZ`4VJ%rHH(`qy*}iSHr4y}^)AM$x4xC#w1Ib53JNw3fr<0w|5mMCVE9n6)~^1v4WZpdp?HQvsmCi`kBtAi4a~D&k=GSgB3mP*$d1&r~ zF=wSSxtRz`V5v2uwr4nSl`fq17>Za#r5&b?of3mcZO_05jcfYAn7TsxL>{TxP|;=w zHhC*R#!BDCkvk#18d5epO!t(g^u+$WxD-ix9OU=RqmmP353mxHW_WL$kls0(+^Y{# zfTa0N5kcJZLH(1n1+Df>>rerEQaE^!KQSWYI6bT?It%3hWy@m8)*Z^iRHifv=tIZK zemr^`sqR*(vQma!44l4vi_X=C`-@ic`;-?&c{WxhDT>Xf5Uz=W`E7yXTe_Kfa+?=7 zTg!tg_v(CCM7L3lpo42i)u70;UIHw_Y;%k(+4%+@u@zwe3I&J)x2P%Qs-T%bOav<* zqu4{Ie(fh#>5FnD$$3c=&=9h>V{QL7Qj1WZiNF7PFR3^C_-yX1yoWP^1>J=`7Ja$s z^N$QE<)3Lt@OGjN6e0z<$E%lXDU_6>;d~-D zV|tBq1-LXPHUY?{*oo+zGnMDJF{kIT25?1!E&4rC9p)11p>tFB9I z71#`X6Slt5n9&t$cz?Bm-|9-SH1;zci;hG2&==-F7n!LpP@yMGYPObd%PPjt^^+Lm zG9#_-f!WMw%*zMw1|jAN_m5e%@NQhFQ@c8Nnt5cN`_anPfOuG9?kw{vT)s8ukKvVb zgH&x_*{xi}oKJXx=R`8Qq;ssPH=GgLl!Ge!Lb@B_!MT_6U089!i;Q%1JYOfNKypZl zS_&0CJQOzpn;5Vu3Bt=uwC};?Dt&g94Jx4NXfN*!%NYR&^i(EKI{lvb1zZK^=~e+p z^cO7*LZp!BxD_QM3sx zWSV3MOM|!PDmDi`J7F}DlD$<~dkV~~S`wd?Oj?&AYF>gA+X7KNUP~@~h3;9d?JZ!X zNS~|oB3Z!*E@D9hs9^Ba=ZNEFPRQi33zzK2{{g?Di66Jc1&XBZ3dxiCCR_&#l<5p6f zV*c+pw5y#R+)908TtgMrB1Je-$fBqmKE%jLU^-MlB!z>A5HVpyjpi}h!WqIrhPRhg zSVw%p6~j~?Mx&;pp}uuGJbYB4A%^Pe0&^Z8R6eM9nrWX5FJ9tw^CSFh3`vzPwJXNL z5_xL~<`5hqU$$@Uwu^$>3Ov~NO~V1eKKM=yw=5Xb4WId#Im?@kz1PuL!Y9w?t0KtF zNutg|u*c^C0vi}W0V3cWG~k(tOaT6Re=5_+*f)Il%IGxj~uCjG8jTpd+;EK<}?e39rn^5ktWXK-_~^*+ZYo9hyDa5f37$ z3f4d6QRsp1LJD`0&hO*7UHl8Ct=n|4TcVpW7c~uh9%8^s4gCPp)nH~B=Cs~v3l~dI zg|zil`1FlW9hO)hhGCO>M-mRD35i+ zAqA`a7jb^ufb`510Nz<7ag`&px=MT+xl>HDipz4%mTTIG(EA#N8XA8-x zF|c8>!6aYzy0;vX*k^H%4Pfth(CtY^bk=3swQN|~nWhJwHMpmcDEPNtWoPGxROv-_2M@9(;f>(j8b!jQjEDXss-hAO(4ph_~&S#{i9#`w_*UCaj#*JAenM$c=Oo~dc zXO-+cFVnPNOAng#luq&T)(EDlKV;R(&y%M?I_qHfS!2P>y=kGqtygdVU=1X7H@_;Z zSTStWNGcL};2#!8*qpp!?QR(obA@EqFMKNChIbFb6NxTPcq>oi^UMprsDx1^uajbW_tuSjZ9F2?Oe#Dhvi(;EG z(d|*LijwuQKOKcoX!D@hX7fo56eu7Z;yQm=kEWFqXv%CJ*x5Y@wKm{=a=M~q5AvmC zRf&06luUH$A>Yt)TY3s<=K!A6GCS)iZy#%=SXsdEaf(6;HYKOwj7H)Hk}!LGH-JP} z)g&P+350mlajC>{N?VOA5+*UfkR`{%&{=YP*;_@{fEoXba)4|vC`G)qjD`pR!gUD? zlF-{BLs1AOfnDY)L5aclAHC4%G2S=23pKjQ$Pz|p&X|o3#sjNMroiU%jxU*yhA#(9mf2!J8ScN3T0=iy=D<7bEw&n7`KmlTn-%P3h zcI@9K%P0E}Qn3sI;$`4`C@v#Uw-?gTQDnukmFV3;OlDeLHKN4eI>aT%NznVJ8?KOa%XHXcg^D6<|GXT3-<{^ zZnC_H-+*8FokK@Fm1ve(0sR>{)khB*{YKT%)`FVWs4**Qk6-~b+Je%~Ie#BJg!Ny^ zaQyNbsC};R_p^tP#Q|f2dGJ6NloDqt8aUk4ToK6T`++(yELPI9;y9xErN3yX7usmSB6@Hxg zD7r&G(Z%&S)RJSOWEU$XVwpXoJ9Wtj%3Z$b*8&cI0f*MsEnjK2X)y+U<29@ za$N?xK7Z_#VN<@h$sm;~%Q;3Sv_Ux4p~8B!O=LsX36p>X~k1TwuuzNqoa$=gHj*#kOr z*O5FAIq(Q;!d$xJva(=rgA;;$_(CwnrAWmwNMF4{1G+v*lHEv2e<-Z`4Mdvc1a8*^ zqUZ2+z7mMGCiGchEc4q}f~txA%>+tSUH_8QKJ>q?C@oGPVm1Y2+jBzlY*W zROepFM6De2r@hmiV2Sse6PV6mj2cqM@~F5-N{^u$W7eD~uCyjLU=qMJHKL$-3G6|T zhau}gjW@H{)zm!$YVYI+(fidr*q-QRghr5;LEqev?+>@9Sdsr#UPWJ0M^KL87!)Md zmi_$);-f6uKP9ULX^+mC01AWrT3L#IeAIMC!1D2)z_fRetd9Q=#2?)V)8r@Y7 zpYUL?8e2zxp9m+ zZD~fP$T{M&Jj7kSqzY$>tN20XYxDjZa5PJYydu-TO-5|mSwJalL7Y(*)6fHe?X}`p zZgIl_REusf9ARiI`m0bgd%#4$!T`+N7lgpbb|=@PWrF((Ix6xqQy z8V~q-FJ1PvgX`QY;Ycn7z46-z;GDp24QLOA2EIP{N*OC*EXqJ67FFOeN9pWIQdqG3 zkFNNN!&=lsb2DODh$V%t8zlxfYC6dpJFYy@ZvpBlr8;YPvixFHMH#xnYG?7Z9q;YI4sWPMo zdG72KJ_cu|>99r-*$HS8UgD3>3RH1&7UL#84j2Cr0=AC5!v%YzIsE3*Tvu*~9APhN zYoHd8sRCfhlGJv=4#BTu3C~q%&9!)65V9M1cTD=XgH9Obe#pJp&>(G%;V4$ zZZQ6#sj;*`ZvJB`xI(j{32 zu6S=VP$*LkJDs1f$VpV9>BdiBw8P$4OEc_sTL`NIgsL=n@$<9@RuiiILw8!mzqeOH7@i;`=CKVRQv{V`&tNj4Z)5feDx+F$|YDu3XxY*!r*2F^KNn zp4(GjgvH*|i0&DzT!s~F)bad`3O1>Zn9lk_xj-WDC6gw^2hH!T;xSJnbN$kNT z5?U|zVA$5t_erYwf(24-*^6v~pvaBE=>|66N+3vtK9Ts6L@HR=(GPZvf^+6W;b9a^ zVBnb&@eFXdOlMS%SlKXZ*_C_IMPonr7IrkBOdLyC6CKc?uGBgV2AJRnGs{UlMJYCB zV?ej@4SqOyUmd}@)yP$#gDjcy<;jr4{eZO9+{#txs+*`sE=lqe?&o5~LC6dOdwX(n zA<|u{l4ObjQ$kLUQLGaHrR==qR0M#)dN6Mt$P$2q;0f_MnH2%fdEU`zBdpihaL~dP z0O1~9XM)4A;04uBVVqJ@y}}@X9Y2HhRfvZTj)Q0}G#(J2MIrG+i@_=uMjWzu91_Zs zL`fLQuu^bJ#U>41dYP9YlOVe+$W;gnLll)=Ix0n!g+=*XJ)kf6F$T#g<=74Go8@Rf#Qzl+a&5rLICS5WnidV{13D>&AL>-^3qVQWn0IwC*A{@T}k?|LfHd1 zxD<0Rwq0h#%E-t9ge!Op;&de#T+q6#|8csM7IwPOlbOsJ=GC#&livn)^D2No$kD3K zm+$V$%hzDW7==CI9N>U%fhZSzBFO~{G{#-Hw33b*wnYel&f=kC+sFzo zOn^Z7?gd>axa>I8)^{5iSlGi<#|mliL8CUl}vuT3Q3g z0jI09vz!A#<7QleAvkI#=K%k|xtyMn!{jTn5;ZPd-ijs)lT~YT_{oY*fN=t^XF$!s z$$`S2`(7Gq4n3%B-8fn%++Vn?wB1FywQ#zev&GhXMZ$V!_GZ%?M9!eODn6;3w3s8o zcN8MF-sft6Y}M+hIcj29fFhU&gVc(*3upwdC`PuW7+-fxzuNwK7P`6LCKH(HUs^dj zQ~qXYsV9bD8jq+CD*&H3}FtS`Rh*$us zq82s3Psq4D%EsQYcFY^&M&YO)dBZrQgF2x6tG(*eUhVO2?~2Z7Nt;{NV2zcQS}40m zk5WUQ(c9UQ#hH>gl_)|8nUfetICuaT{NO{cIb@UOZ?`_aWIB>ki3Guy-fhRk+&nAJ zWS8F5zx&@A<+n$9{Gs^yt(Ma!b-*3pxRuNG$Xh^Kr>Ye#*7VibjU4GzjKLq757D!B zVZ0@6IH$+K^p8^SWjt|sSU||qm%s;dZuzZwmh{KHycxB@l~m!T(D;x@lmD_cMxTA{Gj6nQq%O*bHYtVv5L zX~3*|8ljSVNd)Us6-VLYo!5dk1@FpZx5}(;EJ~W5h#acq@qRLgvxftyW zS~6Rdm3?s&qOiyZAs5}%HVjN3NjbDR%d`IK&SZPe-`ROO4+a2#{yM4Ck4^!A!G9&_ z{n$9^fcdr6-@>R{;)^tXVtEmv?h+$B(?gyV#X>hos z_K(&1|1k_o-f2&c@RvaJ60gObzutVQSVD_=kuUTGF8mpKGs?&HadPY&E61!cY7Epr zQZYiqJmiBu;QXh(X?d^ra5r~Jr*v5B8?C+SvWqUD52?uQlx1($W=2ZVoU#OCj5O3h z@yjdsowwU+bBrP2YpbE;){Z3rYo&QVB_6b&^A6qN#;vO-IJa}L%%k47+8vTHsy|1O z{STdn8*K{%ayz_Wr|#(Bm$TAnnmFho<6*%xx7#gpYYrB-A_6Z_;HeQ4;fK|dvd~~z zgoivvNter)?ZdB7+y19+jt5oghj$@XgoZBYwR z7*g5|K+l~-U2CpmFRt0~O4gFG{;|~tD~nv-?s$!B?^W%NNw%kVQB}?5y3i;lbk4QC zMeg-*(yHUSxMPY=R`wBIQL{b_9vx&sbh|Rleww2$1DtSA%O%g>g_fqCRaf*5GDkYp z0+~-kP+R=iA7E8$QcWM|OgpJSK}xu7@g`o%SkE-s%VBngwYlcTMqysga>I8Sr^}4~ zlR{l(_$!HA;fM6p7^CB%rPYEhL2tQ6QRkkfJ_w>Fk`e*dy735rHCJG2bdBJa3pcJP zB=9I=9^U>dlM_fVA%qhCzZXdhSO%C-cF&`LzTFYDUNI9MUB*3l+(bNg=hM>!NQlnMu& z9i_q3@kC&+s|YFRW}To!k!;DLMd_-&mcj(^ z=E}h&3?CCQTui{w8IR#?9EOvz7>>qZ*c**uYZd|ae9T19#f+fN^q{k8K_^p#j-~|d zO^()eD2sop;r0x5RylumVZzu+AC{dfG43oFyw+O!Y)cP;@)H4)t z=z;t;jOikV=*NNaQy`YEsY;r>bjuTNR>+t?Q4?~DEQ+Y2i7uHj#S&W_am5o~0y+T= zebWkRgsecpk5#K>)2QcHfnf-SHNciFtCu0d`r<}xIC4Cljte_M`qDa_A(qO``P4<%Y zVm(Yjepf=q6=<-ZPwrAfyQ|=YUx*n6`sl8s)*=MabH@zl*UuRDf9j_d%ED*#iG_4u zsNoaO&=uMPd!3*Dts5dZFH)C^Jk9;w%;lWT$sEq!Y|qB5&ay1XtW3$clw@cIr7^Xs zNHlo~#vN<2lM#7@VTT%gkfb2SAK!fNN{#0pyX(4(${cslZd)-F2oes6)Rhc9^k$}? z7}nw>v6mu)POd>b-q@Y!i{Z)H#XSbMgXzURncU&F+-#DId%VdVZp%$ik{^vP8juo; z^q778`0?Xk_Z6RdKGcDtK@>h1wuf8 zgMop8aq5JRF#2S{d%VS6-k7~L_(Oq@9LW+0M4#Z|P_%M7A^-pq|Jqi38c*S$USjdx zK%0@#pn{VOSFzWp4WOZ*SCi(K9!OsFceQlKn^;LI8G0K9B)_6#HK3;Al~C}oV{sIr zAkki%vl_>Ssm zMmD06Y9aA-oNUMJn3H-I38{-JHQuXcXFH6O9oTZ?4QcVMcqn9gzl*f0L0yX=)7|6B3X7F*uy!!Inj zx4^!^0iw7zytF%!4f|G0N3XU~YuS)h^_pg_Ijq`re8H0WLsyO)g@0gd6b$|s@!~dk zr~QUKHuner=ft@`Y`lHLe!AWG^W@Fs`WH;~zrq&`n*Z`f@YU_?=;j}OZvB3=TKnhy zy=Hyw8ynw#bc_7PW+C7DD|Ps9U#n>R<+uFs{rK#C^p#}sd$+t#Lbu=Cc_lXD=$j<| z^R06~g8goA>vuM(Jk$RYy+6HNy;A?Lhh}=c{=jGV;PLfS7M}jw%Bgz#=5<@AMXcS+ z*MEFtWnX>p;E#(t$$$4hAO7TU`wjc}@7I19?9rc#o534rHCc3P`YUTcy?y%9M;D6p z|Lwef|H0Y3c)`?XA6*Ns%`Z}W=Qq}OPnY8=d%0Dr_WV0Pvy(ZtIRE}XJ^K9i@GU)f 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⪼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 -
focus 25:00 @@ -228,82 +211,8 @@
{% endblock %} diff --git a/templates/accounts/register.html b/templates/accounts/register.html index c72c298..a81773e 100644 --- a/templates/accounts/register.html +++ b/templates/accounts/register.html @@ -1,9 +1,9 @@ {% extends "base.html" %} -{% block title %}Register — diora{% endblock %} +{% block title %}Registrieren — diora{% endblock %} {% block content %}
-

Create Account

+

Konto anlegen

{% csrf_token %} {% for field in form %} @@ -29,10 +29,10 @@ {% endfor %}
{% endif %} - +

- Already have an account? Login + Schon ein Konto? Anmelden

{% endblock %} diff --git a/templates/accounts/settings.html b/templates/accounts/settings.html index 69e373a..702cbcf 100644 --- a/templates/accounts/settings.html +++ b/templates/accounts/settings.html @@ -1,9 +1,9 @@ {% extends "base.html" %} -{% block title %}Settings — diora{% endblock %} +{% block title %}Einstellungen — diora{% endblock %} {% block content %}
-

Settings

+

Einstellungen

{% if lastfm_error %}
{{ lastfm_error }}
@@ -15,7 +15,7 @@ {% if has_lastfm %}

- Connected as {{ profile.lastfm_username }} + Verbunden als {{ profile.lastfm_username }}

{% csrf_token %} @@ -23,46 +23,46 @@ - Scrobble tracks to Last.fm + Titel an Last.fm scrobbeln
{% csrf_token %} - +
{% else %}

- Connect your Last.fm account to automatically scrobble the tracks you listen to. + Verbinde dein Last.fm-Konto, damit gehörte Titel automatisch gescrobbelt werden.

- Connect Last.fm + Last.fm verbinden {% endif %}
-

Background

+

Hintergrund

{% if request.user.profile.background_encrypted %} -

Encrypted background is set. (Preview not available — decrypted in browser on main page.)

+

Ein verschlüsselter Hintergrund ist gesetzt. (Keine Vorschau – er wird erst auf der Hauptseite im Browser entschlüsselt.)

{% csrf_token %} - +
-

Upload a new image to replace it:

+

Neues Bild hochladen, um ihn zu ersetzen:

{% elif request.user.profile.background_image_data %}

- Your background + Dein Hintergrund

{% csrf_token %} - +
-

Upload a new image to replace it (will be stored encrypted):

+

Neues Bild hochladen, um ihn zu ersetzen (wird verschlüsselt gespeichert):

{% else %} -

Upload a custom background image (JPG, PNG or WebP, max 5 MB). Stored end-to-end encrypted.

+

Eigenes Hintergrundbild hochladen (JPG, PNG oder WebP, höchstens 5 MB). Es wird Ende-zu-Ende-verschlüsselt gespeichert.

{% endif %}
- +
@@ -70,11 +70,11 @@
-

Account

-

Logged in as {{ request.user.username }}

+

Konto

+

Angemeldet als {{ request.user.username }}

- Change password + Passwort ändern
{% csrf_token %} {% for field in password_form %} @@ -90,13 +90,13 @@
{{ password_form.non_field_errors|join:", " }}
{% endif %}
-
+
{% csrf_token %} - +
@@ -249,20 +249,20 @@ async function uploadBackground(input) { const status = document.getElementById('bg-upload-status'); const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { - status.textContent = 'Only JPEG, PNG, or WebP images are allowed.'; + status.textContent = 'Nur JPEG-, PNG- oder WebP-Bilder sind erlaubt.'; return; } if (file.size > 5 * 1024 * 1024) { - status.textContent = 'Image must be 5 MB or smaller.'; + status.textContent = 'Das Bild darf höchstens 5 MB groß sein.'; return; } - status.textContent = 'Encrypting…'; + status.textContent = 'Verschlüssele …'; try { const key = await _getOrCreateEncKey(); const buf = await file.arrayBuffer(); const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt({name: 'AES-GCM', iv}, key, buf); - status.textContent = 'Uploading…'; + status.textContent = 'Lade hoch …'; const res = await fetch('/accounts/background/upload/', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, @@ -270,9 +270,10 @@ async function uploadBackground(input) { }); const data = await res.json(); if (data.ok) { location.reload(); } - else { status.textContent = data.error || 'Upload failed'; } + else { status.textContent = data.error === 'only jpeg, png, or webp allowed' ? 'Nur JPEG, PNG oder WebP erlaubt.' + : data.error === 'file too large (max 5 MB)' ? 'Datei zu groß (höchstens 5 MB).' : (data.error || 'Hochladen fehlgeschlagen'); } } catch (e) { - status.textContent = 'Upload failed: ' + e.message; + status.textContent = 'Hochladen fehlgeschlagen: ' + e.message; } input.value = ''; } diff --git a/templates/base.html b/templates/base.html index 509e3c3..64f4baa 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,5 +1,5 @@ - + @@ -7,7 +7,7 @@ - + {% if user.is_authenticated %}{% endif %} @@ -22,17 +22,17 @@ diff --git a/templates/radio/player.html b/templates/radio/player.html index e80683d..9d57b91 100644 --- a/templates/radio/player.html +++ b/templates/radio/player.html @@ -1,46 +1,46 @@ {% extends "base.html" %} -{% block title %}diora — radio player{% endblock %} +{% block title %}diora — Radio{% endblock %} {% block content %}
- — no station — + — kein Sender —
- + - + - +
- focus + Fokus 25:00 - - + + - +
@@ -48,24 +48,24 @@
- + - +
- - - + + +
@@ -215,8 +215,8 @@

Podcasts werden geladen …

{% else %}

- Log in or register - to subscribe to podcasts. + Anmelden oder registrieren, + um Podcasts zu abonnieren.

{% endif %}
@@ -226,7 +226,7 @@ {% if user.is_authenticated %}
- Drop .epub or .pdf here or + .epub oder .pdf hierher ziehen oder
@@ -248,8 +248,8 @@
{% else %}

- Log in or register - to use the encrypted ebook reader. + Anmelden oder registrieren, + um den verschlüsselten E-Book-Reader zu nutzen.

{% endif %} @@ -263,30 +263,30 @@ - - + + - - - - + + + + - +
diff --git a/templates/radio/stream_player.html b/templates/radio/stream_player.html index 97b1f04..a3e7177 100644 --- a/templates/radio/stream_player.html +++ b/templates/radio/stream_player.html @@ -1,5 +1,5 @@ - + @@ -80,15 +80,15 @@
- +
- vol + Lautst.
- + From 64e9d706477cc042e9d3cfc8ba8b7f1c1d0ede13 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sat, 3 Oct 2026 14:40:39 +0200 Subject: [PATCH 065/176] =?UTF-8?q?Podcasts:=20mehrere=20Folgen=20nacheina?= =?UTF-8?q?nder=20wegwischen,=20w=C3=A4hrend=20=E2=80=9ER=C3=BCckg=C3=A4ng?= =?UTF-8?q?ig=E2=80=9C=20steht;=20sw=20v72?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- static/js/sw.js | 2 +- static/podcasts/native-podcast.js | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/static/js/sw.js b/static/js/sw.js index bb22f5b..8c93abc 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v71'; +const CACHE = 'diora-v72'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', diff --git a/static/podcasts/native-podcast.js b/static/podcasts/native-podcast.js index 27ca360..48023e6 100644 --- a/static/podcasts/native-podcast.js +++ b/static/podcasts/native-podcast.js @@ -1,4 +1,4 @@ -// Copied from diora-android abda34a 2026-10-03: web-native/static/native-podcast.js +// Copied from diora-android 370e653 2026-10-03: web-native/static/native-podcast.js // by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android // and copy again. The browser host is static/js/podcasts-web.js. @@ -71,6 +71,8 @@ let preview = null; // {id, url, data | choices | error} while a feed is looked at before subscribing let previewSeq = 0; let refreshing = false; + let swipingSince = 0; // a row is being swiped (since then): render waits (rowGestures) + let renderAfterSwipe = false; let selected = null; // Set of episode ids while choosing several (queue or inbox) let locked = false; // queue locked: no dragging try { locked = localStorage.getItem('nui_pod_locked') === '1'; } catch (e) {} @@ -982,6 +984,9 @@ } function render() { + // A finger is swiping a row: redrawing now would pull the row away under it (and end + // the swipe). It redraws when the swipe ends — unless that never came (a lost pointer). + if (swipingSince && Date.now() - swipingSince < 8000) { renderAfterSwipe = true; return; } episodeScreenParts(); if (!root) return; const scroll = scrollTop(); @@ -1174,6 +1179,7 @@ function rowGestures(row, onLong, onSwipe, list) { let start = null; + let holding = false; // this row counts in swipingSince let timer = 0; let mode = ''; let dx = 0; @@ -1217,6 +1223,11 @@ delete other.dataset.act; }; const finish = done => { + if (holding) { + holding = false; + swipingSince = 0; + if (renderAfterSwipe) { renderAfterSwipe = false; setTimeout(render, done ? 200 : 220); } + } row.classList.remove('nui-pod-swiping'); const gone = bg; bg = null; @@ -1257,7 +1268,9 @@ if (Math.abs(mx) > 8 || Math.abs(my) > 8) { stopTimer(); start.moved = true; } if (onSwipe && start.touch && Math.abs(mx) > 12 && Math.abs(mx) > Math.abs(my) * 1.5) { mode = 'swipe'; - commitSwipe(); + // The swipe before stays undoable (its bar too) until this one is done (offerUndo). + holding = true; + swipingSince = Date.now(); try { row.setPointerCapture(ev.pointerId); } catch (e) {} bg = el('div', 'nui-pod-swipebg'); bg.append(el('span', 'nui-pod-swipe-l'), el('span', 'nui-pod-swipe-r')); From b7a81f3e26c83282220ce41f6a609015e58c4fdc Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sat, 3 Oct 2026 23:08:57 +0200 Subject: [PATCH 066/176] =?UTF-8?q?Neuer=20Tab=20=E2=80=9EAmbient=E2=80=9C?= =?UTF-8?q?:=20Drift,=20ein=20generativer=20Ambient-Synthesizer;=20sw=20v7?= =?UTF-8?q?3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - static/ambient/index.html: Drift (aus der Session "Ambient-Synthesizer-Webapp", eine Datei, Web Audio, offline), eingebettet per iframe (?embed=1&bg=#000) - app.js: Tab 'ambient', iframe lädt beim ersten Öffnen und füllt den Platz bis zur Leiste; Radio/Podcast und Ambient wechseln sich ab (postMessage {type:'ambient', cmd|state}) - sw.js: Ambient im Offline-Cache (ignoreSearch für die Shell-Dateien) Co-Authored-By: Claude Opus 5.5 --- static/ambient/index.html | 778 ++++++++++++++++++++++++++++++ static/css/app.css | 9 + static/js/app.js | 51 +- static/js/sw.js | 6 +- static/podcasts/native-podcast.js | 9 +- templates/radio/player.html | 9 + 6 files changed, 858 insertions(+), 4 deletions(-) create mode 100644 static/ambient/index.html diff --git a/static/ambient/index.html b/static/ambient/index.html new file mode 100644 index 0000000..fd541e0 --- /dev/null +++ b/static/ambient/index.html @@ -0,0 +1,778 @@ + + + + + +Drift – Ambient Synth + + + + +
+
+

Drift

+
Generativer Ambient-Synthesizer · alles wird live im Browser erzeugt
+
+ +
+ – + Noch still. +
+
+ +
+ + + + + + +
+ + + + + +
+
+ + + + diff --git a/static/css/app.css b/static/css/app.css index 050ed16..e6d8b7b 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -389,6 +389,15 @@ a:hover { min-height: 200px; } +/* Ambient: the synthesizer's frame fills the room down to the player bar (app.js _ambientSize). */ +.ambient-frame { + display: block; + width: 100%; + height: 70vh; + border: 0; + background: #000; +} + .sub-tabs { border-bottom-color: transparent; margin-bottom: 1rem; diff --git a/static/js/app.js b/static/js/app.js index 6016b1b..6e6a0cd 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -158,6 +158,7 @@ function customPrompt(message, defaultValue = '') { function playStation(url, name, stationId) { stopPlayback(false); + if (window.__dioraAmbientStop) window.__dioraAmbientStop(); // A secure page can't load plain-http streams (mixed content). Native shells // that play the stream themselves set window.DIORA_PLAYS_HTTP_STREAMS. @@ -1177,7 +1178,7 @@ async function editNotes(pk, current) { // Tabs // --------------------------------------------------------------------------- -const TOP_TABS = ['radio', 'focus', 'podcasts', 'books']; +const TOP_TABS = ['radio', 'focus', 'podcasts', 'books', 'ambient']; const RADIO_SUB_TABS = ['search', 'saved', 'history']; // --------------------------------------------------------------------------- @@ -1234,8 +1235,56 @@ function showTab(name) { _syncHash(); if (name === 'books') loadBookList(); + if (name === 'ambient') _ambientLoad(); } +// --------------------------------------------------------------------------- +// Ambient: "Drift" (static/ambient/), a generative synthesizer in a frame of its +// own. It and the radio / podcast player take turns — one starting stops the +// other — by postMessage: {type: 'ambient', cmd: 'start'|'stop'} to the frame, +// {type: 'ambient', state: 'playing'|'stopped'} from it. +// --------------------------------------------------------------------------- +let _ambientPlaying = false; + +function _ambientFrame() { + return $('ambient-frame'); +} + +function _ambientLoad() { + const f = _ambientFrame(); + if (!f) return; + if (!f.getAttribute('src')) f.src = f.dataset.src; + requestAnimationFrame(_ambientSize); +} + +// The frame fills the room between the tabs and the player bar at the bottom. +function _ambientSize() { + const f = _ambientFrame(); + if (!f || _currentTopTab !== 'ambient') return; + const root = getComputedStyle(document.documentElement); + const bar = parseFloat(root.getPropertyValue('--nui-dock-h')) || parseFloat(root.getPropertyValue('--dpw-bar-h')) || 0; + const top = f.getBoundingClientRect().top + window.scrollY; + f.style.height = Math.max(320, Math.floor(window.innerHeight - top - bar)) + 'px'; +} +window.addEventListener('resize', () => requestAnimationFrame(_ambientSize)); + +// The radio or a podcast starts: Ambient stops. +window.__dioraAmbientStop = function () { + const f = _ambientFrame(); + if (_ambientPlaying && f && f.contentWindow) f.contentWindow.postMessage({type: 'ambient', cmd: 'stop'}, '*'); +}; + +window.addEventListener('message', e => { + const f = _ambientFrame(); + const d = e.data; + if (!f || e.source !== f.contentWindow || !d || d.type !== 'ambient' || !d.state) return; + _ambientPlaying = d.state === 'playing'; + if (!_ambientPlaying) return; + // Ambient starts: the radio and a podcast here stop. + if (typeof isPlaying !== 'undefined' && isPlaying) stopPlayback(false); + if (window.__dioraPausePlayer) window.__dioraPausePlayer(); +}); + function showRadioTab(name) { RADIO_SUB_TABS.forEach(p => { const panel = $(`tab-${p}`); diff --git a/static/js/sw.js b/static/js/sw.js index 8c93abc..0f6ab15 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v72'; +const CACHE = 'diora-v73'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', @@ -11,6 +11,7 @@ const SHELL = [ '/static/podcasts/podcast-ui.css', '/static/podcasts/podcasts-web.css', '/static/manifest.json', + '/static/ambient/index.html', '/static/fonts/SourceSerif4-normal-latin.woff2', '/static/fonts/SourceSerif4-normal-latin-ext.woff2', '/static/fonts/SourceSerif4-italic-latin.woff2', @@ -77,7 +78,8 @@ self.addEventListener('fetch', function (event) { const isShell = SHELL.some(function (s) { return url.pathname === s; }); if (isShell) { event.respondWith( - caches.match(event.request).then(function (cached) { + // ignoreSearch: the Ambient frame loads with ?embed=1&bg=… + caches.match(event.request, {ignoreSearch: true}).then(function (cached) { return cached || fetch(event.request); }) ); diff --git a/static/podcasts/native-podcast.js b/static/podcasts/native-podcast.js index 48023e6..0ad47fc 100644 --- a/static/podcasts/native-podcast.js +++ b/static/podcasts/native-podcast.js @@ -1,4 +1,4 @@ -// Copied from diora-android 370e653 2026-10-03: web-native/static/native-podcast.js +// Copied from diora-android d323614 2026-10-03: web-native/static/native-podcast.js // by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android // and copy again. The browser host is static/js/podcasts-web.js. @@ -4294,6 +4294,8 @@ window.__dioraPlayer = function (p) { const before = player; player = p || {kind: null}; + // Something starts playing here (an episode, the radio): Ambient stops (app.js). + if (player.playing && !before.playing && !player.remote && window.__dioraAmbientStop) window.__dioraAmbientStop(); renderMini(); renderPlayer(); // The current episode changed: the queue view marks it. @@ -4302,6 +4304,11 @@ if (epScreen && (before.episodeId !== player.episodeId || !!before.playing !== !!player.playing)) episodeScreenParts(); }; + // Ambient starts (app.js): an episode playing here pauses. + window.__dioraPausePlayer = function () { + if (player.kind === 'podcast' && player.playing && !player.remote && N.podcastToggle) N.podcastToggle(); + }; + // The app loads this in ; the browser may load it after the page is parsed. function start(fn) { if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); diff --git a/templates/radio/player.html b/templates/radio/player.html index 9d57b91..78ee240 100644 --- a/templates/radio/player.html +++ b/templates/radio/player.html @@ -51,6 +51,7 @@ + @@ -254,6 +255,14 @@ {% endif %} + +{% comment %}"Drift", a generative ambient synthesizer (static/ambient/), in a frame of its own: its + script and styles stay apart from the page's. Loaded the first time the tab opens.{% endcomment %} + + @@ -261,6 +262,10 @@ {% comment %}Filled by static/js/home.js: the book in reading, the next episode, stations, Ambient.{% endcomment %} + +{% comment %}The feed reader, news and blogs: filled by static/js/feeds.js.{% endcomment %} + + {% comment %}"Drift", a generative ambient synthesizer (static/ambient/), in a frame of its own: its script and styles stay apart from the page's. Loaded the first time the tab opens.{% endcomment %} @@ -345,4 +350,5 @@ + {% endblock %} From 1a17c75c1be8ee05f7776f0d7b43a699068c8509 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sun, 4 Oct 2026 20:22:56 +0200 Subject: [PATCH 085/176] =?UTF-8?q?Home:=20Karte=20=E2=80=9ENachrichten?= =?UTF-8?q?=E2=80=9C=20mit=20den=20Top-Themen=20seit=20dem=20letzten=20Mal?= =?UTF-8?q?,=20neue=20Meldungen=20und=20Blogartikel;=20sw=20v90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- static/js/home.js | 73 ++++++++++++++++++++++++++++++++++++++++++++--- static/js/sw.js | 2 +- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/static/js/home.js b/static/js/home.js index a689d20..e50cf7f 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -1,11 +1,11 @@ /** * diora Home: what you were in the middle of, one click away — the book you're - * reading, the next episode, your stations, Ambient. + * reading, the next episode, the news, your stations, Ambient. * * Built only from what the page already has: the book list (app.js), the * podcast host (window.DioraPodcastHost in the browser and diora desktop, * window.DioraNative in the Android app), the saved stations in the radio tab - * and Drift (app.js __dioraAmbient*). Renders into #tab-home; the same file runs + * Drift (app.js __dioraAmbient*) and the feed reader (feeds.js __dioraFeeds*). Renders into #tab-home; the same file runs * in the browser, diora desktop and the Android app (copied with static/js/). * Written without ?. and ?? so old WebViews can still parse it. */ @@ -125,6 +125,56 @@ button + ''; } + // ---- Nachrichten -------------------------------------------------------------- + + // The briefing comes from the server, so it's fetched now and then, not every render. + let feeds = null; + let feedsAt = 0; + let feedsBusy = false; + + function refreshFeeds(maxAgeMs) { + if (typeof window.__dioraFeedsSummary !== 'function' || feedsBusy || !online()) return; + if (Date.now() - feedsAt < maxAgeMs) return; + feedsBusy = true; + window.__dioraFeedsSummary().then(r => { feeds = r; }, () => {}).then(() => { + feedsAt = Date.now(); + feedsBusy = false; + render(); + }); + } + + function feedsSection() { + if (!document.getElementById('tab-feeds') || !feeds || !feeds.state) return ''; + const s = feeds.state; + const sources = s.sources || []; + if (!sources.length) return ''; + const hasNews = sources.some(x => x.kind === 'news'); + const top = ((feeds.briefing && feeds.briefing.top) || []).slice(0, 4); + let body = ''; + if (top.length) { + body = '
' + top.map(x => { + const lead = x.lead || {}; + return ''; + }).join('') + '
'; + } else if (hasNews) { + body = '
' + (s.news_new ? 'Keine Top-Themen seit dem letzten Mal.' : 'Nichts Neues seit dem letzten Mal.') + '
'; + } + const chips = (s.news_new ? '' : '') + + (s.blogs_unread ? '' : ''); + if (!body && !chips) return ''; + return section(hasNews ? 'Nachrichten' : 'Blogs', body + (chips ? '
' + chips + '
' : ''), 'feeds'); + } + + function openFeeds(view) { + showTab('feeds'); + const b = view && document.querySelector('#tab-feeds [data-view="' + view + '"]:not(.on)'); + if (b) b.click(); + } + // ---- Radio ------------------------------------------------------------------ function stationRows() { @@ -205,7 +255,8 @@ tab.appendChild(host); } let html = '
diora
'; - for (const part of [booksSection, podcastSection, radioSection, ambientSection]) { + refreshFeeds(3 * 60000); + for (const part of [booksSection, podcastSection, feedsSection, radioSection, ambientSection]) { try { html += part(); } catch (e) { console.warn('home', e); } } if (html !== lastHtml) { @@ -230,6 +281,12 @@ const h = podHost(); if (act === 'tab') showTab(el.dataset.tab); else if (act === 'book') openBook(Number(el.dataset.id)); + else if (act === 'story') { + const id = Number(el.dataset.id); + const story = feeds && feeds.briefing && (feeds.briefing.top || []).find(x => x.id === id); + if (story && window.__dioraFeedsOpenStory) { window.__dioraFeedsOpenStory(story); feedsAt = 0; } + else openFeeds('news'); + } else if (act === 'feeds') { openFeeds(el.dataset.view); feedsAt = 0; } else if (act === 'station') playStationFromHome(el.dataset.url, el.dataset.name); else if (act === 'pod-queue' && h) { const q = (parse(h.podcastState(), {}).queue || [])[0]; @@ -250,7 +307,10 @@ }); // Fresh whenever it shows, and while it shows (players move on, the book list arrives). - window.__dioraHomeShow = render; + window.__dioraHomeShow = function () { + refreshFeeds(30000); + render(); + }; if (typeof window.renderBookList === 'function') { const renderBookList = window.renderBookList; window.renderBookList = function () { @@ -292,6 +352,11 @@ '#tab-home .home-row { display: flex; justify-content: space-between; gap: 12px; width: 100%; padding: 10px 14px; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; font-size: 14px; text-align: left; }', '#tab-home .home-row-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '#tab-home .home-row-side { flex: none; color: var(--nui-muted, #9a9a9a); font-size: 12px; }', + '#tab-home .home-news { flex-direction: column; align-items: stretch; gap: 0; padding: 4px 0; }', + '#tab-home .home-story { align-items: baseline; }', + '#tab-home .home-news .home-story:last-child { border-bottom: none; }', + '#tab-home .home-story-title { overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; line-height: 1.35; }', + '#tab-home .home-story .home-row-side { max-width: 35%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '#tab-home .home-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }', '#tab-home .home-chip { padding: 6px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 16px; background: none; color: inherit; font: inherit; font-size: 13px; cursor: pointer; }', '#tab-home .home-stations { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; }', diff --git a/static/js/sw.js b/static/js/sw.js index a35056d..98827c7 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v89'; +const CACHE = 'diora-v90'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From c8e03f343e5e12eec554584cc92e2828bcaf1414 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sun, 4 Oct 2026 20:54:29 +0200 Subject: [PATCH 086/176] =?UTF-8?q?Feeds:=20Ordner=20f=C3=BCr=20Blogs,=20B?= =?UTF-8?q?logs=20ohne=20Datum=20in=20Feed-Reihenfolge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Die Kategorie eines Blogs ist sein Ordner: Ordner-Chips mit Ungelesen-Zahl in der Blog-Ansicht, /feeds/blogs/?folder=, Ordner im OPML-Export und -Import - Neue Feeds ohne (brauchbare) Daten: die Reihenfolge des Feeds gilt als Zeit, nur die ersten 3 ungelesen; ein neuer Blog bringt 7 statt 14 Tage ungelesen - sw v91 Co-Authored-By: Claude Opus 5.5 --- feeds/fetch.py | 8 +++++--- feeds/refresh.py | 22 +++++++++++++------- feeds/tests.py | 23 +++++++++++++++++++-- feeds/views.py | 8 ++++++-- static/js/feeds.js | 51 +++++++++++++++++++++++++++++++++------------- static/js/sw.js | 2 +- 6 files changed, 85 insertions(+), 29 deletions(-) diff --git a/feeds/fetch.py b/feeds/fetch.py index 4b23c75..6963692 100644 --- a/feeds/fetch.py +++ b/feeds/fetch.py @@ -68,7 +68,7 @@ def _short(text, n=SUMMARY_CHARS): def _when(entry): - """The entry's time (published, else updated), never in the future.""" + """The entry's time (published, else updated), never in the future, or None.""" now = timezone.now() for key in ('published_parsed', 'updated_parsed', 'created_parsed'): t = entry.get(key) @@ -78,7 +78,7 @@ def _when(entry): except (OverflowError, ValueError, TypeError): continue return min(when, now) - return now + return None class _FirstImage(HTMLParser): @@ -131,12 +131,14 @@ def parse_entry(entry, feed_url): text = plain(content) summary = plain(summary_html) or text title = plain(html.unescape(entry.get('title') or '')) or _short(text, 80) or 'Ohne Titel' + when = _when(entry) return { 'guid_hash': guid_hash(entry), 'url': link[:2000], 'title': title[:500], 'author': plain(entry.get('author') or '')[:300], - 'published': _when(entry), + 'published': when or timezone.now(), + 'dated': when is not None, # not an Item field: refresh._apply uses and drops it 'summary': _short(summary), 'content': content, 'image_url': _image(entry, content_html, base)[:2000], diff --git a/feeds/refresh.py b/feeds/refresh.py index d82796f..ee2cf48 100644 --- a/feeds/refresh.py +++ b/feeds/refresh.py @@ -22,7 +22,8 @@ FEED_WORKERS = 12 ARTICLE_WORKERS = 8 ARTICLES_PER_RUN = 80 # articles fetched per refresh at most; the rest when opened BLOG_KEEP = 1000 # items kept per blog -BLOG_FIRST_UNREAD_DAYS = 14 # a new blog's older articles start out read +BLOG_FIRST_UNREAD_DAYS = 7 # a new blog's older articles start out read +UNDATED_FIRST_UNREAD = 3 # of a new feed without dates, only the first few GONE_KEEP_DAYS = 30 # expired news rows kept (without text) against re-adding @@ -68,11 +69,16 @@ def _apply(source, result, now, keep_days): entries = [fetch.parse_entry(e, source.url) for e in parsed.entries] # Some feeds (Deutschlandfunk) give every entry the time the feed was made: # then the dates say nothing, and the time an entry first shows up counts. - stamps = Counter(f['published'] for f in entries) - bogus_dates = len(entries) >= 5 and stamps.most_common(1)[0][1] >= 0.8 * len(entries) - for fields in entries: - if bogus_dates: - fields['published'] = now + stamps = Counter(f['published'] for f in entries if f['dated']) + bogus_dates = len(entries) >= 5 and bool(stamps) and stamps.most_common(1)[0][1] >= 0.8 * len(entries) + for n, fields in enumerate(entries): + dated = fields.pop('dated') + undated = bogus_dates or not dated + if undated: + # First time: the feed's order stands for the time (newest first), so the + # list doesn't fill up with "just now"; afterwards, when it showed up. + fields['published'] = now - timedelta(minutes=n) if first else now + fields['undated'] = first and n >= UNDATED_FIRST_UNREAD if fields['guid_hash'] in known: continue known.add(fields['guid_hash']) @@ -80,8 +86,10 @@ def _apply(source, result, now, keep_days): continue if oldest_kept and fields['published'] < oldest_kept: continue + old_undated = fields.pop('undated', False) item = Item(source=source, user=source.user, kind=source.kind, **fields) - if source.kind == Source.BLOG and first and fields['published'] < now - timedelta(days=BLOG_FIRST_UNREAD_DAYS): + if source.kind == Source.BLOG and first and ( + old_undated or fields['published'] < now - timedelta(days=BLOG_FIRST_UNREAD_DAYS)): item.read_at = now new.append(item) if new: diff --git a/feeds/tests.py b/feeds/tests.py index 3f0dbf8..e482caf 100644 --- a/feeds/tests.py +++ b/feeds/tests.py @@ -142,6 +142,16 @@ class ApiTest(TestCase): self.post(f'/feeds/items/{item.id}/star/', {'starred': True}) self.assertEqual(len(self.client.get('/feeds/starred/').json()['items']), 1) + def test_blog_folders(self): + with mock.patch('feeds.refresh._fetch_feed', return_value=fetched(rss([{'guid': 'x', 'title': 'Essay', 'text': LONG}]))): + self.post('/feeds/sources/add/', {'url': 'https://a.example/feed', 'kind': 'blog', 'category': 'Philosophie'}) + with mock.patch('feeds.refresh._fetch_feed', return_value=fetched(rss([{'guid': 'y', 'title': 'Code', 'text': LONG}]))): + self.post('/feeds/sources/add/', {'url': 'https://b.example/feed', 'kind': 'blog', 'category': ''}) + self.assertEqual(self.client.get('/feeds/state/').json()['folders'], ['Philosophie']) + self.assertEqual([i['title'] for i in self.client.get('/feeds/blogs/?folder=Philosophie').json()['items']], ['Essay']) + self.assertEqual([i['title'] for i in self.client.get('/feeds/blogs/?folder=').json()['items']], ['Code']) + self.assertIn('text="Blogs – Philosophie"', self.client.get('/feeds/export.opml').content.decode()) + def test_old_news_expires_and_is_not_taken_in_again(self): old = (timezone.now() - timedelta(days=3)).strftime('%a, %d %b %Y %H:%M:%S +0000') feed = rss([{'guid': 'new', 'title': 'Heute passiert', 'text': LONG}, @@ -161,7 +171,16 @@ class ApiTest(TestCase): stamp = (timezone.now() - timedelta(hours=5)).strftime('%a, %d %b %Y %H:%M:%S +0000') self.add('news', rss([{'guid': f'g{n}', 'title': f'Meldung {n}', 'text': LONG, 'date': stamp} for n in range(6)])) for i in Item.objects.filter(user=self.u): - self.assertLess((timezone.now() - i.published).total_seconds(), 60) + self.assertLess((timezone.now() - i.published).total_seconds(), 600) # first fetch: feed order, minutes apart + + def test_new_blog_without_dates_keeps_order_and_few_unread(self): + feed = (b'Undatiert' + + b''.join(b'u%dPost %dx' % (n, n) + for n in range(6)) + b'') + self.add('blog', feed) + items = list(Item.objects.filter(user=self.u).order_by('-published')) + self.assertEqual([i.title for i in items], [f'Post {n}' for n in range(6)]) + self.assertEqual(sum(1 for i in items if i.read_at is None), 3) def test_switching_kind_moves_items(self): src = self.add('news', rss([{'guid': 'a', 'title': 'Etwas', 'text': LONG}])) @@ -185,7 +204,7 @@ class ApiTest(TestCase): self.add('blog', rss([]), url='https://blog.example/feed') r = self.client.get('/feeds/export.opml') self.assertIn(b'xmlUrl="https://blog.example/feed"', r.content) - self.assertIn(b'text="Blogs"', r.content) + self.assertIn('text="Blogs – Politik"', r.content.decode()) def test_settings(self): self.post('/feeds/settings/', {'news_keep_days': 5}) diff --git a/feeds/views.py b/feeds/views.py index 5bb5471..31fccf3 100644 --- a/feeds/views.py +++ b/feeds/views.py @@ -12,7 +12,7 @@ News: GET /feeds/briefing/ what's new since the briefing was POST /feeds/briefing/seen/ seen: everything up to now counts as read GET /feeds/news/?before=&category= stories, newest first GET /feeds/stories// one story with all its items -Blogs: GET /feeds/blogs/?source=&unread=1&before= +Blogs: GET /feeds/blogs/?folder=&source=&unread=1&before= (a blog's category is its folder) Items: GET /feeds/items// the article (fetches the page if the feed had a teaser) POST /feeds/items/read/ {ids, read} POST /feeds/items//star/ {starred} @@ -121,6 +121,7 @@ def state(request): return JsonResponse({ 'sources': [_source_data(x, unread.get(x.id, 0)) for x in sources], 'categories': sorted({x.category for x in sources if x.kind == Source.NEWS and x.category}), + 'folders': sorted({x.category for x in sources if x.kind == Source.BLOG and x.category}), 'news_new': new_news, 'blogs_unread': sum(n for sid, n in unread.items() if any(x.id == sid and x.kind == Source.BLOG for x in sources)), 'settings': {'news_keep_days': s.news_keep_days, 'news_seen_at': _iso(s.news_seen_at)}, @@ -274,7 +275,8 @@ def export_opml(request): return err groups = {} for s in Source.objects.filter(user=request.user).order_by('kind', 'category', 'title'): - label = ('Nachrichten' + (f' – {s.category}' if s.category else '')) if s.kind == Source.NEWS else 'Blogs' + kind = 'Nachrichten' if s.kind == Source.NEWS else 'Blogs' + label = kind + (f' – {s.category}' if s.category else '') groups.setdefault(label, []).append(s) lines = ['', '', 'diora Feeds', ''] @@ -378,6 +380,8 @@ def blogs(request): qs = Item.objects.filter(user=request.user, kind=Source.BLOG, gone=False).select_related('source') if request.GET.get('source', '').isdigit(): qs = qs.filter(source_id=int(request.GET['source'])) + elif 'folder' in request.GET: + qs = qs.filter(source__category=request.GET['folder']) if request.GET.get('unread') == '1': qs = qs.filter(read_at=None) before = parse_datetime(request.GET.get('before') or '') diff --git a/static/js/feeds.js b/static/js/feeds.js index c4d71ad..2d85db2 100644 --- a/static/js/feeds.js +++ b/static/js/feeds.js @@ -23,6 +23,7 @@ category: '', items: [], itemsMore: false, blogSource: '', blogUnread: true, + blogFolder: null, // null = all blogs, '' = blogs without a folder, else a folder name found: null, // discover results opml: null, // parsed OPML import editing: null, // source id being edited @@ -96,6 +97,7 @@ async function loadBlogs(more) { const q = []; if (st.blogSource) q.push('source=' + st.blogSource); + else if (st.blogFolder !== null) q.push('folder=' + encodeURIComponent(st.blogFolder)); if (st.blogUnread) q.push('unread=1'); if (more && st.items.length) q.push('before=' + encodeURIComponent(st.items[st.items.length - 1].published)); const data = await api('/feeds/blogs/' + (q.length ? '?' + q.join('&') : '')); @@ -227,9 +229,23 @@ return '

Noch keine Blogs abonniert.

' + '
'; } - let h = '
' + - '' + + shown.map(b => '').join('') + '' + '' + (st.items.some(i => !i.read) ? '' : '') + @@ -252,8 +268,8 @@ } function categoryInput(value) { - const cats = (st.state && st.state.categories) || []; - return '' + + const cats = ((st.state && st.state.categories) || []).concat((st.state && st.state.folders) || []); + return '' + '' + cats.map(c => ''; } @@ -270,7 +286,7 @@ (f.samples.length ? '
    ' + f.samples.slice(0, 3).map(t => '
  • ' + esc(t) + '
  • ').join('') + '
' : '') + (f.subscribed ? '

Schon abonniert.

' : '
' + - '
' + + '
' + '') + '').join(''); } @@ -283,12 +299,12 @@ list.map(x => x.id === st.editing ? editForm(x) : '').join('') + ''; h += group('Nachrichten', news.sort((a, b) => (a.category || '').localeCompare(b.category || '') || a.title.localeCompare(b.title))); - h += group('Blogs', blogs); + h += group('Blogs', blogs.sort((a, b) => (a.category || '~').localeCompare(b.category || '~') || a.title.localeCompare(b.title))); h += '

Import & Einstellungen

' + ' ' + @@ -307,7 +323,7 @@ '
' + esc(x.url) + '
' + (x.last_error ? '

Letzter Fehler: ' + esc(x.last_error) + '

' : '') + '
' + - '' + + '' + '
' + '
' + '' + - '
'; + '' + + loginForm(x); + } + + // A paywalled site (junge Welt …): the subscriber login, kept encrypted on the + // server, which then fetches whole articles. The password never comes back. + function loginForm(x) { + const l = x.login; + if (!l) return ''; + let h = ''; + h += '

Neu eingeben:

'; + } + return h + + '' + + '' + + '

Das Passwort liegt verschlüsselt auf dem diora-Server und wird nie wieder angezeigt. ' + + 'Damit holt der Server die ganzen Artikel statt nur des freien Teils.

' + + '
'; } function renderOpml() { @@ -449,7 +471,10 @@ const content = i.content || ''; if (i.image_url && content.indexOf(i.image_url) < 0) h += ''; h += '
' + (content || '

' + esc(i.summary) + '

') + '
'; - if (!i.content_full && i.url && (content.replace(/<[^>]+>/g, '').length < 700)) { + if (i.paywalled) { + h += '

Nur der freie Teil des Artikels. Mit deiner Abo-Anmeldung (⚙ → ' + esc(i.source_title) + + ') holt diora den ganzen.

'; + } else if (!i.content_full && i.url && (content.replace(/<[^>]+>/g, '').length < 700)) { h += '

Der Feed bringt nur einen Anriss, der ganze Artikel ließ sich nicht laden.

'; } if (story && story.items.length > 1) { @@ -591,6 +616,11 @@ await api('/feeds/items/read/', {ids, read: true}).catch(() => {}); return load(); } + if (act === 'login-delete') { + await api('/feeds/logins/delete/', {source: parseInt(t.dataset.id, 10)}).catch(() => {}); + await loadState().catch(() => {}); + return render(); + } if (act === 'cancel-edit') { st.editing = null; return render(); @@ -678,6 +708,14 @@ category: f.category.value}); found.subscribed = true; await loadState(); + } else if (f.dataset.form === 'login') { + if (btn) btn.textContent = 'Meldet an …'; + const r = await api('/feeds/logins/', {source: parseInt(f.dataset.id, 10), username: f.username.value, password: f.password.value}); + f.password.value = ''; + await loadState(); + if (typeof customAlert === 'function') { + customAlert('Angemeldet.' + (r.refetched ? ' ' + r.refetched + ' Artikel jetzt vollständig.' : '')); + } } else if (f.dataset.form === 'edit') { await api('/feeds/sources/' + f.dataset.id + '/', {title: f.title.value, kind: f.kind.value, category: f.category.value, fulltext: f.fulltext.value}); @@ -836,6 +874,8 @@ '#tab-feeds .fd-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px; }', '#tab-feeds .fd-grid label, #tab-feeds .fd-edit > label { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--nui-muted, #9a9a9a); }', '#tab-feeds .fd-buttons { display: flex; flex-wrap: wrap; gap: 8px; }', + '#tab-feeds .fd-login h4 { margin: 0; font-size: 13px; }', + '#tab-feeds .fd-login p { margin: 0; font-size: 13px; }', '#tab-feeds .fd-src { display: flex; flex-direction: column; gap: 2px; width: 100%; padding: 10px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; text-align: left; cursor: pointer; }', '#tab-feeds .fd-opml { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; }', '#tab-feeds .fd-opml-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; }', diff --git a/static/js/sw.js b/static/js/sw.js index 725d6f1..c79b824 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v91'; +const CACHE = 'diora-v92'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From b063e91113ef05155b5518126c6541fcb2743b85 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sun, 4 Oct 2026 21:32:15 +0200 Subject: [PATCH 088/176] Blog-Feed nach Kino: Abschnitte, Favoriten, Seltene zuerst, Ausblenden, NEU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Abschnitte Heute/Gestern/Diese Woche/Diesen Monat/Älter; je Abschnitt bis zu 5 Posts je Favoriten-Blog zuerst, dann „Seltene zuerst“ (mittlerer Abstand der letzten 10 Posts, ein Post = 30 Tage) oder chronologisch - Blog „im Hauptfeed ausblenden“: nicht unter Alle und nicht gezählt, im Ordner da - NEU (jünger als 24 h, ungelesen) am Post, als Zahl am Tab, Punkt am Ordner - Chips Ungelesen/Alle, ★ Favoriten, Seltene zuerst; Schalter je Blog - Lesezeit (Wörter/230) an jedem Post; words wird beim Speichern gezählt - sw v93 Co-Authored-By: Claude Opus 5.5 --- feeds/fetch.py | 5 + feeds/migrations/0003_blog_flags_words.py | 42 ++++++ feeds/models.py | 6 + feeds/refresh.py | 6 +- feeds/tests.py | 43 ++++++ feeds/views.py | 56 +++++-- static/js/feeds.js | 170 +++++++++++++++++++--- static/js/sw.js | 2 +- 8 files changed, 294 insertions(+), 36 deletions(-) create mode 100644 feeds/migrations/0003_blog_flags_words.py diff --git a/feeds/fetch.py b/feeds/fetch.py index 7c3f050..fa25a1b 100644 --- a/feeds/fetch.py +++ b/feeds/fetch.py @@ -141,10 +141,15 @@ def parse_entry(entry, feed_url): 'dated': when is not None, # not an Item field: refresh._apply uses and drops it 'summary': _short(summary), 'content': content, + 'words': word_count(content), 'image_url': _image(entry, content_html, base)[:2000], } +def word_count(content_html): + return len(plain(content_html).split()) + + def is_teaser(content_html): return len(plain(content_html)) < TEASER_CHARS diff --git a/feeds/migrations/0003_blog_flags_words.py b/feeds/migrations/0003_blog_flags_words.py new file mode 100644 index 0000000..a3625cf --- /dev/null +++ b/feeds/migrations/0003_blog_flags_words.py @@ -0,0 +1,42 @@ +# Generated by Django 6.1 on 2026-10-04 19:28 + +from django.db import migrations, models + + +def count_words(apps, schema_editor): + """The reading time of the articles already there.""" + from feeds.fetch import word_count + Item = apps.get_model('feeds', 'Item') + for item in Item.objects.exclude(content='').only('id', 'content').iterator(): + Item.objects.filter(pk=item.pk).update(words=word_count(item.content)) + + +class Migration(migrations.Migration): + + dependencies = [ + ('feeds', '0002_item_paywalled_sitelogin'), + ] + + operations = [ + migrations.AddField( + model_name='item', + name='words', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='source', + name='fav', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='source', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='source', + name='notify', + field=models.BooleanField(default=False), + ), + migrations.RunPython(count_words, migrations.RunPython.noop), + ] diff --git a/feeds/models.py b/feeds/models.py index 1775467..7b81112 100644 --- a/feeds/models.py +++ b/feeds/models.py @@ -28,6 +28,11 @@ class Source(models.Model): category = models.CharField(max_length=100, blank=True) fulltext = models.CharField(max_length=10, choices=FULLTEXTS, default=FULLTEXT_AUTO) added_at = models.DateTimeField(auto_now_add=True) + # Blogs: favorites come first; a hidden blog stays out of "Alle" (and its + # counts) but shows in its folder; notify = a notification for new posts. + fav = models.BooleanField(default=False) + hidden = models.BooleanField(default=False) + notify = models.BooleanField(default=False) # Refresh bookkeeping, as for podcast feeds (see podcasts.views._fetch_feed). last_refreshed_at = models.DateTimeField(null=True, blank=True) @@ -82,6 +87,7 @@ class Item(models.Model): # a subscriber login for the site (logins.py). paywalled = models.BooleanField(default=False) image_url = models.URLField(max_length=2000, blank=True) + words = models.PositiveIntegerField(default=0) # of the content: the reading time read_at = models.DateTimeField(null=True, blank=True) starred = models.BooleanField(default=False) story = models.ForeignKey(Story, null=True, blank=True, on_delete=models.SET_NULL, related_name='items') diff --git a/feeds/refresh.py b/feeds/refresh.py index 6314e74..5f96656 100644 --- a/feeds/refresh.py +++ b/feeds/refresh.py @@ -146,20 +146,22 @@ def _store_article(item, got): item.paywalled = True if longer: item.content = content + item.words = fetch.word_count(content) if not item.image_url and image: item.image_url = image[:2000] - item.save(update_fields=['paywalled', 'content', 'image_url']) + item.save(update_fields=['paywalled', 'content', 'words', 'image_url']) return if not longer: item.fulltext_failed = True # the feed had more item.save(update_fields=['fulltext_failed']) return item.content = content + item.words = fetch.word_count(content) item.content_full = True item.paywalled = False if not item.image_url and image: item.image_url = image[:2000] - item.save(update_fields=['content', 'content_full', 'paywalled', 'image_url']) + item.save(update_fields=['content', 'words', 'content_full', 'paywalled', 'image_url']) def _articles(items, deadline): diff --git a/feeds/tests.py b/feeds/tests.py index b0d8e2f..4245fb8 100644 --- a/feeds/tests.py +++ b/feeds/tests.py @@ -291,3 +291,46 @@ class SiteLoginTest(TestCase): by_id = {s['id']: s for s in state['sources']} self.assertIsNone(by_id[other.id]['login']) self.assertIsNotNone(by_id[self.src.id]['login']) + + +class BlogFeedTest(TestCase): + def setUp(self): + self.u = User.objects.create_user('tester', password='pw12345678') + self.client.force_login(self.u) + now = timezone.now() + self.busy = Source.objects.create(user=self.u, url='https://busy/', title='Busy', kind='blog', category='Politik') + self.rare = Source.objects.create(user=self.u, url='https://rare/', title='Rare', kind='blog', category='Politik') + for n in range(10): # a post every two hours + Item.objects.create(source=self.busy, user=self.u, kind='blog', guid_hash=f'b{n}', title=f'Busy {n}', + content='

' + 'wort ' * 460 + '

', words=460, published=now - timedelta(hours=2 * n)) + Item.objects.create(source=self.rare, user=self.u, kind='blog', guid_hash='r', title='Rare essay', + published=now - timedelta(hours=5)) + + def post(self, url, data): + return self.client.post(url, json.dumps(data), content_type='application/json') + + def by_id(self): + return {s['id']: s for s in self.client.get('/feeds/state/').json()['sources']} + + def test_interval_new_and_words(self): + st = self.by_id() + self.assertLess(st[self.busy.id]['interval_days'], 1) + self.assertEqual(st[self.rare.id]['interval_days'], 30.0) # one post: counts as rare + self.assertEqual(st[self.busy.id]['new'], 10) + items = self.client.get('/feeds/blogs/?unread=1').json()['items'] + self.assertEqual(next(i for i in items if i['title'] == 'Busy 0')['words'], 460) + + def test_hidden_blog_only_in_its_folder_and_not_counted(self): + self.post(f'/feeds/sources/{self.busy.id}/', {'hidden': True}) + state = self.client.get('/feeds/state/').json() + self.assertEqual((state['blogs_unread'], state['blogs_new']), (1, 1)) + main = [i['title'] for i in self.client.get('/feeds/blogs/?unread=1').json()['items']] + self.assertEqual(main, ['Rare essay']) + folder = self.client.get('/feeds/blogs/?unread=1&folder=Politik').json()['items'] + self.assertEqual(len(folder), 11) + + def test_favorites_filter(self): + self.post(f'/feeds/sources/{self.rare.id}/', {'fav': True}) + self.assertTrue(self.by_id()[self.rare.id]['fav']) + favs = [i['title'] for i in self.client.get('/feeds/blogs/?fav=1').json()['items']] + self.assertEqual(favs, ['Rare essay']) diff --git a/feeds/views.py b/feeds/views.py index 6a676a4..de64433 100644 --- a/feeds/views.py +++ b/feeds/views.py @@ -12,7 +12,8 @@ News: GET /feeds/briefing/ what's new since the briefing was POST /feeds/briefing/seen/ seen: everything up to now counts as read GET /feeds/news/?before=&category= stories, newest first GET /feeds/stories// one story with all its items -Blogs: GET /feeds/blogs/?folder=&source=&unread=1&before= (a blog's category is its folder) +Blogs: GET /feeds/blogs/?folder=&source=&unread=1&fav=1&before= (a blog's category is its folder; + without folder/source the hidden blogs stay out) Items: GET /feeds/items// the article (fetches the page if the feed had a teaser) POST /feeds/items/read/ {ids, read} POST /feeds/items//star/ {starred} @@ -40,6 +41,10 @@ from . import fetch, logins, refresh from .models import Item, SiteLogin, Source, Story PAGE = 40 +BLOG_PAGE = 150 # blog posts per page (all unread ones come at once, up to BLOG_UNREAD_MAX) +BLOG_UNREAD_MAX = 600 +NEW_HOURS = 24 # a blog post this young and unread is "new" +RARE_POSTS = 10 # how often a blog posts: the mean gap over its last this many posts BRIEFING_TOP = 8 BRIEFING_PER_CATEGORY = 5 REFRESH_WAIT = 40 # seconds an explicit refresh may take @@ -63,10 +68,13 @@ def _iso(dt): return dt.isoformat() if dt else None -def _source_data(s, unread=0): +def _source_data(s, unread=0, new=0, interval=None): return { 'id': s.id, 'url': s.url, 'title': s.title, 'site_url': s.site_url, 'icon_url': s.icon_url, - 'kind': s.kind, 'category': s.category, 'fulltext': s.fulltext, 'unread': unread, + 'kind': s.kind, 'category': s.category, 'fulltext': s.fulltext, 'unread': unread, 'new': new, + 'fav': s.fav, 'hidden': s.hidden, 'notify': s.notify, + # Days between posts (blogs, for "Seltene zuerst"); a blog with one post counts as rare. + 'interval_days': interval, 'last_refreshed_at': _iso(s.last_refreshed_at), 'last_error': s.last_error, 'error_count': s.error_count, 'login': logins.info(s.user_id, s.url), } @@ -75,7 +83,7 @@ def _source_data(s, unread=0): def _item_data(i, content=False): d = { 'id': i.id, 'source': i.source_id, 'source_title': i.source.title, 'kind': i.kind, - 'url': i.url, 'title': i.title, 'author': i.author, 'published': _iso(i.published), + 'url': i.url, 'title': i.title, 'author': i.author, 'published': _iso(i.published), 'words': i.words, 'summary': i.summary, 'image_url': i.image_url, 'read': i.read_at is not None, 'starred': i.starred, 'story': i.story_id, } @@ -117,21 +125,40 @@ def state(request): return err unread = dict(Item.objects.filter(user=request.user, gone=False, read_at=None) .values_list('source').annotate(n=Count('id'))) + now = timezone.now() + new = dict(Item.objects.filter(user=request.user, kind=Source.BLOG, gone=False, read_at=None, + published__gte=now - timedelta(hours=NEW_HOURS)) + .values_list('source').annotate(n=Count('id'))) sources = list(Source.objects.filter(user=request.user)) + intervals = _intervals(request.user, [x for x in sources if x.kind == Source.BLOG], now) s = refresh.settings_for(request.user) since = s.news_seen_at or (timezone.now() - timedelta(days=1)) new_news = Story.objects.filter(user=request.user, last_at__gt=since, items__gone=False, items__read_at=None).distinct().count() return JsonResponse({ - 'sources': [_source_data(x, unread.get(x.id, 0)) for x in sources], + 'sources': [_source_data(x, unread.get(x.id, 0), new.get(x.id, 0), intervals.get(x.id)) for x in sources], 'categories': sorted({x.category for x in sources if x.kind == Source.NEWS and x.category}), 'folders': sorted({x.category for x in sources if x.kind == Source.BLOG and x.category}), 'news_new': new_news, - 'blogs_unread': sum(n for sid, n in unread.items() if any(x.id == sid and x.kind == Source.BLOG for x in sources)), + 'blogs_unread': sum(unread.get(x.id, 0) for x in sources if x.kind == Source.BLOG and not x.hidden), + 'blogs_new': sum(new.get(x.id, 0) for x in sources if x.kind == Source.BLOG and not x.hidden), 'settings': {'news_keep_days': s.news_keep_days, 'news_seen_at': _iso(s.news_seen_at)}, }) +def _intervals(user, blogs, now): + """Mean days between the last RARE_POSTS posts of each blog, up to now: + (now - the oldest of them) / their number; one post counts as 30 days.""" + out = {} + for s in blogs: + dates = list(s.items.filter(gone=False).order_by('-published').values_list('published', flat=True)[:RARE_POSTS]) + if not dates: + continue + days = (now - dates[-1]).total_seconds() / 86400 / len(dates) + out[s.id] = round(max(days, 30.0) if len(dates) == 1 else days, 2) + return out + + def _probe(url): """(feed url, title, entries) if `url` is a feed, else ([candidate feeds], icon).""" r = requests.get(url, timeout=fetch.PAGE_TIMEOUT, headers={'User-Agent': fetch.USER_AGENT}) @@ -248,6 +275,9 @@ def edit_source(request, pk): return JsonResponse({'error': 'not found'}, status=404) if 'title' in body and (body['title'] or '').strip(): source.title = body['title'].strip()[:300] + for flag in ('fav', 'hidden', 'notify'): + if isinstance(body.get(flag), bool): + setattr(source, flag, body[flag]) if 'category' in body: source.category = (body['category'] or '').strip()[:100] if body.get('fulltext') in dict(Source.FULLTEXTS): @@ -385,14 +415,22 @@ def blogs(request): if request.GET.get('source', '').isdigit(): qs = qs.filter(source_id=int(request.GET['source'])) elif 'folder' in request.GET: + # A folder shows all its blogs, the hidden ones too. qs = qs.filter(source__category=request.GET['folder']) - if request.GET.get('unread') == '1': + else: + qs = qs.filter(source__hidden=False) + if request.GET.get('fav') == '1': + qs = qs.filter(source__fav=True) + unread_only = request.GET.get('unread') == '1' + if unread_only: qs = qs.filter(read_at=None) before = parse_datetime(request.GET.get('before') or '') if before: qs = qs.filter(published__lt=before) - items = list(qs.order_by('-published')[:PAGE]) - return JsonResponse({'items': [_item_data(i) for i in items], 'more': len(items) == PAGE}) + # Unread: all of them at once (the page sorts them into days, favorites, rare blogs). + limit = BLOG_UNREAD_MAX if unread_only else BLOG_PAGE + items = list(qs.order_by('-published')[:limit]) + return JsonResponse({'items': [_item_data(i) for i in items], 'more': len(items) == limit}) @csrf_exempt diff --git a/static/js/feeds.js b/static/js/feeds.js index b591a33..35c7352 100644 --- a/static/js/feeds.js +++ b/static/js/feeds.js @@ -23,6 +23,8 @@ category: '', items: [], itemsMore: false, blogSource: '', blogUnread: true, + blogFav: false, // main feed: only the favorite blogs + rareFirst: true, // within a day: blogs that seldom post first blogFolder: null, // null = all blogs, '' = blogs without a folder, else a folder name found: null, // discover results opml: null, // parsed OPML import @@ -33,6 +35,8 @@ try { const saved = localStorage.getItem('diora_feeds_view'); if (saved === 'news' || saved === 'blogs') st.view = saved; + if (localStorage.getItem('diora_feeds_rare') === '0') st.rareFirst = false; + if (localStorage.getItem('diora_feeds_unread') === '0') st.blogUnread = false; } catch (e) {} const esc = s => String(s == null ? '' : s).replace(/&/g, '&').replace(/= 120 ? Math.max(1, Math.round(i.words / 230)) + ' Min.' : ''; + } + + function section(iso) { + const d = new Date(iso); + const label = dayLabel(d); + if (label === 'Heute' || label === 'Gestern') return label; + const days = (Date.now() - d.getTime()) / 86400000; + if (days < 7) return 'Diese Woche'; + if (days < 31) return 'Diesen Monat'; + return 'Älter'; + } + + // Posts into sections (newest first); in each, up to FAV_PER_DAY posts per + // favorite blog first, then the rest — blogs that post seldom ahead of the + // busy ones ("Seltene zuerst"), or simply by time. + function blogSections(items, byId) { + const order = ['Heute', 'Gestern', 'Diese Woche', 'Diesen Monat', 'Älter']; + const groups = {}; + items.forEach(i => { (groups[section(i.published)] = groups[section(i.published)] || []).push(i); }); + const newest = (a, b) => new Date(b.published) - new Date(a.published); + return order.filter(k => groups[k]).map(k => { + const list = groups[k].slice().sort(newest); + const favCount = {}; + const fav = []; + const rest = []; + list.forEach(i => { + const src = byId[i.source]; + if (src && src.fav && (favCount[i.source] || 0) < FAV_PER_DAY) { + favCount[i.source] = (favCount[i.source] || 0) + 1; + fav.push(i); + } else { + rest.push(i); + } + }); + if (st.rareFirst) { + const gap = i => { const src = byId[i.source]; return (src && src.interval_days) || 0; }; + rest.sort((a, b) => (gap(b) - gap(a)) || newest(a, b)); + } + return {label: k, items: fav.concat(rest)}; + }); + } + function renderBlogs() { const blogs = sources('blog'); if (!blogs.length) { return '

Noch keine Blogs abonniert.

' + '
'; } - // Folders (a blog's category): chips with their unread count. + const byId = {}; + blogs.forEach(b => { byId[b.id] = b; }); + // Folders (a blog's category): chips with their unread count, a dot when something is new. const folders = (st.state.folders || []); - const unreadIn = f => blogs.filter(b => (b.category || '') === f).reduce((n, b) => n + b.unread, 0); - const total = blogs.reduce((n, b) => n + b.unread, 0); + const inFolder = f => blogs.filter(b => (b.category || '') === f); + const sum = (list, key) => list.reduce((n, b) => n + b[key], 0); + const visible = blogs.filter(b => !b.hidden); let h = ''; if (folders.length) { - const chip = (value, label, n) => ''; - h += '
' + chip(null, 'Alle', total) + - folders.map(f => chip(f, f, unreadIn(f))).join('') + - (blogs.some(b => !b.category) ? chip('', 'Ohne Ordner', unreadIn('')) : '') + '
'; + const chip = (value, label, list) => ''; + h += '
' + chip(null, 'Alle', visible) + + folders.map(f => chip(f, f, inFolder(f))).join('') + + (blogs.some(b => !b.category) ? chip('', 'Ohne Ordner', inFolder('')) : '') + '
'; } - const shown = st.blogFolder === null ? blogs : blogs.filter(b => (b.category || '') === st.blogFolder); + const main = st.blogFolder === null; + const shown = main ? blogs : inFolder(st.blogFolder); + h += '
' + + '' + + '' + + (main ? '' : '') + + '' + + '
'; h += '
' + '' + - '' + + (b.fav ? '★ ' : '') + esc(b.title) + (b.unread ? ' (' + b.unread + ')' : '') + (b.hidden ? ' – ausgeblendet' : '') + + '').join('') + '' + (st.items.some(i => !i.read) ? '' : '') + '
'; + const one = st.blogSource ? byId[st.blogSource] : null; + if (one) { + // The selected blog's switches. + h += '
' + + '' + + '' + + '
'; + } if (!st.items.length) return h + '

' + (st.loading ? 'Lädt …' : (st.blogUnread ? 'Alles gelesen.' : 'Keine Artikel.')) + '

'; - h += st.items.map(i => - '').join(''); + blogSections(st.items, byId).forEach(sec => { + h += '

' + esc(sec.label) + '

'; + h += sec.items.map(i => { + const src = byId[i.source]; + const rt = readingTime(i); + return ''; + }).join(''); + }); if (st.itemsMore) h += ''; return h; } @@ -298,7 +383,7 @@ '

' + title + ' ' + list.length + '

' + list.map(x => x.id === st.editing ? editForm(x) : '' + + '' + + '' + + ''; + sheet.addEventListener('click', e => { + const b = e.target.closest('[data-s]'); + if (!b && e.target !== sheet) return; + sheet.remove(); + const what = b ? b.dataset.s : 'cancel'; + if (what === 'open') { st.blogFolder = name; st.blogSource = ''; load(); } + if (what === 'rename') folderAction('folder-rename', name); + if (what === 'dissolve') folderAction('folder-dissolve', name); + }); + document.body.appendChild(sheet); + } + // ---- events tab.addEventListener('click', async e => { @@ -734,6 +862,7 @@ await api('/feeds/items/read/', {ids, read: true}).catch(() => {}); return load(); } + if (act === 'folder-rename' || act === 'folder-dissolve') return folderAction(act, t.dataset.name); if (act === 'login-delete') { await api('/feeds/logins/delete/', {source: parseInt(t.dataset.id, 10)}).catch(() => {}); await loadState().catch(() => {}); @@ -834,7 +963,12 @@ customAlert('Angemeldet.' + (r.refetched ? ' ' + r.refetched + ' Artikel jetzt vollständig.' : '')); } } else if (f.dataset.form === 'edit') { - const change = {title: f.title.value, kind: f.kind.value, category: f.category.value, fulltext: f.fulltext.value}; + const change = {title: f.title.value, kind: f.kind.value, fulltext: f.fulltext.value}; + if (f.category) change.category = f.category.value; + if (f.newfolder) { + change.folders = Array.prototype.filter.call(f.querySelectorAll('input[name=folder]'), c => c.checked).map(c => c.value); + if (f.newfolder.value.trim()) change.folders.push(f.newfolder.value.trim()); + } if (f.fav) change.fav = f.fav.checked; if (f.hidden) change.hidden = f.hidden.checked; if (f.notify) change.notify = f.notify.checked; @@ -982,6 +1116,19 @@ '#tab-feeds .fd-blogbar { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 8px; }', '#tab-feeds .fd-new { display: inline-block; padding: 1px 5px; border-radius: 3px; background: var(--accent, #e63946); color: #fff; font-size: 10px; letter-spacing: 0.05em; vertical-align: 2px; }', '#tab-feeds .fd-hidden { opacity: 0.6; }', + '#tab-feeds .fd-prog { display: block; height: 3px; margin-top: 4px; background: #2a2a2a; border-radius: 2px; overflow: hidden; }', + '#tab-feeds .fd-prog > span { display: block; height: 100%; background: var(--accent, #e63946); }', + '#tab-feeds .fd-folders { border: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; }', + '#tab-feeds .fd-folders legend { font-size: 12px; color: var(--nui-muted, #9a9a9a); margin-bottom: 4px; }', + '#tab-feeds .fd-folders input[name=newfolder] { flex-basis: 100%; }', + '#tab-feeds .fd-folder-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid var(--border, #2a2a2a); flex-wrap: wrap; }', + '#fd-sheet { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 995; background: rgba(0,0,0,0.5); display: flex; align-items: flex-end; justify-content: center; }', + '#fd-sheet .fd-sheet-box { width: 100%; max-width: 480px; background: var(--bg, #000); border: 1px solid var(--border, #2a2a2a); border-bottom: none; border-radius: 10px 10px 0 0; padding: 8px 0 calc(8px + env(safe-area-inset-bottom, 0px)); }', + '#fd-sheet .fd-sheet-title { padding: 10px 18px; font-size: 13px; color: var(--nui-muted, #9a9a9a); }', + '#fd-sheet button { display: block; width: 100%; padding: 14px 18px; background: none; border: none; color: var(--fg, #fff); font: inherit; text-align: left; cursor: pointer; }', + 'body.eink #tab-feeds .fd-prog > span { background: #000; }', + 'body.eink #fd-sheet .fd-sheet-box { background: #fff; }', + 'body.eink #fd-sheet button { color: #000; }', 'body.eink #tab-feeds .fd-new, body.eink .fd-dot { background: #000; }', '#tab-feeds .fd-day { margin-top: 16px; }', '#tab-feeds .fd-card { display: flex; gap: 12px; width: 100%; padding: 12px 0; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; text-align: left; cursor: pointer; }', diff --git a/static/js/sw.js b/static/js/sw.js index 73e0028..dda6cf9 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v94'; +const CACHE = 'diora-v95'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From 3d659fc80971a3773e691c64376f44f8b6b0396e Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Mon, 5 Oct 2026 17:50:44 +0200 Subject: [PATCH 091/176] =?UTF-8?q?/api/sync/=3Fonly=3Dbooks:=20nur=20der?= =?UTF-8?q?=20B=C3=BCcher-Teil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Buch-Synchronisation der Apps braucht vom Schnappschuss nur Bücher, Fortschritt, Markierungen und Lesezeichen; der Podcast-Teil (jede je gespielte Folge) ist für ein Konto aus AntennaPod 11 MB groß und wurde bei jedem Abgleich mitgeladen – über mobile Daten Gigabytes. Co-Authored-By: Claude Opus 5.5 --- accounts/sync.py | 15 ++++++++++++++- accounts/tests.py | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/accounts/sync.py b/accounts/sync.py index dcd2422..5dccfb3 100644 --- a/accounts/sync.py +++ b/accounts/sync.py @@ -19,7 +19,11 @@ def sync_snapshot(request): endpoints (e.g. GET /books//data/), which this same token also unlocks. Writes (progress, highlights, bookmarks, ...) go through those existing endpoints too, to reuse their merge semantics rather than - duplicating it here.""" + duplicating it here. + + ?only=books: just the books' part. The apps' book sync needs nothing else, + and the podcast part is by far the biggest (every episode ever played: + megabytes for an account that moved over from AntennaPod).""" if not request.user.is_authenticated: return JsonResponse({'error': 'authentication required'}, status=401) @@ -43,6 +47,15 @@ def sync_snapshot(request): for bm in book_bookmarks: bm['updated_at'] = bm['updated_at'].isoformat() + if request.GET.get('only') == 'books': + return JsonResponse({ + 'server_time': timezone.now().isoformat(), + 'books': books, + 'book_progress': book_progress, + 'book_highlights': book_highlights, + 'book_bookmarks': book_bookmarks, + }) + podcast_feeds = list( PodcastFeed.objects.filter(user=user) .annotate(latest_episode_at=Max('episodes__pub_date')) diff --git a/accounts/tests.py b/accounts/tests.py index 638ec51..35d015e 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -85,6 +85,12 @@ class SyncSnapshotTests(TestCase): resp = self.client.get('/api/sync/') self.assertEqual(resp.status_code, 401) + def test_only_books(self): + resp = self.client.get('/api/sync/?only=books', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') + data = resp.json() + self.assertEqual(set(data), {'server_time', 'books', 'book_progress', 'book_highlights', 'book_bookmarks'}) + self.assertEqual(data['book_highlights'][0]['ct'], 'hl-ct') + def test_snapshot_shape_and_data(self): resp = self.client.get('/api/sync/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') self.assertEqual(resp.status_code, 200) From aa4cf23b58412bb4c86997cdfb029cc2b58f6594 Mon Sep 17 00:00:00 2001 From: Marwin Schulz Date: Wed, 7 Oct 2026 08:50:07 +0200 Subject: [PATCH 092/176] =?UTF-8?q?B=C3=BCcherliste:=20Seitenzahl=20neben?= =?UTF-8?q?=20dem=20Lesefortschritt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PDFs liefern die echte Seitenzahl, bei EPUBs wird sie aus der Zeichenzahl geschätzt (1800 Zeichen je Seite). Gespeichert in den verschlüsselten Metadaten beim Öffnen des Buchs. Co-Authored-By: Claude Sonnet 5.5 --- static/js/app.js | 18 ++++++++++++++---- static/js/sw.js | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index d12f1a5..5112828 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1991,6 +1991,8 @@ let readerScrollSaveTimer = null; let _resizeObserver = null; let _currentPositionAnchor = ''; let _currentBookCharCount = 0; +// Rough characters per printed book page, used to estimate a real page count for EPUBs. +const CHARS_PER_BOOK_PAGE = 1800; const bookMetaCache = {}; // id → {title, author, type} // Reading-position percentages (the progress readout, annotation "% im Buch" @@ -2493,8 +2495,8 @@ async function loadBookList() { try { const metaBuf = await decryptBytes(key, b.meta_iv, b.meta_ct); const meta = JSON.parse(new TextDecoder().decode(metaBuf)); - bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || ''}; - decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); + bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', pages: meta.pages || 0}; + decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', pages: meta.pages || 0, scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } catch (e) { bookMetaCache[b.id] = {title: `Buch #${b.id}`, author: '', type: 'epub', filename: '', folder: '', isbn: '', shelfTag: ''}; decrypted.push({id: b.id, title: `Buch #${b.id}`, author: '', type: 'epub', folder: '', isbn: '', shelfTag: '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); @@ -2536,7 +2538,7 @@ async function loadBookList() { if (b.last_read) return 1; return (b.uploaded_at || '').localeCompare(a.uploaded_at || ''); }); - for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || '', isbn: b.isbn || '', shelfTag: b.shelfTag || ''}; + for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || '', isbn: b.isbn || '', shelfTag: b.shelfTag || '', pages: b.pages || 0}; const uploadArea = $('book-upload-area'); if (uploadArea) uploadArea.style.display = 'none'; renderBookList(cachedBooks); @@ -2698,7 +2700,7 @@ function _renderBookItemHtml(b) { ${escapeHtml(b.title)}${keyWarning}${b.is_read ? ' ✓ gelesen' : ''} ${escapeHtml(b.author)} - ${pct > 0 ? `${pct} % gelesen` : ''} + ${pct > 0 ? `${pct} % gelesen${b.pages ? ` (≈ ${Math.max(1, Math.round(b.pages * pct / 100))} von ${b.pages} Seiten)` : ''}` : (b.pages ? `ca. ${b.pages} Seiten` : '')} ${b.shelfTag ? `${escapeHtml(b.shelfTag)}` : ''} @@ -2825,6 +2827,7 @@ async function _updateBookMeta(bookId, overrides) { isbn: cached.isbn || book.isbn || '', folder: cached.folder || book.folder || '', shelfTag: cached.shelfTag || book.shelfTag || '', + pages: cached.pages || book.pages || 0, ...overrides, }; const key = await getOrCreateEncKey(); @@ -3767,6 +3770,13 @@ async function _openBookOnce(bookId) { currentBookId = bookId; + // Remember the book's real (PDF) or estimated (EPUB) page count in its encrypted meta so + // the book list can show it; rewritten only when it changed. + const bookPages = isPdfBook ? numPages : Math.max(1, Math.round(_currentBookCharCount / CHARS_PER_BOOK_PAGE)); + if (bookPages > 0 && cachedMeta.pages !== bookPages) { + _updateBookMeta(bookId, {pages: bookPages}).catch(() => {}); + } + // Load bookmarks and highlights await Promise.all([ loadBookmarks(bookId), diff --git a/static/js/sw.js b/static/js/sw.js index dda6cf9..b6114eb 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v95'; +const CACHE = 'diora-v96'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From 077612e7bb6c7315352ed4ee9972067296fa0fbc Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Thu, 8 Oct 2026 18:41:56 +0200 Subject: [PATCH 093/176] =?UTF-8?q?B=C3=BCcher=20schneller=20=C3=B6ffnen:?= =?UTF-8?q?=20Kapitel=20parallel=20entpacken,=20vorbereitete=20B=C3=BCcher?= =?UTF-8?q?=20merken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemessen mit Das Kapital (148 Kapitel, 26 000 Absätze) im diora Reader, CPU 6-fach gedrosselt wie auf einem E-Reader: Öffnen 2,4 s → 1,2 s. - parseEpub entpackt alle Kapitel auf einmal statt eines nach dem anderen (jedes wartete auf JSZips eigene Ticks) und pausiert nach ~50 ms Arbeit statt alle 8 Kapitel einen Frame; ebenso das Einfügen der Kapitel - Vorbereitete Bücher (entpackt und bereinigt) liegen verschlüsselt mit dem Buchschlüssel in IndexedDB („parsed“): gültig für eine Fassung der Buchdatei (IV) und des Vorbereitungs-Codes (Hash seines Quelltexts – eine Änderung am Sanitizer bereitet jedes Buch neu vor); Bilder bleiben in der Buchdatei, die Kapitel tragen „diora-img:“ statt Blob-URLs. Nach 4 Wochen ohne Öffnen weg. Geprüft: Text, Absätze, Bilder, Fußnotenlinks, Inhaltsverzeichnis identisch. Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 134 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 121 insertions(+), 13 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index d12f1a5..98f2c43 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1815,11 +1815,21 @@ async function parseEpub(arrayBuffer) { .map(ref => manifest[ref.getAttribute('idref')]?.href) .filter(Boolean); + // All chapters unpacked at once: one after the other, every chapter waits for + // JSZip's own ticks (a book like Das Kapital has 148 of them). + const texts = await Promise.all(spineItems.map(href => { + const f = zip.file(href); + return f ? f.async('text').catch(() => null) : null; + })); + const chapters = []; + let slice = performance.now(); for (let i = 0; i < spineItems.length; i++) { const href = spineItems[i]; + if (texts[i] == null) continue; // missing try { - let chapterText = await zip.file(href).async('text'); + let chapterText = texts[i]; + texts[i] = null; // Strip style/script blocks early — publisher EPUBs often embed large base64 fonts in CSS. // This shrinks the string before regex and DOMParser work on it. chapterText = chapterText.replace(/]*>[\s\S]*?<\/style>/gi, ''); @@ -1828,8 +1838,12 @@ async function parseEpub(arrayBuffer) { const withBlobs = _injectImageBlobs(chapterText, chapterDir, imageMap); const sanitized = sanitizeEpubHtml(withBlobs); chapters.push(`
${sanitized}
`); - if (i % 8 === 0) await new Promise(r => requestAnimationFrame(r)); - } catch (e) { /* skip missing */ } + // Let the page breathe (spinner, taps) every ~50 ms of work, not after a fixed count. + if (performance.now() - slice > 50) { + await new Promise(r => setTimeout(r, 0)); + slice = performance.now(); + } + } catch (e) { /* skip unreadable */ } } return {title, author, chapters, toc, imageMap}; @@ -2208,11 +2222,12 @@ function _cmpProgress(anchorA, fracA, anchorB, fracB) { const _BOOK_CACHE_STORE = 'books'; const _BOOK_META_STORE = 'book_meta'; const _BOOK_ANNOTATIONS_STORE = 'book_annotations'; +const _BOOK_PARSED_STORE = 'parsed'; // prepared chapters, see _getParsedBook const _BOOK_CACHE_EVICT_MS = 4 * 7 * 24 * 60 * 60 * 1000; function _openBookCacheDb() { return new Promise((resolve, reject) => { - const req = indexedDB.open('diora_book_cache', 2); + const req = indexedDB.open('diora_book_cache', 3); req.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains(_BOOK_CACHE_STORE)) @@ -2221,12 +2236,98 @@ function _openBookCacheDb() { db.createObjectStore(_BOOK_META_STORE, {keyPath: 'id'}); if (!db.objectStoreNames.contains(_BOOK_ANNOTATIONS_STORE)) db.createObjectStore(_BOOK_ANNOTATIONS_STORE, {keyPath: 'id'}); + if (!db.objectStoreNames.contains(_BOOK_PARSED_STORE)) + db.createObjectStore(_BOOK_PARSED_STORE, {keyPath: 'id'}); }; req.onsuccess = e => resolve(e.target.result); req.onerror = () => reject(); }); } +// --------------------------------------------------------------------------- +// Prepared books: an EPUB's chapters after unpacking and sanitizing, so opening +// it again skips both (for a book like Das Kapital that's most of the time an +// e-reader needs to open it). Encrypted with the book key like everything else +// on the device. Valid for one version of the book file (its IV) and of the +// code that prepares chapters (a hash of its source: changing the sanitizer +// makes every book be prepared anew). Images stay in the book file: the +// chapters keep "diora-img:" in place of the session's blob URLs. +// --------------------------------------------------------------------------- +let _parseCodeSig = null; + +function _parsePreparationSig() { + if (_parseCodeSig) return _parseCodeSig; + const src = [parseEpub, sanitizeEpubHtml, _injectImageBlobs, _resolveImageBlob, _parseEpubToc] + .map(f => String(f)).join('\n'); + let h = 5381; + for (let i = 0; i < src.length; i++) h = ((h << 5) + h + src.charCodeAt(i)) | 0; + return (_parseCodeSig = (h >>> 0).toString(36) + '.' + src.length); +} + +async function _getParsedBook(bookId, dataIv, key) { + try { + const db = await _openBookCacheDb(); + const row = await new Promise(resolve => { + const req = db.transaction(_BOOK_PARSED_STORE).objectStore(_BOOK_PARSED_STORE).get(bookId); + req.onsuccess = e => resolve(e.target.result || null); + req.onerror = () => resolve(null); + }); + if (!row || row.sig !== dataIv + ':' + _parsePreparationSig()) return null; + const plain = await crypto.subtle.decrypt({name: 'AES-GCM', iv: row.iv}, key, row.ct); + return JSON.parse(new TextDecoder().decode(plain)); + } catch (e) { return null; } +} + +async function _setParsedBook(bookId, dataIv, key, result) { + try { + // Blob URLs live as long as this page; the stored chapters name the image instead. + const pathOf = {}; + for (const [path, url] of Object.entries(result.imageMap || {})) pathOf[url] = path; + const blobRe = /blob:[^"'\s)]+/g; + const chapters = result.chapters.map(c => c.replace(blobRe, u => pathOf[u] ? 'diora-img:' + pathOf[u] : u)); + const doc = {title: result.title, author: result.author, toc: result.toc, chapters, + images: Object.keys(result.imageMap || {})}; + // Raw bytes, not base64: IndexedDB keeps an ArrayBuffer as it is (megabytes for a big book). + const iv = crypto.getRandomValues(new Uint8Array(12)); + const ct = await crypto.subtle.encrypt({name: 'AES-GCM', iv}, key, new TextEncoder().encode(JSON.stringify(doc))); + const db = await _openBookCacheDb(); + await new Promise(resolve => { + const tx = db.transaction(_BOOK_PARSED_STORE, 'readwrite'); + const store = tx.objectStore(_BOOK_PARSED_STORE); + store.put({id: bookId, sig: dataIv + ':' + _parsePreparationSig(), iv, ct, cached_at: Date.now()}); + // Prepared books not opened for a while go (they come back on the next open). + const all = store.getAll(); + all.onsuccess = () => all.result.forEach(r => { + if (r.id !== bookId && Date.now() - (r.cached_at || 0) > _BOOK_CACHE_EVICT_MS) store.delete(r.id); + }); + tx.oncomplete = resolve; + tx.onerror = resolve; + }); + } catch (e) {} +} + +/** A prepared book back to what parseEpub returns: fresh blob URLs for its images. */ +async function _restoreParsedBook(doc, epubBytes) { + const imageMap = {}; + if (doc.images && doc.images.length) { + const zip = await JSZip.loadAsync(epubBytes); + await Promise.all(doc.images.map(async path => { + const f = zip.file(path); + if (!f) return; + try { + const buf = await f.async('arraybuffer'); + const ext = path.split('.').pop().toLowerCase(); + const type = {jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', + svg: 'image/svg+xml', webp: 'image/webp'}[ext] || ''; + imageMap[path] = URL.createObjectURL(new Blob([buf], type ? {type} : {})); + } catch (e) {} + })); + } + const placeholder = /diora-img:([^"'\s)]+)/g; + const chapters = doc.chapters.map(c => c.replace(placeholder, (m, path) => imageMap[path] || '')); + return {title: doc.title, author: doc.author, toc: doc.toc, chapters, imageMap}; +} + async function _getCachedBook(bookId) { try { const db = await _openBookCacheDb(); @@ -3740,22 +3841,29 @@ async function _openBookOnce(bookId) { currentPdfBuffer = plain; } else { currentPdfBuffer = null; - const result = await parseEpub(plain); + // Prepared before (same book file, same preparation code): only the images to unpack. + const prepared = await _getParsedBook(bookId, data_iv, key); + const result = prepared ? await _restoreParsedBook(prepared, plain) : await parseEpub(plain); + if (!prepared) { + // Kept for the next open once the book is on screen (encrypting takes a moment). + setTimeout(() => _setParsedBook(bookId, data_iv, key, result), 3000); + } title = result.title || title; author = result.author || author; toc = result.toc; currentImageMap = result.imageMap; - // Inject chapters progressively so the browser stays responsive on large books - const BATCH = 8; - for (let i = 0; i < result.chapters.length; i += BATCH) { + // Inject chapters progressively so the browser stays responsive on large books: + // a batch per ~50 ms of work (a fixed count waited a frame every 8 chapters). + const tmp = document.createElement('div'); + for (let i = 0; i < result.chapters.length;) { const frag = document.createDocumentFragment(); - const tmp = document.createElement('div'); - for (let j = i; j < Math.min(i + BATCH, result.chapters.length); j++) { - tmp.innerHTML = result.chapters[j]; + const start = performance.now(); + do { + tmp.innerHTML = result.chapters[i++]; while (tmp.firstChild) frag.appendChild(tmp.firstChild); - } + } while (i < result.chapters.length && performance.now() - start < 50); contentEl.appendChild(frag); - await new Promise(r => requestAnimationFrame(r)); + if (i < result.chapters.length) await new Promise(r => setTimeout(r, 0)); } // Used to decide whole-percent vs one-decimal display further down — // see _formatBookPercent(). From 3f2e309c9ce8da86a8876e351d3bc4fca3db54ff Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Thu, 8 Oct 2026 18:51:05 +0200 Subject: [PATCH 094/176] =?UTF-8?q?Reader:=20Fu=C3=9Fnoten=20und=20Seitenz?= =?UTF-8?q?ahlen=20in=20Verlags-EPUBs;=20sw=20v96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - (XHTML) blieb beim Parsen als HTML offen und umschloss den folgenden Text, in jedem weiteren Absatz neu (Suhrkamp/transpect): die inline-Elemente werden vorher geschlossen. - Seitenzahlen der Druckausgabe (epub:type="pagebreak") als kleine graue Marke statt am Text klebend („3Anton Jäger“). - Fußnoten-Popover: Absatz der Notiz auch bei id auf einem inline-Element, ohne eigene Nummer und Seitenzahl, mehrabsätzige Noten (Gedichte). Co-Authored-By: Claude Opus 5.5 --- static/css/app.css | 4 ++++ static/js/app.js | 36 +++++++++++++++++++++++++++++++++++- static/js/sw.js | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/static/css/app.css b/static/css/app.css index e6d8b7b..a2b225f 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1814,6 +1814,10 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h) .footnote-popover.reader-theme-bright { background:#f0f0f0; color:#111; border-color:#bbb; } .footnote-popover-close { position:absolute; top:4px; right:6px; background:none; border:none; color:inherit; opacity:0.5; cursor:pointer; font-size:14px; padding:2px 4px; line-height:1; } +/* Page numbers of the printed edition (epub:type="pagebreak") */ +.reader-content .epub-pagenum { display:inline-block; font-size:0.62em; line-height:1.3; font-weight:400; font-style:normal; letter-spacing:0; text-transform:none; vertical-align:0.35em; padding:0 0.35em; margin-right:0.4em; border:1px solid currentColor; border-radius:3px; opacity:0.45; user-select:none; } +.reader-content .epub-pagenum:empty { display:none; } + /* Highlight marks */ .reader-no-bold * { font-weight: inherit !important; } .reader-content img { max-width: 100%; height: auto; display: block; } diff --git a/static/js/app.js b/static/js/app.js index d12f1a5..b5d9891 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1942,7 +1942,16 @@ function _looksLikeFootnoteLink(el, href) { return !!(el.closest('sup') || el.querySelector('sup')); } +// Chapters are XHTML, but parsed as HTML (forgiving with broken books). HTML +// knows no self-closing : the anchor stays open and swallows the +// text after it — and being a formatting element it is re-opened in every +// following paragraph (Suhrkamp/transpect books put one in each heading and +// note). Close the inline ones; block elements are left alone so the block +// indexes that highlights are anchored to stay the same. +const _SELF_CLOSING_INLINE = /<(a|span|b|i|em|strong|sup|sub|small|u|s|code|font|abbr|cite|q|mark|var)(\s(?:[^<>"']|"[^"]*"|'[^']*')*)?\/>/gi; + function sanitizeEpubHtml(html) { + html = html.replace(_SELF_CLOSING_INLINE, '<$1$2>'); const doc = new DOMParser().parseFromString(html, 'text/html'); doc.querySelectorAll('script, iframe, object, embed, style, head, meta, link').forEach(el => el.remove()); @@ -1957,6 +1966,11 @@ function sanitizeEpubHtml(html) { // Remove any non-blob, non-data src that slipped through (broken relative paths) if (src && !src.startsWith('blob:') && !src.startsWith('data:')) el.removeAttribute('src'); } + // The printed edition's page numbers ("[12]") as small marks, not glued to the text. + const etype = el.getAttribute('epub:type') || ''; + if (/\bpagebreak\b/.test(etype) || el.getAttribute('role') === 'doc-pagebreak') { + el.classList.add('epub-pagenum'); + } if (el.tagName === 'A') { const href = el.getAttribute('href') || ''; if (href.startsWith('http://') || href.startsWith('https://')) { @@ -6021,11 +6035,31 @@ function showFootnotePopover(link) { dismissFootnotePopover(); const targetEl = document.getElementById(link.dataset.footnoteRef); if (!targetEl) return; - const container = targetEl.closest('.footnote') || targetEl.parentElement; + // The note's paragraph, also when the id sits on an inline element in it + // (

143 Note…

). + const inline = /^(A|SPAN|SUP|SUB|B|I|EM|STRONG|SMALL)$/.test(targetEl.tagName); + const container = targetEl.closest('.footnote') || + (inline && targetEl.closest('p, li, dd, blockquote')) || targetEl.parentElement; if (!container) return; const clone = container.cloneNode(true); clone.querySelector('sup')?.remove(); + clone.querySelectorAll('.epub-pagenum').forEach(e => e.remove()); + // The note's own number at its start, a link back to the text ("1 Note…"). + const first = Array.from(clone.querySelectorAll('a')).find(a => a.textContent.trim()); + const num = first ? first.textContent.trim() : ''; + if (/^[\[(]?\d{1,4}[\])]?\.?$/.test(num) && clone.textContent.trim().startsWith(num)) first.remove(); + // Only the number: the note (a poem, a long quote) runs on in the next + // paragraphs, up to the next note's anchor. + if (!clone.textContent.trim()) { + let len = 0; + for (let next = container.nextElementSibling; next && len < 6000; next = next.nextElementSibling) { + if (/^H[1-6]$/.test(next.tagName) || next.querySelector('[id]')) break; + if (!next.textContent.trim()) continue; + clone.appendChild(next.cloneNode(true)); + len += next.textContent.length; + } + } const popover = document.createElement('div'); popover.className = 'footnote-popover'; diff --git a/static/js/sw.js b/static/js/sw.js index dda6cf9..b6114eb 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v95'; +const CACHE = 'diora-v96'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From 3f2fcec3b87665498925a5cc17bd4d5cca91c36d Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Thu, 8 Oct 2026 18:52:16 +0200 Subject: [PATCH 095/176] =?UTF-8?q?sw=20v97=20(Fu=C3=9Fnoten-Fix=20und=20s?= =?UTF-8?q?chnelleres=20=C3=96ffnen=20nach=20v96=20aus=20aa4cf23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- static/js/sw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/sw.js b/static/js/sw.js index b6114eb..fba4890 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v96'; +const CACHE = 'diora-v97'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From 53ca3b00024188476a3addd85d1e84c82da79768 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Thu, 8 Oct 2026 19:01:40 +0200 Subject: [PATCH 096/176] =?UTF-8?q?Leseposition:=20Bin=C3=A4rsuche=20?= =?UTF-8?q?=C3=BCber=20die=20Bl=C3=B6cke=20statt=20alle=20vom=20Buchanfang?= =?UTF-8?q?=20an=20messen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getPositionAnchor maß jeden Block vom Buchanfang bis zur Leseposition und suchte die Blöcke bei jedem Aufruf neu – im E-Reader-Modus bei jedem Umblättern (die Prozentanzeige), beim Speichern des Fortschritts sowieso. In Das Kapital ab Kapitel 15: 0,8 s bei 6-fach gedrosselter CPU; auf dem tolino filtert compat61 das :has() dazu in JavaScript. - Blockliste samt Kapitel je Block einmal je geöffnetem Buch (verworfen bei jeder Änderung am Inhalt, MutationObserver) - Binärsuche nur über die angezeigten Kapitel; gleiche Regel wie vorher (letzter Block mit Höhe, der an/über der Oberkante beginnt, bei gleicher Höhe der erste; sonst der erste darunter) - Geprüft an 480 Stellen (Kapital, Hyperpolitik; E-Reader und Scrollen): gleiche Anker wie vorher; 53/281 ms → 4 ms; Umblättern 21–48 ms statt bis 0,9 s - sw v98 Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 88 +++++++++++++++++++++++++++++++++++++++++------- static/js/sw.js | 2 +- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index fe12f31..a094a04 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2120,26 +2120,88 @@ function _checkScrollJump(contentEl) { const EPUB_BLOCK_SELECTOR = 'p, h1, h2, h3, h4, h5, h6, li, blockquote, dt, dd, figcaption, div:not(:has(*))'; -function getPositionAnchor(contentEl) { +// The open book's text blocks (EPUB_BLOCK_SELECTOR), with the chapter each one +// belongs to — found once and kept until the book's content changes (a +// highlight wrapping text can turn a leaf div into a non-leaf one). Selecting +// them is the expensive part on an e-reader: Chrome 61 has no :has(), so +// compat61.js filters every div in JavaScript. +let _blockCache = null; // {content, blocks, chapterOf: Int32Array, chapters: Element[]} + +function _epubBlocks(contentEl) { + if (_blockCache && _blockCache.content === contentEl) return _blockCache; const blocks = Array.from(contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR)); + const chapters = Array.from(contentEl.querySelectorAll('[data-epub-src]')); + const chapterOf = new Int32Array(blocks.length); + for (let i = 0, ch = -1; i < blocks.length; i++) { + while (ch + 1 < chapters.length && chapters[ch + 1].compareDocumentPosition(blocks[i]) & Node.DOCUMENT_POSITION_FOLLOWING) ch++; + chapterOf[i] = ch; + } + const observer = new MutationObserver(() => { + if (_blockCache && _blockCache.content === contentEl) _blockCache = null; + observer.disconnect(); + }); + observer.observe(contentEl, {childList: true, subtree: true}); + _blockCache = {content: contentEl, blocks, chapterOf, chapters}; + return _blockCache; +} + +// The anchor of the reading position: "block:inner" — the last block (with a +// height) that starts at or above the top of the view, the first of several +// starting at the same height; else the first block below. Blocks are in +// document order and so top to bottom: a binary search over the chapters on +// screen (the e-reader mode shows one), not every block from the book's start. +function getPositionAnchor(contentEl) { + const {blocks, chapterOf, chapters} = _epubBlocks(contentEl); if (!blocks.length) return ''; const containerTop = contentEl.getBoundingClientRect().top; const containerBottom = containerTop + contentEl.clientHeight; - let bestIndex = 0; - let bestDelta = Infinity; + // Index ranges of the blocks in chapters that are laid out (all, when reading by scrolling). + const shown = new Set(); + chapters.forEach((ch, i) => { if (ch.getClientRects().length) shown.add(i); }); + const order = []; for (let i = 0; i < blocks.length; i++) { - const rect = blocks[i].getBoundingClientRect(); - if (rect.height < 1) continue; - if (rect.top > containerBottom) break; - const delta = rect.top - containerTop; - if (delta <= 0 && Math.abs(delta) < Math.abs(bestDelta)) { - bestIndex = i; - bestDelta = delta; - } else if (delta > 0 && bestDelta === Infinity) { - bestIndex = i; - bestDelta = delta; + if (chapterOf[i] < 0 || shown.has(chapterOf[i])) { + let j = i; + while (j + 1 < blocks.length && chapterOf[j + 1] === chapterOf[i]) j++; + order.push([i, j]); + i = j; } } + const count = order.reduce((n, r) => n + r[1] - r[0] + 1, 0); + const at = k => { // k-th block on screen -> block index + for (const [a, b] of order) { if (k <= b - a) return a + k; k -= b - a + 1; } + return -1; + }; + const rectOf = k => blocks[at(k)].getBoundingClientRect(); + // A block without height says nothing about the position: use the next one with a height. + const measured = k => { + for (let n = k; n < count; n++) { const r = rectOf(n); if (r.height >= 1) return {k: n, top: r.top}; } + return null; + }; + // Binary search for the last block with a height whose top <= containerTop. + let lo = 0, hi = count - 1, best = -1; + while (lo <= hi) { + const mid = (lo + hi) >> 1; + const m = measured(mid); + if (m && m.top <= containerTop) { best = m.k; lo = m.k + 1; } + else hi = mid - 1; + } + let bestIndex; + if (best >= 0) { + // Several blocks starting at the same height (a blockquote and its first paragraph): the first. + const top = rectOf(best).top; + let k = best; + for (let n = best - 1; n >= 0; n--) { + const r = rectOf(n); + if (r.height < 1) continue; + if (r.top !== top) break; + k = n; + } + bestIndex = at(k); + } else { + const first = measured(0); + bestIndex = first && first.top <= containerBottom ? at(first.k) : 0; + } const rect = blocks[bestIndex].getBoundingClientRect(); const innerFraction = Math.max(0, Math.min(1, (containerTop - rect.top) / (rect.height || 1))); return `${bestIndex}:${innerFraction.toFixed(6)}`; diff --git a/static/js/sw.js b/static/js/sw.js index fba4890..853f5fb 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v97'; +const CACHE = 'diora-v98'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From fda6c0194351821eb5c7f4c94aa5fa43c11e6dd5 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Thu, 8 Oct 2026 19:22:04 +0200 Subject: [PATCH 097/176] Einstellungen: Cloud-Verbindung bearbeiten (Name, URL, Benutzer, Unterordner, Passwort); sw v99 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher ließ sich eine Verbindung nur anlegen und löschen, der Startordner also nicht nachträglich setzen. Das Passwort wird nicht angezeigt; leer lassen behält das gespeicherte. Co-Authored-By: Claude Opus 5.5 --- accounts/tests.py | 48 ++++++++++++++++++++++++++++++++ accounts/urls.py | 1 + accounts/views.py | 29 +++++++++++++++++++ static/css/app.css | 9 ++++++ static/js/sw.js | 2 +- templates/accounts/settings.html | 27 ++++++++++++++++++ 6 files changed, 115 insertions(+), 1 deletion(-) diff --git a/accounts/tests.py b/accounts/tests.py index 35d015e..8eb3201 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -546,3 +546,51 @@ class CloudFetchCachingTests(TestCase): with patch('books.webdav.fetch_file', return_value=b'EPUB-BYTES'): resp = self.client.get(f'/books/cloud/{self.source.pk}/fetch/?path=Dune.epub') self.assertEqual(resp['Cache-Control'], 'no-store') + + +class WebDAVEditTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.other = User.objects.create_user(username='bob', password='pw12345678') + self.source = WebDAVSource.objects.create( + user=self.user, label='cloud', base_url='https://cloud.example.com', + username='alice', password='app-pw', + ) + + def _edit(self, pk, **data): + form = {'label': 'cloud', 'base_url': 'https://cloud.example.com', 'username': 'alice', 'root_path': ''} + form.update(data) + with patch('accounts.views.list_directory', return_value=[]): + return self.client.post(f'/accounts/webdav/{pk}/edit/', form) + + def test_sets_start_folder_and_keeps_password(self): + self.client.force_login(self.user) + resp = self._edit(self.source.pk, root_path='Books', password='') + self.assertEqual(resp.status_code, 302) + self.source.refresh_from_db() + self.assertEqual(self.source.root_path, 'Books') + self.assertEqual(self.source.password, 'app-pw') + + def test_new_password_replaces_the_old_one(self): + self.client.force_login(self.user) + self._edit(self.source.pk, password='new-pw') + self.source.refresh_from_db() + self.assertEqual(self.source.password, 'new-pw') + + def test_name_and_url_are_required(self): + self.client.force_login(self.user) + self._edit(self.source.pk, label='', root_path='Books') + self.source.refresh_from_db() + self.assertEqual(self.source.root_path, '') + + def test_foreign_source_is_not_editable(self): + self.client.force_login(self.other) + self._edit(self.source.pk, root_path='x') + self.source.refresh_from_db() + self.assertEqual(self.source.root_path, '') + + def test_settings_page_does_not_show_the_password(self): + self.client.force_login(self.user) + html = self.client.get('/accounts/settings/').content.decode() + self.assertIn(f'/accounts/webdav/{self.source.pk}/edit/', html) + self.assertNotIn('app-pw', html) diff --git a/accounts/urls.py b/accounts/urls.py index 4bee71f..f569262 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -17,6 +17,7 @@ urlpatterns = [ path('change-password/', views.change_password, name='change_password'), path('api-token/regenerate/', views.regenerate_api_token, name='regenerate_api_token'), path('webdav/add/', views.webdav_add, name='webdav_add'), + path('webdav//edit/', views.webdav_edit, name='webdav_edit'), path('webdav//test/', views.webdav_test, name='webdav_test'), path('webdav//delete/', views.webdav_delete, name='webdav_delete'), ] diff --git a/accounts/views.py b/accounts/views.py index 30878ff..78ee9d7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -290,6 +290,35 @@ def webdav_add(request): return redirect('settings') +@login_required +@require_http_methods(['POST']) +def webdav_edit(request, pk): + source = WebDAVSource.objects.filter(pk=pk, user=request.user).first() + if not source: + messages.error(request, 'Verbindung nicht gefunden.') + return redirect('settings') + + label = request.POST.get('label', '').strip() + base_url = request.POST.get('base_url', '').strip() + if not label or not base_url: + messages.error(request, 'Name und Server-URL sind erforderlich.') + return redirect('settings') + + source.label = label[:100] + source.base_url = base_url[:500] + source.username = request.POST.get('username', '').strip()[:200] + source.root_path = request.POST.get('root_path', '').strip()[:500] + # The stored password is never sent to the page: empty means keep it. + password = request.POST.get('password', '') + if password: + source.password = password[:500] + source.save() + + level, message = _probe_source(source) + messages.add_message(request, level, message) + return redirect('settings') + + @login_required @require_http_methods(['POST']) def webdav_test(request, pk): diff --git a/static/css/app.css b/static/css/app.css index a2b225f..36e4a4b 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -826,6 +826,15 @@ a:hover { align-self: flex-start; } +/* Editing a saved connection: a full row under its name and buttons */ +.webdav-edit { + flex-basis: 100%; + padding-top: 0.75rem; +} +.webdav-edit[hidden] { + display: none; +} + /* --- Cloud import browser (books tab) --- */ .cloud-browser { diff --git a/static/js/sw.js b/static/js/sw.js index 853f5fb..4d369e7 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v98'; +const CACHE = 'diora-v99'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', diff --git a/templates/accounts/settings.html b/templates/accounts/settings.html index 702cbcf..c2d17f7 100644 --- a/templates/accounts/settings.html +++ b/templates/accounts/settings.html @@ -150,6 +150,8 @@ {{ source.base_url }}{% if source.root_path %} · /{{ source.root_path }}{% endif %}
+
{% csrf_token %} @@ -160,6 +162,31 @@
+ {% endfor %} From 60dd4926cff2f16936a4891fb466a9afc0a0ce8d Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Mon, 12 Oct 2026 19:31:36 +0200 Subject: [PATCH 098/176] Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop. - Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht. Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben. /books/ mit X-Server-Now, /api/sync/ mit server_now. - Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert. Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat. Angebote "Auf weitergelesen" und "Weiteste Stelle". - Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste Stelle, Liste und Snapshot). Co-Authored-By: Claude Opus 5.5 --- accounts/sync.py | 12 +- accounts/tests.py | 2 +- .../0005_progress_last_read_wins.py | 43 ++ books/models.py | 7 + books/tests.py | 162 ++++++++ books/views.py | 114 +++-- docs/progress-sync.md | 71 ++++ static/js/app.js | 391 ++++++++++++++---- static/js/sw.js | 2 +- 9 files changed, 693 insertions(+), 111 deletions(-) create mode 100644 books/migrations/0005_progress_last_read_wins.py create mode 100644 books/tests.py create mode 100644 docs/progress-sync.md diff --git a/accounts/sync.py b/accounts/sync.py index 5dccfb3..38a17d6 100644 --- a/accounts/sync.py +++ b/accounts/sync.py @@ -4,6 +4,7 @@ from django.utils import timezone from django.views.decorators.http import require_http_methods from books.models import EBook, EBookProgress, EBookHighlights, EBookBookmarks +from books.views import _now_ms, progress_state from podcasts.models import PodcastFeed, EpisodeProgress, PodcastQueue from radio.models import SavedStation @@ -33,11 +34,10 @@ def sync_snapshot(request): for b in books: b['uploaded_at'] = b['uploaded_at'].isoformat() - book_progress = list( - EBookProgress.objects.filter(user=user).values('book_id', 'scroll_fraction', 'position_anchor', 'updated_at') - ) - for p in book_progress: - p['updated_at'] = p['updated_at'].isoformat() + book_progress = [ + {'book_id': p.book_id, 'updated_at': p.updated_at.isoformat(), **progress_state(p)} + for p in EBookProgress.objects.filter(user=user) + ] book_highlights = list(EBookHighlights.objects.filter(user=user).values('book_id', 'ct', 'iv', 'updated_at')) for h in book_highlights: @@ -50,6 +50,7 @@ def sync_snapshot(request): if request.GET.get('only') == 'books': return JsonResponse({ 'server_time': timezone.now().isoformat(), + 'server_now': _now_ms(), 'books': books, 'book_progress': book_progress, 'book_highlights': book_highlights, @@ -84,6 +85,7 @@ def sync_snapshot(request): return JsonResponse({ 'server_time': timezone.now().isoformat(), + 'server_now': _now_ms(), 'books': books, 'book_progress': book_progress, 'book_highlights': book_highlights, diff --git a/accounts/tests.py b/accounts/tests.py index 8eb3201..6a068b8 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -88,7 +88,7 @@ class SyncSnapshotTests(TestCase): def test_only_books(self): resp = self.client.get('/api/sync/?only=books', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') data = resp.json() - self.assertEqual(set(data), {'server_time', 'books', 'book_progress', 'book_highlights', 'book_bookmarks'}) + self.assertEqual(set(data), {'server_time', 'server_now', 'books', 'book_progress', 'book_highlights', 'book_bookmarks'}) self.assertEqual(data['book_highlights'][0]['ct'], 'hl-ct') def test_snapshot_shape_and_data(self): diff --git a/books/migrations/0005_progress_last_read_wins.py b/books/migrations/0005_progress_last_read_wins.py new file mode 100644 index 0000000..baf8ff9 --- /dev/null +++ b/books/migrations/0005_progress_last_read_wins.py @@ -0,0 +1,43 @@ +from django.db import migrations, models + + +def keep_what_we_know(apps, schema_editor): + """The continue point so far was the furthest place: it is that, and it was + last saved at updated_at.""" + EBookProgress = apps.get_model('books', 'EBookProgress') + for p in EBookProgress.objects.all().iterator(): + p.furthest_fraction = p.scroll_fraction + p.furthest_anchor = p.position_anchor + p.read_at = int(p.updated_at.timestamp() * 1000) + p.save(update_fields=['furthest_fraction', 'furthest_anchor', 'read_at']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('books', '0004_ebook_is_read'), + ] + + operations = [ + migrations.AddField( + model_name='ebookprogress', + name='read_at', + field=models.BigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='ebookprogress', + name='device', + field=models.CharField(blank=True, default='', max_length=40), + ), + migrations.AddField( + model_name='ebookprogress', + name='furthest_fraction', + field=models.FloatField(default=0.0), + ), + migrations.AddField( + model_name='ebookprogress', + name='furthest_anchor', + field=models.CharField(blank=True, default='', max_length=30), + ), + migrations.RunPython(keep_what_we_know, migrations.RunPython.noop), + ] diff --git a/books/models.py b/books/models.py index 9bc06c7..ee1a91e 100644 --- a/books/models.py +++ b/books/models.py @@ -19,10 +19,17 @@ class EBook(models.Model): class EBookProgress(models.Model): + """Where to continue a book: the place it was last read, on any device + (docs/progress-sync.md), plus the furthest place ever reached.""" user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ebook_progress') book = models.ForeignKey(EBook, on_delete=models.CASCADE, related_name='progress') scroll_fraction = models.FloatField(default=0.0) position_anchor = models.CharField(max_length=30, blank=True, default='') + # When the reader was at that place (ms since epoch, server clock); null before 2026-10-12. + read_at = models.BigIntegerField(null=True, blank=True) + device = models.CharField(max_length=40, blank=True, default='') + furthest_fraction = models.FloatField(default=0.0) + furthest_anchor = models.CharField(max_length=30, blank=True, default='') updated_at = models.DateTimeField(auto_now=True) class Meta: diff --git a/books/tests.py b/books/tests.py new file mode 100644 index 0000000..93d9ac4 --- /dev/null +++ b/books/tests.py @@ -0,0 +1,162 @@ +"""The reading position: the place last read wins (docs/progress-sync.md).""" +import json +from unittest.mock import patch + +from django.contrib.auth.models import User +from django.test import TestCase + +from accounts.models import ApiToken +from .models import EBook, EBookProgress + +NOW = 1_760_270_000_000 # the server's clock in these tests (ms) +MIN = 60_000 + + +class ProgressTests(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='alice', password='pw12345678') + self.auth = {'HTTP_AUTHORIZATION': f'Bearer {ApiToken.objects.create(user=self.user).token}'} + self.book = EBook.objects.create(user=self.user, meta_ct='m', meta_iv='i', data_ct='d', data_iv='i') + self.now = NOW + clock = patch('books.views._now_ms', side_effect=lambda: self.now) + clock.start() + self.addCleanup(clock.stop) + + def save(self, anchor, frac=None, read_at=None, sent_at=None, device='Handy', **extra): + body = {'scroll_fraction': frac if frac is not None else 0.1, 'position_anchor': anchor, **extra} + if read_at is not None: + body.update(read_at=read_at, sent_at=sent_at if sent_at is not None else self.now, device=device) + resp = self.client.post(f'/books/{self.book.id}/progress/', json.dumps(body), + content_type='application/json', **self.auth) + self.assertEqual(resp.status_code, 200) + return resp.json() + + def state(self): + return EBookProgress.objects.get(book=self.book) + + # -- the new rule + + def test_newer_place_wins_even_if_it_is_earlier_in_the_book(self): + self.save('300:0.5', read_at=NOW - 10 * MIN, device='Handy') + r = self.save('12:0.0', read_at=NOW - 1 * MIN, device='diora Desktop') + self.assertTrue(r['stored']) + p = self.state() + self.assertEqual((p.position_anchor, p.device, p.read_at), ('12:0.0', 'diora Desktop', NOW - MIN)) + self.assertEqual(p.furthest_anchor, '300:0.5') + + def test_an_older_write_arriving_late_does_not_win(self): + # The phone read offline at 10:00 and syncs at 12:00; the desktop read at 11:00. + self.save('40:0.0', read_at=NOW - 60 * MIN, device='diora Desktop') + r = self.save('90:0.0', read_at=NOW - 120 * MIN, device='Handy') + self.assertFalse(r['stored']) + self.assertEqual(r['position_anchor'], '40:0.0') + p = self.state() + self.assertEqual((p.position_anchor, p.device), ('40:0.0', 'diora Desktop')) + self.assertEqual(p.furthest_anchor, '90:0.0', 'the late write still counts as reached') + + def test_resaving_an_unchanged_place_keeps_its_time_and_cannot_win(self): + self.save('50:0.0', read_at=NOW - 5 * MIN, device='Handy') + self.save('70:0.0', read_at=NOW - 2 * MIN, device='diora Desktop') + # The phone, idle since, saves its place again on going to the background. + self.now += MIN + r = self.save('50:0.0', read_at=NOW - 5 * MIN, device='Handy') + self.assertFalse(r['stored']) + self.assertEqual(self.state().position_anchor, '70:0.0') + + def test_the_device_clock_error_is_taken_out(self): + # A tolino 3 hours behind: it read one minute ago by its clock, and sends now by its clock. + behind = 3 * 60 * MIN + self.save('10:0.0', read_at=NOW - 30 * MIN, device='Handy') + r = self.save('20:0.0', read_at=NOW - behind - MIN, sent_at=NOW - behind, device='tolino') + self.assertTrue(r['stored']) + self.assertEqual(self.state().read_at, NOW - MIN) + + def test_nothing_wins_from_the_future(self): + self.save('10:0.0', read_at=NOW + 24 * 60 * MIN, sent_at=NOW) + self.assertEqual(self.state().read_at, NOW) + self.now += MIN + self.assertTrue(self.save('11:0.0', read_at=self.now, sent_at=self.now)['stored']) + + def test_equal_times_take_the_new_write(self): + self.save('10:0.0', read_at=NOW - MIN) + self.assertTrue(self.save('11:0.0', read_at=NOW - MIN)['stored']) + + def test_force_sets_the_place_now(self): + self.save('300:0.0', read_at=NOW - MIN) + self.assertTrue(self.save('0:0.0', frac=0.0, force=True)['stored']) + p = self.state() + self.assertEqual((p.position_anchor, p.read_at), ('0:0.0', NOW)) + self.assertEqual(p.furthest_anchor, '300:0.0') + + def test_furthest_only_grows(self): + self.save('80:0.0', read_at=NOW - 3 * MIN) + self.save('20:0.0', read_at=NOW - 2 * MIN) + self.save('81:0.0', read_at=NOW - 9 * MIN) + p = self.state() + self.assertEqual((p.position_anchor, p.furthest_anchor), ('20:0.0', '81:0.0')) + + def test_pdfs_compare_by_fraction(self): + self.save('', frac=0.7, read_at=NOW - 2 * MIN) + self.save('', frac=0.3, read_at=NOW - MIN) + p = self.state() + self.assertEqual((p.scroll_fraction, p.furthest_fraction), (0.3, 0.7)) + + def test_response_is_the_state_after_the_write(self): + r = self.save('5:0.25', frac=0.05, read_at=NOW - MIN, device='Browser') + self.assertEqual(r['server_now'], NOW) + for key in ('scroll_fraction', 'position_anchor', 'read_at', 'device', + 'furthest_fraction', 'furthest_anchor', 'last_read'): + self.assertIn(key, r) + self.assertEqual(r['read_at'], NOW - MIN) + + def test_implausible_times_count_as_an_old_app(self): + self.save('10:0.0', read_at=NOW - MIN) + # read_at=True or seconds instead of ms are not times: only forward, like an old app. + self.assertFalse(self.save('5:0.0', read_at=True)['stored']) + self.assertFalse(self.save('5:0.0', read_at=1_760_270_000)['stored']) + + # -- apps from before the change (no read_at) + + def test_old_apps_only_move_forward(self): + self.save('50:0.0', read_at=NOW - MIN) + self.assertFalse(self.save('20:0.0')['stored']) + self.assertTrue(self.save('60:0.0')['stored']) + self.assertEqual(self.state().read_at, NOW) + + def test_old_apps_resaving_the_same_place_change_nothing(self): + self.save('50:0.0', read_at=NOW - 5 * MIN, device='Handy') + self.now += MIN + self.assertFalse(self.save('50:0.0')['stored']) + p = self.state() + self.assertEqual((p.read_at, p.device), (NOW - 5 * MIN, 'Handy')) + + def test_first_write_of_an_old_app_is_stored(self): + self.assertTrue(self.save('0:0.0', frac=0.0)['stored']) + + def test_old_force_still_works(self): + self.save('50:0.0', read_at=NOW - MIN) + self.assertTrue(self.save('1:0.0', force=True)['stored']) + + # -- reading + + def test_book_list_and_snapshot_carry_the_state(self): + self.save('7:0.5', frac=0.2, read_at=NOW - MIN, device='Browser') + resp = self.client.get('/books/', **self.auth) + b = resp.json()[0] + self.assertEqual(resp['X-Server-Now'], str(NOW)) + self.assertEqual((b['position_anchor'], b['read_at'], b['device'], b['furthest_anchor']), + ('7:0.5', NOW - MIN, 'Browser', '7:0.5')) + self.assertTrue(b['last_read'].startswith('2025-10-12T')) + snap = self.client.get('/api/sync/?only=books', **self.auth).json() + p = snap['book_progress'][0] + self.assertEqual((p['book_id'], p['read_at'], p['device']), (self.book.id, NOW - MIN, 'Browser')) + self.assertIn('server_now', snap) + + def test_rows_from_before_have_their_save_time_as_read_at(self): + p = EBookProgress.objects.create(user=self.user, book=self.book, scroll_fraction=0.4, position_anchor='9:0.0') + b = self.client.get('/books/', **self.auth).json()[0] + self.assertEqual(b['read_at'], int(p.updated_at.timestamp() * 1000)) + + def test_a_never_opened_book(self): + b = self.client.get('/books/', **self.auth).json()[0] + self.assertEqual((b['read_at'], b['last_read'], b['position_anchor']), (None, None, '')) diff --git a/books/views.py b/books/views.py index 121a604..41865ad 100644 --- a/books/views.py +++ b/books/views.py @@ -1,10 +1,13 @@ import base64 import json import re +import time import xml.etree.ElementTree as ET +from datetime import datetime, timezone as dt_timezone import requests from django.conf import settings +from django.db import transaction from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @@ -247,11 +250,40 @@ def _progress_is_further(new_anchor, new_frac, old_anchor, old_frac): """True if the new reading position is at least as far into the book as the old one. Compares by anchor block index (precise, decoupled from layout); falls back to scroll_fraction only when an anchor is missing (PDFs).""" - nb, ni = _anchor_parts(new_anchor) - ob, oi = _anchor_parts(old_anchor) - if nb >= 0 and ob >= 0: - return ni >= oi if nb == ob else nb >= ob - return new_frac >= old_frac + return _cmp_progress(new_anchor, new_frac, old_anchor, old_frac) >= 0 + + +def _cmp_progress(a_anchor, a_frac, b_anchor, b_frac): + """> 0 if A is further into the book than B, < 0 if behind, 0 if level.""" + ab, ai = _anchor_parts(a_anchor) + bb, bi = _anchor_parts(b_anchor) + if ab >= 0 and bb >= 0: + if ab != bb: + return ab - bb + return (ai > bi) - (ai < bi) + return (a_frac > b_frac) - (a_frac < b_frac) + + +def _now_ms(): + return int(time.time() * 1000) + + +def progress_state(p): + """A book's reading state as every client sees it (docs/progress-sync.md); + p may be None (never opened).""" + if p is None: + return {'scroll_fraction': 0.0, 'position_anchor': '', 'read_at': None, 'device': '', + 'furthest_fraction': 0.0, 'furthest_anchor': '', 'last_read': None} + read_at = p.read_at if p.read_at is not None else int(p.updated_at.timestamp() * 1000) + return { + 'scroll_fraction': p.scroll_fraction, + 'position_anchor': p.position_anchor, + 'read_at': read_at, + 'device': p.device, + 'furthest_fraction': p.furthest_fraction, + 'furthest_anchor': p.furthest_anchor, + 'last_read': datetime.fromtimestamp(read_at / 1000, tz=dt_timezone.utc).isoformat(), + } @require_http_methods(['GET']) @@ -264,21 +296,17 @@ def book_list(request): ) for b in books: b['uploaded_at'] = b['uploaded_at'].isoformat() - # Include saved scroll_fraction for each book - progress_map = { - p.book_id: (p.scroll_fraction, p.updated_at, p.position_anchor) - for p in EBookProgress.objects.filter(user=request.user) - } + progress_map = {p.book_id: p for p in EBookProgress.objects.filter(user=request.user)} highlighted_ids = set( EBookHighlights.objects.filter(user=request.user).values_list('book_id', flat=True) ) for b in books: - prog = progress_map.get(b['id']) - b['scroll_fraction'] = prog[0] if prog else 0.0 - b['last_read'] = prog[1].isoformat() if prog else None - b['position_anchor'] = prog[2] if prog else '' + b.update(progress_state(progress_map.get(b['id']))) b['has_highlights'] = b['id'] in highlighted_ids - return JsonResponse(books, safe=False) + response = JsonResponse(books, safe=False) + # The list is a bare array (old clients expect that): the server clock goes in a header. + response['X-Server-Now'] = str(_now_ms()) + return response @csrf_exempt @@ -497,22 +525,50 @@ def save_progress(request, pk): if isinstance(raw_anchor, str) and re.fullmatch(r'\d{1,7}:\d(\.\d{1,6})?', raw_anchor): position_anchor = raw_anchor - force = bool(body.get('force', False)) + force = body.get('force') is True + device = body.get('device') if isinstance(body.get('device'), str) else '' + now = _now_ms() + read_at = _client_ms(body.get('read_at')) + sent_at = _client_ms(body.get('sent_at')) - progress, _ = EBookProgress.objects.get_or_create( - user=request.user, - book=book, - ) - # Always advance to the furthest-read position (by anchor block index, so a - # transiently wrong scroll_fraction can't freeze the position) unless the - # client explicitly forces a reset (e.g. "start over" button in the reader). - if force or _progress_is_further(position_anchor, scroll_fraction, - progress.position_anchor, progress.scroll_fraction): - progress.scroll_fraction = scroll_fraction - progress.position_anchor = position_anchor - progress.save(update_fields=['scroll_fraction', 'position_anchor', 'updated_at']) + with transaction.atomic(): + progress, created = EBookProgress.objects.select_for_update().get_or_create(user=request.user, book=book) + if created: + progress.read_at = None + if force: + when, store = now, True + elif read_at is not None: + # The device's clock may be off: move its time onto ours by the + # difference at sending, and nothing counts as later than now. + when = read_at + (now - sent_at) if sent_at is not None else read_at + when = min(when, now) + store = progress.read_at is None or when >= progress.read_at + else: + # An app from before 2026-10-12: only forward, as it used to be — but + # strictly, so re-saving the same place changes nothing. + when = now + store = created or _cmp_progress(position_anchor, scroll_fraction, + progress.position_anchor, progress.scroll_fraction) > 0 + if store: + progress.scroll_fraction = scroll_fraction + progress.position_anchor = position_anchor + progress.read_at = when + progress.device = device[:40] + if _cmp_progress(position_anchor, scroll_fraction, + progress.furthest_anchor, progress.furthest_fraction) > 0 or created: + progress.furthest_fraction = scroll_fraction + progress.furthest_anchor = position_anchor + progress.save() - return JsonResponse({'ok': True}) + return JsonResponse({'ok': True, 'stored': store, 'server_now': now, **progress_state(progress)}) + + +def _client_ms(value): + """A time from a client (ms since epoch) or None if it isn't a plausible one.""" + if isinstance(value, bool) or not isinstance(value, (int, float)): + return None + value = int(value) + return value if 10 ** 12 <= value < 10 ** 14 else None @csrf_exempt diff --git a/docs/progress-sync.md b/docs/progress-sync.md new file mode 100644 index 0000000..1cefc30 --- /dev/null +++ b/docs/progress-sync.md @@ -0,0 +1,71 @@ +# Reading position: the last place read wins + +Since 2026-10-12 the place to continue a book is **where it was last read, on any device**, not the +furthest place ever reached. The furthest place is still kept, as a hint ("Weiteste Stelle"). +This file is the contract between the server (books/views.py `save_progress`), the web reader +(static/js/app.js), diora-android (Ops.kt / SyncEngine.kt / native-*.js) and diora-desktop +(src/main/ops.js / sync.js). They must apply the same rules. + +## The state per book + +| field | meaning | +|---|---| +| `scroll_fraction`, `position_anchor` | the continue point (anchor `"block:inner"`, empty for PDFs) | +| `read_at` | when the reader was **at** that place, ms since epoch, **server clock** | +| `device` | who read there (`"Browser"`, `"diora Desktop"`, `"Handy"`, `"tolino"` …), for "auf X weitergelesen" | +| `furthest_fraction`, `furthest_anchor` | the furthest place ever reported (only ever grows) | + +`last_read` in `/books/` is `read_at` as ISO time (it used to be the time of the last save attempt). + +## Writing: `POST /books//progress/` + +```json +{"scroll_fraction": 0.42, "position_anchor": "187:0.62", "read_at": 1760270000000, + "sent_at": 1760270004000, "device": "diora Desktop"} +``` + +* `read_at` is **the moment the reader arrived at this position** (the last time the visible position + changed because of the reader), in the **device's** clock, not the moment of saving. Saving an + unchanged position again — every 10 s, on hide, on close, on unload — therefore carries the old time + and can never beat a newer position from another device. A device that only *shows* a position it + got from the server (just opened, nothing moved) reports the server's `read_at` back unchanged. +* `sent_at` is the device clock **when the request leaves** (an outbox sets it at flush time, not + when queuing). The server corrects the device's clock error with it: + `t = read_at + (server_now − sent_at)`, then `t = min(t, server_now)`. + So a tolino whose clock is hours off still orders correctly, and nothing can win "from the future". +* The server stores the position if `t >= stored read_at` (or nothing is stored, or `force`). + Older writes (an offline device catching up late) are **not** stored as the continue point. +* Every write — stored or not — raises `furthest_*` if it is further (same comparison as before: + anchor block, then inner fraction; `scroll_fraction` when an anchor is missing). +* `force: true` (the explicit "Diese Stelle als Fortschritt setzen") stores regardless, with `t = server_now`. +* **Old clients** (no `read_at`): stored only if **strictly further** than the stored position + (the old rule, but `>` so an idle old client re-saving its place changes nothing), with `t = server_now`. + This keeps not-yet-updated apps from pulling the continue point back. +* The response is the state after the write: + `{"ok": true, "stored": bool, "scroll_fraction", "position_anchor", "read_at", "device", + "furthest_fraction", "furthest_anchor", "server_now"}`. + +## Reading + +`/books/` and `/api/sync/` (`book_progress`) carry all fields of the state, `read_at` in ms (server clock), +plus `server_now` (ms) so a client can estimate its clock offset `server_now − device_now`. + +## Clients + +* **Local apply / outbox** (Android, desktop, web cache): the same rule as the server, in server time: + a local write converts its `read_at` with the last known offset (0 if none), stores it if newer + than or equal to what the local list holds. Two queued writes for the same book: keep the one with + the later `read_at` (force stays sticky). After a pull, a pending write is reapplied with that rule: + newer than the server's → it shows (and is sent); older → the server's position shows (it's still sent, + for `furthest`). +* **Never save before the position is restored** after opening a book, and never save a position the + reader didn't reach by reading: while a jump is tentative ("Zurück zum Text" after a footnote or link, + "Weit gescrollt — Zurückspringen"), the previous position and its `read_at` are what gets saved, until + the reader returns (nothing changes) or carries on reading there (then the jump target counts, with + its own time). +* **Opening a book** uses the newer of server state and local (not yet sent) state, by `read_at`. +* **Coming back to an open book** (window/app in front again, or after a pull): if the server holds a + newer position from another device than the one shown, offer it: "Auf weitergelesen — + Springen". Do not move the page without asking. +* If `furthest_*` is clearly ahead of the continue point when a book opens, offer + "Weiteste Stelle: 63 % — Springen". diff --git a/static/js/app.js b/static/js/app.js index a094a04..e7ac60e 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -287,7 +287,8 @@ window.addEventListener('beforeunload', () => { } // Flush cached encrypted payloads for reader data (encryption is async so we // use pre-computed blobs stored by the debounced savers) - if (_lastProgressBeacon) navigator.sendBeacon(_lastProgressBeacon.url, _lastProgressBeacon.body); + // The last position written (it's in the outbox too, in case the beacon is lost). + if (_lastProgressBeacon) navigator.sendBeacon(`/books/${_lastProgressBeacon.book}/progress/`, _progressBody(_lastProgressBeacon)); if (_lastBookmarkBeacon) navigator.sendBeacon(_lastBookmarkBeacon.url, _lastBookmarkBeacon.body); if (_lastHighlightBeacon) navigator.sendBeacon(_lastHighlightBeacon.url, _lastHighlightBeacon.body); }); @@ -298,6 +299,8 @@ document.addEventListener('visibilitychange', () => { if (highlightsDirty) saveHighlights(); saveReaderProgress(); } + // Back in front: maybe the book was read on another device meanwhile. + if (document.visibilityState === 'visible' && currentBookId) _checkNewerPositionElsewhere(); }); // Cached beacon payloads — updated after each successful encrypt in save functions @@ -2080,6 +2083,7 @@ function _dismissScrollJumpToast() { _scrollJumpToastEl.remove(); _scrollJumpToastEl = null; } + if (_readPos && _readPos.hold === 'jump') _readPos.hold = false; } function _showScrollJumpToast(backTop) { @@ -2089,6 +2093,8 @@ function _showScrollJumpToast(backTop) { toast.innerHTML = `Weit gescrollt`; document.body.appendChild(toast); _scrollJumpToastEl = toast; + // Until it's gone, the place before the jump stays the reading position. + if (_readPos && !_readPos.hold) _readPos.hold = 'jump'; toast.querySelector('.reader-toast-btn').addEventListener('click', () => { const contentEl = $('reader-content'); if (contentEl) { @@ -2096,8 +2102,16 @@ function _showScrollJumpToast(backTop) { contentEl.scrollTo({top: backTop, behavior: 'smooth'}); } _dismissScrollJumpToast(); + // Back where it was: that's no new reading. + _readerInteractedAt = 0; }); - setTimeout(_dismissScrollJumpToast, 6000); + setTimeout(() => { + if (_scrollJumpToastEl !== toast) return; + _dismissScrollJumpToast(); + // Stayed there: the far place is where the reading goes on. + _noteReaderInteraction(); + if (_readPos) _readPos.key = null; + }, 6000); } function _checkScrollJump(contentEl) { @@ -2294,6 +2308,203 @@ function _cmpProgress(anchorA, fracA, anchorB, fracB) { return (fracA || 0) - (fracB || 0); } +// --------------------------------------------------------------------------- +// Reading position: the place last read wins, on any device (docs/progress-sync.md). +// +// A position counts as read when the reader got there by reading (scrolling, +// turning pages, choosing a chapter); its time is that moment, not the time it +// is saved. So re-saving an unchanged place, on a timer or on closing, carries +// the old time and can never beat a newer place read on another device. A book +// just opened at the server's place has read nothing here yet: nothing is sent. +// Own writes wait in a small outbox (localStorage) until the server has them. +// --------------------------------------------------------------------------- +let _serverOffset = 0; // the server's clock minus this device's (ms) +let _readPos = null; // the open book: {bookId, ready, key, readAt (device ms), moved, hold} +let _readerInteractedAt = 0; + +function _progressDevice() { + return window.DIORA_DEVICE_NAME || 'Browser'; +} + +function _noteServerNow(ms) { + if (typeof ms === 'number' && ms > 1e12) _serverOffset = ms - Date.now(); +} + +function _noteServerNowHeader(res) { + try { _noteServerNow(Number(res.headers.get('X-Server-Now'))); } catch (e) {} +} + +// The reader did something (scroll wheel, touch, key, mouse): the next change +// of position is reading, not a layout settling. +function _noteReaderInteraction() { + _readerInteractedAt = Date.now(); + if (_readPos && _readPos.hold === 'until-moved') _readPos.hold = false; +} +window.__dioraReaderInteraction = _noteReaderInteraction; + +// While a jump is only a look (a footnote's place, "Zurück zum Text"), the +// place before it stays the reading position. true: until the next +// interaction; false: released now. +window.__dioraHoldProgress = function (on) { + if (_readPos) _readPos.hold = on ? 'until-moved' : false; +}; + +function _positionKey(anchor, fraction) { + return anchor || 'f' + (fraction || 0).toFixed(4); +} + +// Called with every computed position: a change that follows an interaction is reading. +function _noteReadingPosition(anchor, fraction) { + if (!_readPos || !_readPos.ready || _readPos.hold) return; + const key = _positionKey(anchor, fraction); + if (key === _readPos.key) return; + _readPos.key = key; + if (Date.now() - _readerInteractedAt < 10000) { + _readPos.readAt = Date.now(); + _readPos.moved = true; + } +} + +function _progressOutboxKey() { + return `diora_progress_outbox_${window.USER_ID || 0}`; +} + +function _progressOutbox() { + try { return JSON.parse(localStorage.getItem(_progressOutboxKey()) || '{}') || {}; } catch (e) { return {}; } +} + +function _progressOutboxPut(entry) { + try { + const box = _progressOutbox(); + const old = box[entry.book]; + if (old && !entry.force && old.read_at > entry.read_at) return; + box[entry.book] = entry; + localStorage.setItem(_progressOutboxKey(), JSON.stringify(box)); + } catch (e) {} +} + +function _progressOutboxDone(bookId, readAt) { + try { + const box = _progressOutbox(); + if (box[bookId] && box[bookId].read_at === readAt) { + delete box[bookId]; + localStorage.setItem(_progressOutboxKey(), JSON.stringify(box)); + } + } catch (e) {} +} + +function _progressBody(e) { + return JSON.stringify({ + scroll_fraction: e.fraction, position_anchor: e.anchor, read_at: e.read_at, + sent_at: Date.now(), device: e.device, ...(e.force && {force: true}), + }); +} + +// Send one queued position; true once the server has it (stored or not: it decides). +async function _sendProgress(e) { + try { + const res = await fetch(`/books/${e.book}/progress/`, { + method: 'POST', + headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, + body: _progressBody(e), + }); + if (!(res.ok || (res.status >= 400 && res.status < 500))) return false; + try { _noteServerNow((await res.json()).server_now); } catch (err) {} + _progressOutboxDone(e.book, e.read_at); + return true; + } catch (err) { + return false; + } +} + +let _progressFlushing = null; +function _flushProgressOutbox() { + if (!_progressFlushing) { + _progressFlushing = (async () => { + for (const e of Object.values(_progressOutbox())) await _sendProgress(e); + })().finally(() => { _progressFlushing = null; }); + } + return _progressFlushing; +} + +// A book's state from the server list, made newer by an own write the server doesn't have yet. +function _withPendingProgress(b) { + const e = _progressOutbox()[b.id]; + if (!e) return b; + const at = e.read_at + _serverOffset; + if (b.read_at != null && at < b.read_at && !e.force) return b; + return {...b, scroll_fraction: e.fraction, position_anchor: e.anchor, read_at: at, + device: e.device, last_read: new Date(at).toISOString()}; +} + +function _bookRecency(b) { + if (typeof b.read_at === 'number') return b.read_at; + const t = b.last_read ? Date.parse(b.last_read) : NaN; + return isNaN(t) ? 0 : t; +} + +function _readerToast(text, button, onClick, ms) { + const toast = document.createElement('div'); + toast.className = 'reader-toast reader-toast-action'; + toast.innerHTML = ''; + toast.querySelector('span').textContent = text; + toast.querySelector('button').textContent = button; + document.body.appendChild(toast); + const done = () => toast.remove(); + toast.querySelector('button').addEventListener('click', () => { done(); onClick(); }); + setTimeout(done, ms || 10000); + return toast; +} + +function _jumpToPosition(anchor, fraction) { + const contentEl = $('reader-content'); + if (!contentEl) return; + _suppressScrollJumpDetect(); + if (currentPdfDoc && readerSettings.pdfPaginated && pdfTotalPages > 1) { + pdfCurrentPage = Math.max(1, Math.round(fraction * (pdfTotalPages - 1)) + 1); + enterPdfPaginatedMode(); + } else if (currentPdfDoc) { + contentEl.scrollTop = fraction * (contentEl.scrollHeight - contentEl.clientHeight); + } else if (!restoreFromAnchor(contentEl, anchor)) { + restoreFromAnchor(contentEl, _anchorForFraction(contentEl, fraction)); + } +} + +// Another device read this book after the place shown here: offer to go there. +let _newerOfferShown = null; +async function _checkNewerPositionElsewhere() { + const pos = _readPos; + if (!pos || !pos.ready || !currentBookId || pos.bookId !== currentBookId) return; + let b; + try { + const res = await fetch('/books/', {cache: 'no-store'}); + _noteServerNowHeader(res); + b = (await res.json()).find(x => x.id === pos.bookId); + } catch (e) { return; } + if (!b || b.read_at == null || _readPos !== pos) return; + if (b.read_at <= pos.readAt + _serverOffset + 3000) return; + if (_positionKey(b.position_anchor || '', b.scroll_fraction) === pos.key) return; + const offerKey = `${pos.bookId}:${b.read_at}`; + if (_newerOfferShown === offerKey) return; + _newerOfferShown = offerKey; + const where = b.device ? `Auf ${b.device}` : 'Auf einem anderen Gerät'; + _readerToast(`${where} weitergelesen · ${_formatBookPercent(b.scroll_fraction || 0)} %`, 'Dorthin', () => { + if (_readPos !== pos) return; + _jumpToPosition(b.position_anchor || '', b.scroll_fraction || 0); + // That place and its time are the server's: nothing new to tell it. + pos.key = null; + pos.readAt = b.read_at - _serverOffset; + pos.moved = false; + setTimeout(() => { + const contentEl = $('reader-content'); + if (_readPos === pos && contentEl) { + pos.key = currentPdfDoc ? _positionKey('', b.scroll_fraction || 0) : _positionKey(getPositionAnchor(contentEl), 0); + } + }, 300); + }, 15000); +} +window.__dioraCheckNewerPosition = _checkNewerPositionElsewhere; + // --------------------------------------------------------------------------- // Book cache — IndexedDB, evict after 4 weeks of inactivity // --------------------------------------------------------------------------- @@ -2497,7 +2708,8 @@ async function _loadCachedBookMeta() { } catch { return []; } } -async function _updateCachedBookProgress(bookId, fraction, anchor, force = false) { +async function _updateCachedBookProgress(bookId, fraction, anchor, force = false, readAt = null) { + const at = (readAt == null ? Date.now() : readAt) + _serverOffset; try { const db = await _openBookCacheDb(); await new Promise(resolve => { @@ -2506,11 +2718,17 @@ async function _updateCachedBookProgress(bookId, fraction, anchor, force = false const req = store.get(bookId); req.onsuccess = e => { const entry = e.target.result; - // Only advance (or on force), so a transiently wrong save can't lose progress. - if (entry && (force || _cmpProgress(anchor, fraction, entry.position_anchor, entry.scroll_fraction) >= 0)) { + // The place read last, as on the server (docs/progress-sync.md). + if (entry && (force || entry.read_at == null || at >= entry.read_at)) { entry.scroll_fraction = fraction; entry.position_anchor = anchor; - entry.last_read = new Date().toISOString(); + entry.read_at = at; + entry.device = _progressDevice(); + entry.last_read = new Date(at).toISOString(); + if (_cmpProgress(anchor, fraction, entry.furthest_anchor || '', entry.furthest_fraction || 0) > 0) { + entry.furthest_anchor = anchor; + entry.furthest_fraction = fraction; + } store.put(entry); } }; @@ -2641,7 +2859,9 @@ async function loadBookList() { try { listEl.innerHTML = '

Bücherliste wird vom Server geholt …

'; + _flushProgressOutbox(); // own positions the server doesn't have yet const res = await fetch('/books/', {cache: 'no-store'}); + _noteServerNowHeader(res); if (!res.ok) { listEl.innerHTML = `

Serverfehler ${res.status} beim Laden der Bücher.

`; return; @@ -2673,48 +2893,23 @@ async function loadBookList() { const metaBuf = await decryptBytes(key, b.meta_iv, b.meta_ct); const meta = JSON.parse(new TextDecoder().decode(metaBuf)); bookMetaCache[b.id] = {title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', filename: meta.filename || '', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', pages: meta.pages || 0}; - decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', pages: meta.pages || 0, scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); + decrypted.push({id: b.id, title: meta.title || '?', author: meta.author || '', type: meta.type || 'epub', folder: meta.folder || '', isbn: meta.isbn || '', shelfTag: meta.shelfTag || '', pages: meta.pages || 0, scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', read_at: b.read_at == null ? null : b.read_at, device: b.device || '', furthest_fraction: b.furthest_fraction || 0, furthest_anchor: b.furthest_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: true, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } catch (e) { bookMetaCache[b.id] = {title: `Buch #${b.id}`, author: '', type: 'epub', filename: '', folder: '', isbn: '', shelfTag: ''}; - decrypted.push({id: b.id, title: `Buch #${b.id}`, author: '', type: 'epub', folder: '', isbn: '', shelfTag: '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); - } - } - // If local cache is further ahead than the server, push it and use it for display. - // Server uses max-semantics so this is always safe. - const _localMeta = await _loadCachedBookMeta(); - const _localById = Object.fromEntries(_localMeta.map(b => [b.id, b])); - for (const b of decrypted) { - const loc = _localById[b.id]; - if (loc && _cmpProgress(loc.position_anchor, loc.scroll_fraction, b.position_anchor, b.scroll_fraction) > 0) { - fetch(`/books/${b.id}/progress/`, { - method: 'POST', - headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, - body: JSON.stringify({scroll_fraction: loc.scroll_fraction, position_anchor: loc.position_anchor || ''}), - }).catch(() => {}); - b.scroll_fraction = loc.scroll_fraction; - b.position_anchor = loc.position_anchor; - b.last_read = loc.last_read; + decrypted.push({id: b.id, title: `Buch #${b.id}`, author: '', type: 'epub', folder: '', isbn: '', shelfTag: '', scroll_fraction: b.scroll_fraction, position_anchor: b.position_anchor || '', read_at: b.read_at == null ? null : b.read_at, device: b.device || '', furthest_fraction: b.furthest_fraction || 0, furthest_anchor: b.furthest_anchor || '', uploaded_at: b.uploaded_at, last_read: b.last_read || null, keyOk: false, is_read: !!b.is_read, has_highlights: !!b.has_highlights}); } } + // An own position the server doesn't have yet (sent above) shows if it's the newer one. + for (let i = 0; i < decrypted.length; i++) decrypted[i] = _withPendingProgress(decrypted[i]); - decrypted.sort((a, b) => { - if (a.last_read && b.last_read) return b.last_read.localeCompare(a.last_read); - if (a.last_read) return -1; - if (b.last_read) return 1; - return b.uploaded_at.localeCompare(a.uploaded_at); - }); + decrypted.sort((a, b) => (_bookRecency(b) - _bookRecency(a)) || b.uploaded_at.localeCompare(a.uploaded_at)); _saveBookMeta(decrypted); // persist for offline renderBookList(decrypted); } catch (e) { // Network error — try offline fallback - const cachedBooks = await _loadCachedBookMeta(); + const cachedBooks = (await _loadCachedBookMeta()).map(_withPendingProgress); if (cachedBooks.length) { - cachedBooks.sort((a, b) => { - if (a.last_read && b.last_read) return b.last_read.localeCompare(a.last_read); - if (a.last_read) return -1; - if (b.last_read) return 1; - return (b.uploaded_at || '').localeCompare(a.uploaded_at || ''); - }); + cachedBooks.sort((a, b) => (_bookRecency(b) - _bookRecency(a)) || (b.uploaded_at || '').localeCompare(a.uploaded_at || '')); for (const b of cachedBooks) bookMetaCache[b.id] = {title: b.title, author: b.author, type: b.type || 'epub', filename: b.filename || '', folder: b.folder || '', isbn: b.isbn || '', shelfTag: b.shelfTag || '', pages: b.pages || 0}; const uploadArea = $('book-upload-area'); if (uploadArea) uploadArea.style.display = 'none'; @@ -3623,6 +3818,8 @@ function _ttsClearHighlight() { // it into a DOM Range via the same char-offset addressing highlights use, and // wraps it in a . Returns the offset to resume searching from. function _ttsHighlightSentence(block, sentence, fromOffset) { + // Listening is reading: where the voice is counts as the reading position. + _noteReaderInteraction(); _ttsClearHighlight(); const full = block.textContent; const idx = full.indexOf(sentence, fromOffset); @@ -3840,7 +4037,6 @@ function exitReaderImmersiveMode() { // progress saved from that doubled DOM had block indices beyond the real book, // which then out-ranked every real position on the server. let _openBookInFlight = null; -let _repairProgressAnchor = false; // stored anchor didn't fit this book async function openBook(bookId) { if (_openBookInFlight) { @@ -3857,7 +4053,8 @@ async function openBook(bookId) { } async function _openBookOnce(bookId) { - _repairProgressAnchor = false; + // Nothing is saved for this book until its place is restored. + _readPos = {bookId, ready: false, key: null, readAt: 0, moved: false, hold: false}; const overlay = $('reader-overlay'); const contentEl = $('reader-content'); const titleEl = $('reader-title'); @@ -4020,21 +4217,25 @@ async function _openBookOnce(bookId) { // Restore scroll position — must happen BEFORE auto-save timer is started _suppressScrollJumpDetect(); + let openedProgress = null; try { let fraction = 0, anchor = ''; + let bookData = null; try { - const progressRes = await fetch('/books/'); + const progressRes = await fetch('/books/', {cache: 'no-store'}); + _noteServerNowHeader(progressRes); const allBooks = await progressRes.json(); - const bookData = allBooks.find(b => b.id === bookId); - fraction = bookData ? (bookData.scroll_fraction || 0) : 0; - anchor = bookData ? (bookData.position_anchor || '') : ''; + bookData = allBooks.find(b => b.id === bookId) || null; } catch { // offline — use cached meta const cached = await _loadCachedBookMeta(); - const bookData = cached.find(b => b.id === bookId); - fraction = bookData ? (bookData.scroll_fraction || 0) : 0; - anchor = bookData ? (bookData.position_anchor || '') : ''; + bookData = cached.find(b => b.id === bookId) || null; } + // The newer of the server's place and an own one it doesn't have yet. + if (bookData) bookData = _withPendingProgress(bookData); + fraction = bookData ? (bookData.scroll_fraction || 0) : 0; + anchor = bookData ? (bookData.position_anchor || '') : ''; + openedProgress = bookData; _currentPositionAnchor = anchor; if (isPdf && readerSettings.pdfPaginated) { if (fraction > 0 && pdfTotalPages > 1) @@ -4058,16 +4259,20 @@ async function _openBookOnce(bookId) { if (!restoreFromAnchor(contentEl, anchor) && fraction > 0) { restoreFromAnchor(contentEl, _anchorForFraction(contentEl, fraction)); } - // An anchor past the end of this book can only come from a broken - // render. The server keeps the furthest block index, so it would block - // every real position from now on: overwrite it with the next save. - const [storedBlock] = _anchorParts(anchor); - if (storedBlock >= contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR).length) { - _repairProgressAnchor = true; - } } } catch (e) {} + // From here on, what the reader does is reading. The place shown has the + // time it was read at (elsewhere, or here earlier): nothing new to send yet. + if (_readPos && _readPos.bookId === bookId && currentBookId === bookId) { + const shownAnchor = isPdf ? '' : getPositionAnchor(contentEl); + const readAtServer = openedProgress && openedProgress.read_at != null ? openedProgress.read_at : null; + _readPos.key = _positionKey(shownAnchor, openedProgress ? (openedProgress.scroll_fraction || 0) : 0); + _readPos.readAt = readAtServer == null ? 0 : readAtServer - _serverOffset; + _readPos.ready = true; + _offerFurthestPosition(openedProgress); + } + // Prime the "settled" baseline to wherever we just restored to, rather // than waiting to lazily pick up whatever scroll event happens to fire // first — that could otherwise be a later, unrelated layout correction. @@ -4110,6 +4315,7 @@ async function _openBookOnce(bookId) { _anchorTrackDebounce = setTimeout(() => { _currentPositionAnchor = getPositionAnchor(contentEl); const f = _fractionFromAnchor(contentEl, _currentPositionAnchor); + _noteReadingPosition(_currentPositionAnchor, f === null ? 0 : f); if (progressInput && f !== null && document.activeElement !== progressInput) { progressInput.value = _formatBookPercent(f); } @@ -4205,6 +4411,19 @@ async function deleteBook(bookId) { } catch (e) {} } +// Opened somewhere before the furthest place reached: offer it. +function _offerFurthestPosition(p) { + if (!p) return; + const furthest = p.furthest_fraction || 0; + if (furthest - (p.scroll_fraction || 0) < 0.02 || furthest >= 0.999) return; + const pos = _readPos; + _readerToast(`Weiteste Stelle: ${_formatBookPercent(furthest)} %`, 'Dorthin', () => { + if (_readPos !== pos) return; + _noteReaderInteraction(); + _jumpToPosition(p.furthest_anchor || '', furthest); + }, 8000); +} + async function saveReaderProgress(force = false) { if (!currentBookId) return; const contentEl = $('reader-content'); @@ -4224,6 +4443,12 @@ async function saveReaderProgress(force = false) { return; } + // Not restored yet (or another book): what's shown isn't a place read. + const pos = _readPos; + if (!pos || !pos.ready || pos.bookId !== currentBookId) return; + // A jump that's only a look: the place before it stays. + if (pos.hold && !force) return; + let anchor = ''; if (!currentPdfDoc) { anchor = getPositionAnchor(contentEl); @@ -4234,27 +4459,30 @@ async function saveReaderProgress(force = false) { } fraction = Math.min(1.0, Math.max(0.0, fraction)); - _updateCachedBookProgress(currentBookId, fraction, anchor, force || (_repairProgressAnchor && !!anchor)); + _noteReadingPosition(anchor, fraction); + if (force) { + pos.readAt = Date.now(); + pos.moved = true; + } + // Nothing read here since opening: the server has this place already. + if (!pos.moved) return; - const repair = _repairProgressAnchor && !!anchor; - if (repair) _repairProgressAnchor = false; - const body = JSON.stringify({scroll_fraction: fraction, position_anchor: anchor, ...((force || repair) && {force: true})}); - _lastProgressBeacon = {url: `/books/${currentBookId}/progress/`, body}; - - try { - await fetch(`/books/${currentBookId}/progress/`, { - method: 'POST', - headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, - body, - }); - if (force) { - const toast = document.createElement('div'); - toast.className = 'reader-toast'; - toast.textContent = 'Position gesetzt'; - document.body.appendChild(toast); - setTimeout(() => toast.remove(), 1800); - } - } catch (e) {} + const entry = {book: currentBookId, fraction, anchor, read_at: pos.readAt, device: _progressDevice(), ...(force && {force: true})}; + const sig = `${entry.book}|${anchor}|${fraction.toFixed(5)}|${entry.read_at}`; + _lastProgressBeacon = entry; + if (!force && sig === pos.sentSig) return; // unchanged since the last save + pos.sentSig = sig; + _progressOutboxPut(entry); + _updateCachedBookProgress(currentBookId, fraction, anchor, force, pos.readAt); + const sent = await _sendProgress(entry); + if (!sent) pos.sentSig = null; // try again with the next save + if (sent && force) { + const toast = document.createElement('div'); + toast.className = 'reader-toast'; + toast.textContent = 'Position gesetzt'; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 1800); + } } function closeReader() { @@ -4262,6 +4490,7 @@ function closeReader() { stopReadAloud(); // Save progress BEFORE hiding — scrollHeight/clientHeight return 0 once display:none saveReaderProgress(); + _readPos = null; // nothing more is saved for this book if (bookmarksDirty) saveBookmarks(); if (highlightsDirty) saveHighlights(); @@ -6410,12 +6639,24 @@ function openRadioSidebar() { window.addEventListener('online', async () => { showOfflineBanner(false); await _syncPendingAnnotations(); - loadBookList(); // loadBookList pushes any locally-advanced progress automatically + loadBookList(); // sends the positions read offline first (the progress outbox) }); // Show banner immediately if already offline at startup if (!navigator.onLine) showOfflineBanner(true); + // Reading position: positions reached by the reader's own doing count (see _noteReadingPosition). + { + const content = $('reader-content'); + if (content) { + for (const type of ['wheel', 'touchstart', 'mousedown']) { + content.addEventListener(type, _noteReaderInteraction, {passive: true, capture: true}); + } + } + document.addEventListener('keydown', () => { if (currentBookId) _noteReaderInteraction(); }, true); + if (IS_AUTHENTICATED) _flushProgressOutbox(); + } + // Footnote hover (desktop) + tap (mobile) via event delegation const _readerContentEl = $('reader-content'); if (_readerContentEl) { diff --git a/static/js/sw.js b/static/js/sw.js index 4d369e7..97ced2b 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v99'; +const CACHE = 'diora-v100'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', From 080cdbde5afebb246b5c2b5b954f2c67b8bcdd88 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Mon, 12 Oct 2026 20:05:13 +0200 Subject: [PATCH 099/176] =?UTF-8?q?Podcast-Oberfl=C3=A4che=20aus=20diora-a?= =?UTF-8?q?ndroid=205821dfa=20(=5F=5FdioraPlayerNow=20f=C3=BCr=20den=20Bil?= =?UTF-8?q?dschirmschoner);=20sw=20v101?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- static/js/sw.js | 2 +- static/podcasts/native-podcast.js | 18 ++- static/podcasts/podcast-ui.css | 184 +++++++++++++++++++++++++++++- 3 files changed, 201 insertions(+), 3 deletions(-) diff --git a/static/js/sw.js b/static/js/sw.js index 97ced2b..2113fd9 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v100'; +const CACHE = 'diora-v101'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', diff --git a/static/podcasts/native-podcast.js b/static/podcasts/native-podcast.js index 3a2ca2e..24ebfac 100644 --- a/static/podcasts/native-podcast.js +++ b/static/podcasts/native-podcast.js @@ -1,4 +1,4 @@ -// Copied from diora-android 7dcc0c6 2026-10-04: web-native/static/native-podcast.js +// Copied from diora-android 5821dfa 2026-10-12: web-native/static/native-podcast.js // by tools/sync_podcast_ui.py. Do not edit here: change it in diora-android // and copy again. The browser host is static/js/podcasts-web.js. @@ -4405,6 +4405,22 @@ if (epScreen && (before.episodeId !== player.episodeId || !!before.playing !== !!player.playing)) episodeScreenParts(); }; + // What plays, for the screensaver (native-saver.js): the player's report and the chapter at its place. + const nowAsked = new Set(); + window.__dioraPlayerNow = function () { + if (!player || player.kind !== 'podcast') return player; + const d = details.get(player.episodeId); + if (!d && player.episodeId && !nowAsked.has(player.episodeId)) { + nowAsked.add(player.episodeId); + Promise.resolve(loadDetails(player.episodeId)).catch(() => {}); + } + let chapter = ''; + const chapters = (d && d.chapters) || []; + const t = (player.pos || 0) / 1000; + for (const c of chapters) if ((c.start || 0) <= t + 0.5) chapter = c.title || ''; + return Object.assign({}, player, {chapter: plain(chapter)}); + }; + // Ambient starts (app.js): an episode playing here pauses. window.__dioraPausePlayer = function () { if (player.kind !== 'podcast' || !player.playing || player.remote) return; diff --git a/static/podcasts/podcast-ui.css b/static/podcasts/podcast-ui.css index 7422bc7..39a02cf 100644 --- a/static/podcasts/podcast-ui.css +++ b/static/podcasts/podcast-ui.css @@ -1,4 +1,4 @@ -/* Copied from diora-android 67a915e 2026-10-03: the podcast parts of web-native/static/native.css +/* Copied from diora-android 5821dfa 2026-10-12: the podcast parts of web-native/static/native.css (bottom sheet, podcasts) by tools/sync_podcast_ui.py. Do not edit here. The web's own additions are in podcasts-web.css. */ @@ -796,3 +796,185 @@ body.eink .nui-pod-facts a { text-decoration-color: #000; } background: rgba(0, 0, 0, 0.55); color: #ddd; } + +/* diora Reader (books only, native.js): no read-aloud, no cloud import. */ +.nui-reader-app #reader-tts-btn, +.nui-reader-app #reader-tts-lang, +.nui-reader-app button[onclick*="toggleCloudImport"], +.nui-reader-app #cloud-browser { display: none !important; } + +/* Datenverbrauch (native-ui.js showDataUsage) */ +.nui-usage { padding: 4px 16px 12px; } +.nui-usage-chips { display: flex; gap: 6px; margin-bottom: 12px; } +.nui-usage-chip { padding: 6px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 16px; background: none; color: inherit; font: inherit; font-size: 13px; } +.nui-usage-chip.on { border-color: var(--accent, #e63946); color: var(--accent, #e63946); } +.nui-usage-total { display: flex; justify-content: space-between; gap: 12px; font-size: 14px; margin-bottom: 10px; } +.nui-usage-total b { color: var(--accent, #e63946); } +.nui-usage-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.nui-usage-table th { text-align: right; font-weight: 400; color: var(--nui-muted, #9a9a9a); padding: 2px 0 6px; } +.nui-usage-table td { padding: 7px 0; border-top: 1px solid var(--border, #2a2a2a); vertical-align: top; } +.nui-usage-table td + td { text-align: right; white-space: nowrap; padding-left: 10px; } +.nui-usage-bar { display: block; height: 3px; margin-top: 4px; background: #2a2a2a; border-radius: 2px; overflow: hidden; } +.nui-usage-bar > span { display: block; height: 100%; background: var(--accent, #e63946); } +.nui-usage-note { font-size: 12px; color: var(--nui-muted, #9a9a9a); margin: 10px 0 0; } +body.eink .nui-usage-bar > span { background: #000; } +body.eink .nui-usage-total b { color: #000; } + +/* ---- sidebar layout (html.nui-sidebar, native-radio.js): the menu as a rail on the left, + the player bars at the bottom of the page. Always on a TV, a choice in the app menu elsewhere. ---- */ + +.nui-rail-brand, .nui-rail-menu { display: none; } + +html.nui-sidebar { --nui-rail-w: 200px; } +html.nui-sidebar body { padding-left: var(--nui-rail-w); } +html.nui-sidebar.nui-reading body { padding-left: 0; } +html.nui-sidebar #nui-dock { left: var(--nui-rail-w); } +html.nui-sidebar #nui-tabbar { + position: fixed; + left: 0; + top: 0; + bottom: 0; + width: var(--nui-rail-w); + flex-direction: column; + padding: 18px 0 12px; + background: #0b0b0b; + border-right: 1px solid var(--border); + overflow-y: auto; +} +html.nui-sidebar #nui-tabbar button { + flex: 0 0 auto; + height: 46px; + flex-direction: row; + justify-content: flex-start; + gap: 14px; + padding: 0 22px; + font-size: 15px; + text-align: left; +} +html.nui-sidebar #nui-tabbar .nui-tab-icon { width: 22px; text-align: center; font-size: 19px; } +html.nui-sidebar #nui-tabbar button.nui-active { background: #181818; box-shadow: inset 3px 0 0 var(--accent); } +html.nui-sidebar .nui-rail-brand { + display: block; + padding: 0 22px 18px; + color: var(--accent); + font-weight: 700; + font-size: 22px; + letter-spacing: 0.08em; +} +html.nui-sidebar .nui-rail-menu { display: flex; margin-top: auto; } +/* Home's own wordmark: the rail has it already. */ +html.nui-sidebar #tab-home .home-brand { display: none; } +body.eink #nui-tabbar { background: #fff; } +/* A narrow screen (a phone upright): the rail shows its signs only. */ +@media (max-width: 640px) { + html.nui-sidebar { --nui-rail-w: 60px; } + html.nui-sidebar #nui-tabbar button { padding: 0; justify-content: center; font-size: 0; gap: 0; } + html.nui-sidebar #nui-tabbar .nui-tab-icon { font-size: 21px; } + html.nui-sidebar .nui-rail-brand { padding: 0 0 14px; text-align: center; font-size: 16px; letter-spacing: 0; overflow: hidden; } +} + +/* ---- television (html.nui-tv, native-tv.js): a focus that the remote moves ---- */ + +html.nui-tv { scroll-padding: 24px 0 calc(var(--nui-dock-h, 0px) + 24px); } +html.nui-tv .nui-tv-focus { + outline: 3px solid var(--accent) !important; + outline-offset: 2px; + border-radius: 6px; +} +html.nui-tv #nui-tabbar button.nui-tv-focus { outline-offset: -3px; background: #222; } +html.nui-tv :focus:not(.nui-tv-focus) { outline: none; } + +/* Books and feeds hidden on the TV (unless switched on in the app menu); Home's + sections of them: native-tv.js (:has, which the e-readers' old WebView lacks). */ +html.nui-noread #nui-tabbar button[data-tab="books"], +html.nui-noread #nui-tabbar button[data-tab="feeds"] { display: none; } + +/* ---- screensaver (native-saver.js): what plays, large, drifting slowly ---- */ + +#nui-saver { + position: fixed; + inset: 0; + z-index: 3000; + display: none; + overflow: hidden; + background: #000; + color: #fff; + cursor: none; +} +#nui-saver.nui-on { display: block; animation: nui-saver-in 1.2s ease-out; } +@keyframes nui-saver-in { from { opacity: 0; } to { opacity: 1; } } +.nui-saver-bg { + position: absolute; + inset: -10%; + background: center / cover no-repeat; + filter: blur(60px) brightness(0.32) saturate(1.3); +} +.nui-saver-stage { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + gap: 5vw; + padding: 8vh 10vw; + transition: transform 44s ease-in-out; +} +.nui-saver-art { + flex: 0 0 auto; + width: 34vmin; + height: 34vmin; + border-radius: 3vmin; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + background: #161616; + box-shadow: 0 2vmin 8vmin rgba(0, 0, 0, 0.6); +} +.nui-saver-art img { width: 100%; height: 100%; object-fit: cover; } +.nui-saver-art span { font-size: 16vmin; color: var(--accent); line-height: 1; } +.nui-saver-text { flex: 0 1 52vw; min-width: 0; display: flex; flex-direction: column; gap: 1.6vmin; } +.nui-saver-kicker, .nui-saver-sub, .nui-saver-chapter, .nui-saver-left { font-family: var(--nui-sans); letter-spacing: normal; } +.nui-saver-kicker { color: var(--accent); font-size: 2.6vmin; font-weight: 600; text-transform: uppercase; } +.nui-saver-title { + font-size: 5.4vmin; + font-weight: 700; + line-height: 1.2; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} +.nui-saver-sub { font-size: 3.4vmin; color: #ddd; line-height: 1.3; } +.nui-saver-chapter { font-size: 3vmin; color: var(--accent); } +.nui-saver-progress { height: 0.6vmin; border-radius: 1vmin; background: rgba(255, 255, 255, 0.18); overflow: hidden; margin-top: 1vmin; } +.nui-saver-progress span { display: block; height: 100%; background: var(--accent); transition: width 4s linear; } +.nui-saver-left { font-size: 2.6vmin; color: #bbb; } +#nui-saver [hidden] { display: none; } +.nui-saver-clock { + position: absolute; + right: 5vw; + bottom: 6vh; + font-size: 7vmin; + font-weight: 200; + color: rgba(255, 255, 255, 0.75); + font-variant-numeric: tabular-nums; + transition: transform 44s ease-in-out; +} +/* Ambient: its own picture full screen; the saver only adds the clock and the sound world. */ +html.nui-saver-ambient #tab-ambient { display: block !important; } +html.nui-saver-ambient #ambient-frame { + position: fixed !important; + inset: 0; + width: 100vw !important; + height: 100vh !important; + z-index: 2999; + border: 0; +} +html.nui-saver-ambient #nui-saver { background: transparent; } +html.nui-saver-ambient .nui-saver-bg, +html.nui-saver-ambient .nui-saver-art, +html.nui-saver-ambient .nui-saver-progress, +html.nui-saver-ambient .nui-saver-left { display: none; } +html.nui-saver-ambient .nui-saver-stage { align-items: flex-start; justify-content: flex-start; } +html.nui-saver-ambient .nui-saver-title { font-size: 3.4vmin; font-weight: 300; letter-spacing: 0.1em; } From 884a18e8ef7aa46b5d8ecd301fbc1486fc35fd20 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Wed, 14 Oct 2026 18:02:33 +0200 Subject: [PATCH 100/176] =?UTF-8?q?Hell=20und=20Dunkel=20(docs/theme.md),?= =?UTF-8?q?=20Vorlese-Schnittstelle=20f=C3=BCr=20die=20Playerleiste;=20sw?= =?UTF-8?q?=20v102?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Farbvariablen für beide Schemata in app.css (--bg --fg --muted --card --panel --hover --raised --border --border-strong --accent --on-accent --scrim --shadow), Hell als html[data-theme=light]. Umschalter im Kopf jeder Seite (base.html, vor dem ersten Zeichnen): Dunkel / Hell / Automatisch, ☾☀◐ in der Navigation; __dioraTheme / __dioraSetTheme / Ereignis diora-theme. - Reader: neues Standard-Theme "Wie die App" (Dunkel/Hell folgt der App). - ◑ (Kontrast zum Hintergrundbild) nur noch mit Hintergrundbild. - home.js auf die Variablen. - window.__dioraTts (state/toggle/stop/next/prev) und Ereignis diora-tts für die Playerleiste; mit __dioraTtsDock keine eigene Vorlese-Leiste mehr. - Reader-Hinweise über der Playerleiste, wenn sie im Reader steht. Co-Authored-By: Claude Opus 5.5 --- docs/theme.md | 44 +++++++++++++++ static/css/app.css | 100 +++++++++++++++++++++++---------- static/js/app.js | 133 +++++++++++++++++++++++++++++++++++++++++--- static/js/home.js | 24 ++++---- static/js/sw.js | 2 +- templates/base.html | 47 +++++++++++++++- 6 files changed, 298 insertions(+), 52 deletions(-) create mode 100644 docs/theme.md diff --git a/docs/theme.md b/docs/theme.md new file mode 100644 index 0000000..4126c77 --- /dev/null +++ b/docs/theme.md @@ -0,0 +1,44 @@ +# Light and dark + +diora has two colour schemes, **dark** (the old look) and **light**, chosen by the reader: +"Dunkel", "Hell" or "Automatisch" (as the operating system says). This file is the contract +between diora-web (app.css, home.js, templates), diora-android (native.css, native-*.js, +podcast-ui.css, feeds.js) and diora-desktop (desktop.css). + +## Switching + +* `` is the scheme in effect. Set as early as possible + (an inline script in base.html's ``, before any CSS paints) so nothing flashes. +* The choice is `localStorage.diora_theme` = `"dark"` (default) | `"light"` | `"auto"`. +* `window.__dioraTheme()` → the choice; `window.__dioraSetTheme(choice)` sets and applies it; + `window.__dioraThemeChanged` (event `diora-theme` on `window`, `detail` = the scheme in + effect) lets native code follow (Android status/navigation bars, Electron's window colour). +* "auto" follows `prefers-color-scheme` live. +* E-ink mode (`html.nui-eink`) keeps its own black-on-white rules and ignores this. +* The reader has its own themes; its default is now "Wie die App" (`readerSettings.theme = 'app'`): + dark → reader dark, light → reader bright. Sepia, Dunkel and Hell stay choosable. +* A wallpaper (the radio page's background image) keeps its own contrast handling + (`body.bright-bg`, the ◑ button); without a wallpaper the scheme decides. + +## Colours: only through these variables + +| variable | dark | light | for | +|---|---|---|---| +| `--bg` | `#000` | `#f6f5f1` | the page | +| `--fg` | `#fff` | `#17171a` | text | +| `--muted` | `#9a9a9a` | `#66666b` | secondary text, icons | +| `--card` | `#0b0b0b` | `#ffffff` | cards, rows that stand out | +| `--panel` | `#111` | `#ffffff` | sheets, menus, docks, bars | +| `--hover` | `#141414` | `#ecebe6` | hover / pressed | +| `--raised` | `#1a1a1a` | `#e4e2dc` | inputs, tracks of sliders, chips | +| `--border` | `#2a2a2a` | `#d9d6ce` | lines | +| `--border-strong` | `#444` | `#b4b0a6` | lines that must show (inputs, focus off) | +| `--accent` | `#e63946` | `#d0263a` | the red: brand, active, progress | +| `--on-accent` | `#fff` | `#fff` | text on the red | +| `--scrim` | `rgba(0,0,0,.6)` | `rgba(20,20,20,.35)` | behind sheets | +| `--shadow` | `none` | `0 1px 3px rgba(0,0,0,.08)` | cards in light (no lines needed in dark) | + +Old names stay as aliases: `--nui-muted: var(--muted)`, `--nui-panel: var(--panel)`, +`--text: var(--fg)`, `--text-muted: var(--muted)`, `--bg-card: var(--card)`. +Images, album art, covers and Ambient's own picture keep their colours. Status colours +(`--green`, `--yellow`, `--red`) may stay as they are. diff --git a/static/css/app.css b/static/css/app.css index 36e4a4b..5689138 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -47,13 +47,13 @@ html, body { height: 100%; overflow: hidden; - scrollbar-color: #333 #000; + scrollbar-color: var(--raised) var(--bg); } ::-webkit-scrollbar { width: 8px; height: 8px; } -::-webkit-scrollbar-track { background: #000; } -::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; } -::-webkit-scrollbar-thumb:hover { background: #555; } +::-webkit-scrollbar-track { background: var(--bg); } +::-webkit-scrollbar-thumb { background: var(--raised); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: var(--border-strong); } :root { --bg: #000; @@ -77,6 +77,42 @@ html, body { --outline: #000; --text: var(--fg); --text-muted: var(--fg); + + /* The colour scheme (docs/theme.md): dark here, light below. Use only these. */ + --muted: #9a9a9a; + --card: #0b0b0b; + --panel: #111; + --hover: #141414; + --raised: #1a1a1a; + --border-strong: #444; + --on-accent: #fff; + --scrim: rgba(0, 0, 0, 0.6); + --shadow: none; + --surface: var(--panel); + color-scheme: dark; +} + +html[data-theme="light"] { + --bg: #f6f5f1; + --fg: #17171a; + --fg-muted: #17171a; + --muted: #66666b; + --card: #fff; + --panel: #fff; + --hover: #ecebe6; + --raised: #e4e2dc; + --border: #d9d6ce; + --border-strong: #b4b0a6; + --accent: #d0263a; + --accent-hover: #b51f31; + --on-accent: #fff; + --scrim: rgba(20, 20, 20, 0.35); + --shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + --bg-card: var(--card); + --bg-alt: var(--hover); + /* Outlined text is for wallpapers (body.bright-bg / a dark picture); on plain light, none. */ + --outline: transparent; + color-scheme: light; } /* inverted: black on white */ @@ -395,7 +431,7 @@ a:hover { width: 100%; height: 70vh; border: 0; - background: #000; + background: var(--bg); } .sub-tabs { @@ -477,7 +513,7 @@ a:hover { .data-table tbody tr:nth-child(odd) { background: transparent; } .data-table tbody tr:nth-child(even) { background: transparent; } -.data-table tbody tr:hover { background: rgba(255, 255, 255, 0.05); } +.data-table tbody tr:hover { background: var(--hover); } .empty-msg { color: var(--text-muted); @@ -508,14 +544,14 @@ a:hover { white-space: nowrap; } .btn:hover { - background: rgba(255, 255, 255, 0.07); - border-color: #444; + background: var(--hover); + border-color: var(--border-strong); } .btn-play { background: var(--accent); border-color: var(--accent); - color: #fff; + color: var(--on-accent); font-size: 0.9rem; padding: 0.4rem 1rem; } @@ -524,14 +560,14 @@ a:hover { border-color: var(--accent-hover); } .btn-play.playing { - background: #333; - border-color: #555; + background: var(--raised); + border-color: var(--border-strong); color: var(--text); } .btn-save { color: var(--yellow); - border-color: #444; + border-color: var(--border-strong); } .btn-save:hover { background: #2a2a00; @@ -546,7 +582,7 @@ a:hover { .btn-primary { background: var(--accent); border-color: var(--accent); - color: #fff; + color: var(--on-accent); } .btn-primary:hover { background: var(--accent-hover); @@ -562,6 +598,9 @@ a:hover { border-color: #a00; color: #fff; } +html[data-theme="light"] .btn-danger { background: #fdecee; border-color: #e7a3aa; color: #a3172a; } +html[data-theme="light"] .btn-danger:hover { background: #a3172a; border-color: #a3172a; color: #fff; } +html[data-theme="light"] .btn-save:hover { background: #fbf3d3; } .btn-lastfm { background: #d51007; @@ -688,6 +727,7 @@ a:hover { border-radius: var(--radius); padding: 0.5rem 0.75rem; } +html[data-theme="light"] .form-errors { background: #fdecee; border-color: #e7a3aa; } .field-help { color: var(--text-muted); @@ -929,7 +969,7 @@ a:hover { .recommendations-section { padding: 12px 16px 4px; - border-bottom: 1px solid #222; + border-bottom: 1px solid var(--border); } .recommendations-context { font-size: 0.75rem; @@ -1009,7 +1049,7 @@ a:hover { display: flex; align-items: center; gap: 10px; - border-bottom: 1px solid #1a1a1a; + border-bottom: 1px solid var(--border); } /* ========================================================= @@ -1108,13 +1148,13 @@ body.dnd-mode .timer-display { flex-wrap: wrap; gap: 6px; padding: 8px 16px; - border-bottom: 1px solid #1a1a1a; + border-bottom: 1px solid var(--border); } .mood-chip { background: transparent; - border: 1px solid #333; - color: #ccc; + border: 1px solid var(--border); + color: var(--fg); border-radius: 999px; padding: 3px 12px; font-size: 0.78rem; @@ -1123,8 +1163,8 @@ body.dnd-mode .timer-display { } .mood-chip:hover { - background: rgba(255, 255, 255, 0.05); - border-color: #555; + background: var(--hover); + border-color: var(--border-strong); } .focus-today { @@ -1274,7 +1314,7 @@ body.dnd-mode .timer-display { .sidebar-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.45); + background: var(--scrim); z-index: 299; } @@ -1334,7 +1374,7 @@ body.dnd-mode .timer-display { .modal-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.55); + background: var(--scrim); z-index: 500; } @@ -1417,7 +1457,7 @@ body.dnd-mode .timer-display { .book-drop-zone:hover, .book-drop-zone.drag-over { border-color: var(--accent); - background: rgba(255,255,255,0.04); + background: var(--hover); } .book-drop-zone label { color: var(--accent); @@ -1791,13 +1831,13 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h) display: flex; align-items: center; justify-content: center; - background: rgba(20, 20, 20, 0.85); + background: var(--scrim); z-index: 200; } .pdf-loading-spinner { width: 36px; height: 36px; - border: 3px solid #333; + border: 3px solid var(--raised); border-top-color: var(--accent, #e63946); border-radius: 50%; animation: pdf-spin 0.7s linear infinite; @@ -1814,7 +1854,7 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h) /* Highlight popover */ .highlight-popover { position:fixed; z-index:500; display:flex; gap:6px; background:var(--bg-card,#1a1a1a); border:1px solid var(--border); border-radius:var(--radius); padding:6px 8px; box-shadow:0 4px 16px rgba(0,0,0,.5); } .hl-color-btn { width:24px; height:24px; border-radius:50%; border:2px solid transparent; cursor:pointer; font-weight:700; font-size:12px; color:#000; line-height:1; } -.hl-color-btn:hover { border-color:#fff; } +.hl-color-btn:hover { border-color:var(--fg); } .hl-note-btn { background:none; border:1px solid var(--border); color:var(--fg); padding:2px 6px; border-radius:var(--radius); cursor:pointer; } /* Footnote popover */ @@ -1908,7 +1948,7 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h) 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); + position:absolute; inset:0; background:var(--scrim); opacity:0; transition:opacity .2s ease; pointer-events:none; } .note-bottom-sheet.open .note-bottom-sheet-backdrop { opacity:1; pointer-events:auto; } @@ -1951,7 +1991,9 @@ mark.tts-current { background:rgba(230,57,70,.55); color:inherit; border-radius: @keyframes toast-fade { 0%,70%{opacity:1} 100%{opacity:0} } .reader-toast-action { display:flex; align-items:center; gap:10px; padding:8px 8px 8px 14px; animation:toast-slide-up 0.2s ease forwards; pointer-events:auto; } -.reader-toast-btn { background:var(--accent,#e63946); color:#fff; border:none; border-radius:calc(var(--radius) - 2px); padding:5px 12px; font-family:var(--font); font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; } +/* The apps: the player bar sits in the reader, above its footer; notices go above both. */ +html.nui-reading .reader-toast { bottom:calc(var(--nui-dock-h, 0px) + 40px); } +.reader-toast-btn { background:var(--accent,#e63946); color:var(--on-accent); border:none; border-radius:calc(var(--radius) - 2px); padding:5px 12px; font-family:var(--font); font-size:13px; font-weight:600; cursor:pointer; white-space:nowrap; } .reader-toast-btn:hover { filter:brightness(1.1); } @keyframes toast-slide-up { 0%{opacity:0; transform:translate(-50%, 8px);} 100%{opacity:1; transform:translate(-50%, 0);} } @@ -1960,7 +2002,7 @@ mark.tts-current { background:rgba(230,57,70,.55); color:inherit; border-radius: bottom: 4px; right: 6px; font-size: 0.65rem; - color: #444; + color: var(--border-strong); pointer-events: none; z-index: 1; } diff --git a/static/js/app.js b/static/js/app.js index e7ac60e..1dad5a3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1629,6 +1629,12 @@ function applyAccent(entry) { root.style.setProperty('--accent-hover', entry.hover); } +// Light and dark: the switch is in base.html (every page has it). The reader's +// "Wie die App" follows it. +window.addEventListener('diora-theme', () => { + if (currentBookId) applyReaderSettings(!!currentPdfDoc); +}); + function setScheme(bright) { document.body.classList.toggle('bright-bg', bright); const btn = document.getElementById('contrast-toggle'); @@ -1736,6 +1742,8 @@ async function applyEncryptedBackground() { const blob = new Blob([plain], {type: ENCRYPTED_BG.mime || 'image/jpeg'}); const url = URL.createObjectURL(blob); document.body.style.backgroundImage = `url('${url}')`; + const contrastBtn = document.getElementById('contrast-toggle'); + if (contrastBtn) contrastBtn.hidden = false; // only with a picture behind the page document.body.style.backgroundSize = 'cover'; document.body.style.backgroundPosition = 'center'; document.body.style.backgroundAttachment = 'fixed'; @@ -3770,6 +3778,8 @@ let ttsAudio = null; let ttsCurrentMark = null; let ttsBarEl = null; let ttsLang = localStorage.getItem('diora_tts_lang') || 'de'; +let _ttsBlocks = null; // the book's blocks while reading aloud +let _ttsCur = null; // what's being read: {bi, si, sentence, block} function setTtsLang(lang) { ttsLang = lang; @@ -3862,15 +3872,19 @@ function _ttsPlayBlob(blob) { }); } -// Walks blocks/sentences from startBlockIndex onward, fetching one sentence -// ahead while the current one plays so there's no gap between them. -async function _ttsPlayLoop(blocks, startBlockIndex, runToken) { +// Walks blocks/sentences from startBlockIndex (and its sentence startSentence) +// onward, fetching one sentence ahead while the current one plays so there's +// no gap between them. +async function _ttsPlayLoop(blocks, startBlockIndex, runToken, startSentence = 0) { function* sentenceStream() { for (let bi = startBlockIndex; bi < blocks.length; bi++) { const block = blocks[bi]; const text = (block.textContent || '').trim(); if (!text) continue; - for (const sentence of _ttsSplitSentences(text)) yield {block, sentence}; + const sentences = _ttsSplitSentences(text); + for (let si = bi === startBlockIndex ? startSentence : 0; si < sentences.length; si++) { + yield {block, sentence: sentences[si], bi, si}; + } } } @@ -3896,8 +3910,14 @@ async function _ttsPlayLoop(blocks, startBlockIndex, runToken) { } if (runToken !== ttsRunToken) return; - if (block !== lastBlock) { lastBlock = block; blockSearchOffset = 0; } + if (block !== lastBlock) { + lastBlock = block; + // Started in the middle of a block (previous/next sentence): find it from there. + blockSearchOffset = _ttsOffsetOf(block, cur.value.si); + } + _ttsCur = {bi: cur.value.bi, si: cur.value.si, sentence, block}; blockSearchOffset = _ttsHighlightSentence(block, sentence, blockSearchOffset); + _ttsNotify(); await _ttsPlayBlob(audioBlob); if (runToken !== ttsRunToken) return; @@ -3915,8 +3935,90 @@ function _ttsTogglePause() { else ttsAudio.play().catch(() => {}); } _ttsUpdateBar(); + _ttsNotify(); } +// ---- read-aloud for a player bar (the shared dock in native-podcast.js) ---------- +// window.__dioraTts: state(), toggle(), stop(), next(), prev(); a 'diora-tts' +// event on window after every change. A dock that shows it sets +// window.__dioraTtsDock = true, and the reader's own little bar stays away. + +function _ttsState() { + const meta = (currentBookId && bookMetaCache[currentBookId]) || {}; + const cur = _ttsCur; + const chapterEl = cur && cur.block.closest('[data-epub-src]'); + const blocks = _ttsBlocks || []; + return { + active: ttsActive, + paused: ttsPaused, + bookId: currentBookId, + title: meta.title || '', + author: meta.author || '', + chapter: chapterEl ? _tocTitleForChapterSrc(chapterEl.dataset.epubSrc) : '', + sentence: cur ? cur.sentence : '', + progress: cur && blocks.length ? cur.bi / blocks.length : 0, + }; +} + +function _ttsNotify() { + try { window.dispatchEvent(new CustomEvent('diora-tts', {detail: _ttsState()})); } catch (e) {} +} + +// Read from another sentence on: the one before/after the current one. +function _ttsRestartAt(bi, si) { + const blocks = _ttsBlocks; + if (!ttsActive || !blocks) return; + ttsRunToken++; + const runToken = ttsRunToken; + if (ttsAudio) { + try { ttsAudio.pause(); ttsAudio.src = ''; } catch (e) {} + ttsAudio = null; + } + _ttsPlayLoop(blocks, bi, runToken, si); +} + +// Where sentence si of a block starts in its text (for the highlight). +function _ttsOffsetOf(block, si) { + const full = block.textContent || ''; + let off = 0; + _ttsSplitSentences(full.trim()).slice(0, si).forEach(s => { + const k = full.indexOf(s, off); + if (k >= 0) off = k + s.length; + }); + return off; +} + +function _ttsSentenceCount(block) { + const text = (block.textContent || '').trim(); + return text ? _ttsSplitSentences(text).length : 0; +} + +function _ttsStep(dir) { + const cur = _ttsCur; + const blocks = _ttsBlocks; + if (!cur || !blocks) return; + if (dir > 0) { + if (cur.si + 1 < _ttsSentenceCount(cur.block)) return _ttsRestartAt(cur.bi, cur.si + 1); + for (let bi = cur.bi + 1; bi < blocks.length; bi++) if (_ttsSentenceCount(blocks[bi])) return _ttsRestartAt(bi, 0); + stopReadAloud(); + } else { + if (cur.si > 0) return _ttsRestartAt(cur.bi, cur.si - 1); + for (let bi = cur.bi - 1; bi >= 0; bi--) { + const n = _ttsSentenceCount(blocks[bi]); + if (n) return _ttsRestartAt(bi, n - 1); + } + _ttsRestartAt(cur.bi, 0); + } +} + +window.__dioraTts = { + state: _ttsState, + toggle: () => (ttsActive ? _ttsTogglePause() : startReadAloud()), + stop: () => stopReadAloud(), + next: () => _ttsStep(1), + prev: () => _ttsStep(-1), +}; + function _ttsUpdateBar() { if (!ttsBarEl) return; const btn = ttsBarEl.querySelector('.tts-pause-btn'); @@ -3925,6 +4027,7 @@ function _ttsUpdateBar() { function _ttsShowBar() { _ttsRemoveBar(); + if (window.__dioraTtsDock) return; // the player bar shows it const bar = document.createElement('div'); bar.className = 'tts-bar'; bar.innerHTML = ` @@ -3968,10 +4071,13 @@ function startReadAloud() { ttsPaused = false; ttsRunToken++; const runToken = ttsRunToken; + _ttsBlocks = blocks; + _ttsCur = null; const btn = $('reader-tts-btn'); if (btn) { btn.classList.add('active'); btn.title = 'Vorlesen beenden'; } _ttsShowBar(); + _ttsNotify(); _ttsPlayLoop(blocks, startIndex, runToken); } @@ -3986,8 +4092,11 @@ function stopReadAloud() { } _ttsClearHighlight(); _ttsRemoveBar(); + _ttsBlocks = null; + _ttsCur = null; const btn = $('reader-tts-btn'); if (btn) { btn.classList.remove('active'); btn.title = 'Vorlesen'; } + _ttsNotify(); } // --------------------------------------------------------------------------- @@ -4647,7 +4756,7 @@ function _readerTypeDefaults() { function loadReaderSettings(bookId) { // Reset to defaults, then apply per-book overrides - Object.assign(readerSettings, { theme: 'dark', fontFamily: 'serif', noBold: false, + Object.assign(readerSettings, { theme: 'app', fontFamily: 'serif', noBold: false, pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false }, _readerTypeDefaults()); try { @@ -4659,6 +4768,8 @@ function loadReaderSettings(bookId) { if (saved[key] === old) delete saved[key]; } } + // Before v3 the reader's theme was always stored, "dark" by default: that now means "Wie die App". + if ((saved.v || 0) < 3 && saved.theme === 'dark') delete saved.theme; delete saved.v; Object.assign(readerSettings, saved); if (saved.pdfPaginated === undefined) { @@ -4669,7 +4780,7 @@ function loadReaderSettings(bookId) { function saveReaderSettings() { if (currentBookId) { - localStorage.setItem(`diora_reader_settings_${currentBookId}`, JSON.stringify({ ...readerSettings, v: 2 })); + localStorage.setItem(`diora_reader_settings_${currentBookId}`, JSON.stringify({ ...readerSettings, v: 3 })); } } @@ -4708,8 +4819,11 @@ function applyReaderSettings(isPdf) { // Theme overlay.classList.remove('reader-theme-sepia', 'reader-theme-bright'); - if (readerSettings.theme === 'sepia') overlay.classList.add('reader-theme-sepia'); - else if (readerSettings.theme === 'bright') overlay.classList.add('reader-theme-bright'); + const readerTheme = readerSettings.theme === 'app' + ? (document.documentElement.getAttribute('data-theme') === 'light' ? 'bright' : 'dark') + : readerSettings.theme; + if (readerTheme === 'sepia') overlay.classList.add('reader-theme-sepia'); + else if (readerTheme === 'bright') overlay.classList.add('reader-theme-bright'); // PDF invert if (isPdf && readerSettings.pdfInverted) overlay.classList.add('pdf-inverted'); @@ -4767,6 +4881,7 @@ function toggleSettingsPanel() { + diff --git a/static/js/home.js b/static/js/home.js index e50cf7f..712ad66 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -331,27 +331,27 @@ '#tab-home .home-brand { font-size: 26px; font-weight: 700; color: var(--accent, #e63946); margin: 4px 0 18px; letter-spacing: 0.05em; }', '#tab-home .home-section { margin-bottom: 22px; }', '#tab-home .home-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 8px; }', - '#tab-home .home-head h2 { margin: 0; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--nui-muted, #9a9a9a); }', - '#tab-home .home-more { background: none; border: none; color: var(--nui-muted, #9a9a9a); font: inherit; font-size: 12px; cursor: pointer; padding: 4px 0; }', - '#tab-home .home-card { display: flex; align-items: center; gap: 12px; width: 100%; box-sizing: border-box; padding: 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: #0b0b0b; color: inherit; font: inherit; text-align: left; }', + '#tab-home .home-head h2 { margin: 0; font-size: 12px; font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted, #9a9a9a); }', + '#tab-home .home-more { background: none; border: none; color: var(--muted, #9a9a9a); font: inherit; font-size: 12px; cursor: pointer; padding: 4px 0; }', + '#tab-home .home-card { display: flex; align-items: center; gap: 12px; width: 100%; box-sizing: border-box; padding: 14px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); color: inherit; font: inherit; text-align: left; }', '#tab-home button.home-card, #tab-home .home-pod-text, #tab-home .home-row, #tab-home .home-station { cursor: pointer; }', '#tab-home .home-main { flex-wrap: nowrap; }', '#tab-home button.home-main { flex-direction: column; align-items: flex-start; gap: 3px; }', - '#tab-home .home-empty { color: var(--nui-muted, #9a9a9a); justify-content: space-between; }', + '#tab-home .home-empty { color: var(--muted, #9a9a9a); justify-content: space-between; }', '#tab-home .home-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent, #e63946); }', '#tab-home .home-title { font-size: 16px; font-weight: 700; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }', - '#tab-home .home-sub { font-size: 13px; color: var(--nui-muted, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }', - '#tab-home .home-state { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12px; color: var(--nui-muted, #9a9a9a); }', - '#tab-home .home-bar { display: inline-block; width: 160px; max-width: 40vw; height: 4px; border-radius: 2px; background: #2a2a2a; overflow: hidden; }', + '#tab-home .home-sub { font-size: 13px; color: var(--muted, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }', + '#tab-home .home-state { display: flex; align-items: center; gap: 8px; margin-top: 6px; font-size: 12px; color: var(--muted, #9a9a9a); }', + '#tab-home .home-bar { display: inline-block; width: 160px; max-width: 40vw; height: 4px; border-radius: 2px; background: var(--raised, #2a2a2a); overflow: hidden; }', '#tab-home .home-bar > span { display: block; height: 100%; background: var(--accent, #e63946); }', '#tab-home .home-pod-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }', - '#tab-home .home-art { width: 56px; height: 56px; flex: none; border-radius: 4px; object-fit: cover; background: #1a1a1a; }', + '#tab-home .home-art { width: 56px; height: 56px; flex: none; border-radius: 4px; object-fit: cover; background: var(--raised, #1a1a1a); }', '#tab-home .home-amb-icon { width: 56px; height: 56px; flex: none; border-radius: 4px; background: #14102a; color: #b9a7ff; display: inline-flex; align-items: center; justify-content: center; font-size: 26px; }', '#tab-home .home-play { flex: none; min-height: 36px; padding: 0 14px; }', '#tab-home .home-list { margin-top: 6px; }', '#tab-home .home-row { display: flex; justify-content: space-between; gap: 12px; width: 100%; padding: 10px 14px; background: none; border: none; border-bottom: 1px solid var(--border, #2a2a2a); color: inherit; font: inherit; font-size: 14px; text-align: left; }', '#tab-home .home-row-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', - '#tab-home .home-row-side { flex: none; color: var(--nui-muted, #9a9a9a); font-size: 12px; }', + '#tab-home .home-row-side { flex: none; color: var(--muted, #9a9a9a); font-size: 12px; }', '#tab-home .home-news { flex-direction: column; align-items: stretch; gap: 0; padding: 4px 0; }', '#tab-home .home-story { align-items: baseline; }', '#tab-home .home-news .home-story:last-child { border-bottom: none; }', @@ -360,12 +360,12 @@ '#tab-home .home-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }', '#tab-home .home-chip { padding: 6px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 16px; background: none; color: inherit; font: inherit; font-size: 13px; cursor: pointer; }', '#tab-home .home-stations { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px; }', - '#tab-home .home-station { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: #0b0b0b; color: inherit; font: inherit; font-size: 14px; text-align: left; }', + '#tab-home .home-station { display: flex; align-items: center; gap: 10px; min-height: 48px; padding: 8px 12px; border: 1px solid var(--border, #2a2a2a); border-radius: 6px; background: var(--card, #0b0b0b); box-shadow: var(--shadow, none); color: inherit; font: inherit; font-size: 14px; text-align: left; }', '#tab-home .home-station-icon { color: var(--accent, #e63946); flex: none; }', '#tab-home .home-station-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', '#tab-home .home-on { border-color: var(--accent, #e63946); }', - '#tab-home .home-note { margin-top: 8px; font-size: 13px; color: var(--nui-muted, #9a9a9a); }', - '@media (hover: hover) { #tab-home .home-card:hover, #tab-home .home-station:hover, #tab-home .home-row:hover { background: #141414; } }', + '#tab-home .home-note { margin-top: 8px; font-size: 13px; color: var(--muted, #9a9a9a); }', + '@media (hover: hover) { #tab-home .home-card:hover, #tab-home .home-station:hover, #tab-home .home-row:hover { background: var(--hover, #141414); } }', '.reader-theme-bright #tab-home, body.eink #tab-home .home-card { background: #fff; }', ].join('\n'); document.head.appendChild(style); diff --git a/static/js/sw.js b/static/js/sw.js index 2113fd9..32bd9f3 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v101'; +const CACHE = 'diora-v102'; const SHELL = [ '/static/css/app.css', '/static/js/app.js', diff --git a/templates/base.html b/templates/base.html index 64f4baa..9d26a8a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,6 +11,50 @@ {% if user.is_authenticated %}{% endif %} + {% block title %}diora{% endblock %} @@ -22,7 +66,8 @@

eC$y!XFj=ddNNY<( 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 literal 0 HcmV?d00001 diff --git a/static/fonts/SourceSerif4-normal-latin.woff2 b/static/fonts/SourceSerif4-normal-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..b98e6b594dd7bd3e8e74245456d6c4a1c1308f79 GIT binary patch literal 122360 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%0DeRj2H1&B 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 Date: Sat, 12 Sep 2026 15:11:50 +0200 Subject: [PATCH 016/176] Reader: Source Serif 4 etwas leichter (400 -> 380) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Mxq5zi49Qs9FyQEQAeR8xc --- static/css/app.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/app.css b/static/css/app.css index 9418cda..5b200a9 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -1980,7 +1980,7 @@ body.dnd-mode .timer-display { line-height: 1.8; font-family: 'Source Serif 4', Georgia, 'Times New Roman', serif; font-size: 16px; - font-weight: 400; + font-weight: 380; } .reader-content > * { max-width: 65ch; From 9d097f5fbaa9cc02d362313d3737754c445230dd Mon Sep 17 00:00:00 2001 From: marwin Date: Thu, 24 Sep 2026 10:49:09 +0000 Subject: [PATCH 017/176] =?UTF-8?q?Reader:=20Notiz-Randspalte=20auf=20dem?= =?UTF-8?q?=20Handy=20nicht=20automatisch=20=C3=B6ffnen=20(SW=20v45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim Öffnen eines Buchs mit Markierungen klappte die Randspalte auch auf schmalen Bildschirmen auf und verdeckte den Text. Auf Mobil (_isMobileReader) bleibt sie jetzt zu und ist weiter über ✎ erreichbar. Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 3 ++- static/js/sw.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index ff4f1d6..751d2b1 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4552,7 +4552,8 @@ async function openBook(bookId) { // If this book already has notes/highlights, surface them right away // instead of making the reader hide them behind a manual toggle. - if (!isPdfBook && currentHighlights.length && !readerAnnotationsMarginOpen) { + // Not on phones: there the margin covers the text; it stays one tap away (✎). + if (!isPdfBook && currentHighlights.length && !readerAnnotationsMarginOpen && !_isMobileReader()) { toggleAnnotationsMargin(); } diff --git a/static/js/sw.js b/static/js/sw.js index 72ac2ca..f99fe3a 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v44'; +const CACHE = 'diora-v45'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From 09055b98742809b32ed7aeaeef951313f674ce0d Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Sun, 27 Sep 2026 19:57:10 +0200 Subject: [PATCH 018/176] Reader: Lesefortschritt nach Textanteil statt Pixeln (SW v46) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Fortschritt (Speichern/Sync, Prozentanzeige im Reader-Kopf, Sprung über das Prozentfeld, "≈ x % im Buch" bei Markierungen) wird jetzt aus dem Positions-Anker als Anteil des Buchtexts in Zeichen berechnet statt aus scrollTop/scrollHeight. Das ist unabhängig vom Layout (Schriftgröße, Breite, welche Kapitel gerendert sind) und damit auf allen Clients gleich — die Android-App rendert im E-Reader-Modus nur ein Kapitel und kennt die Pixelhöhe des Buchs nicht. Pixel zählten außerdem Ränder, Überschriften und Bilder mit. Anker und Server-Logik bleiben unverändert; PDFs weiter nach Seiten. Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 83 ++++++++++++++++++++++++++++++++++++++++++++---- static/js/sw.js | 2 +- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 751d2b1..64a27d3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2966,6 +2966,59 @@ function getPositionAnchor(contentEl) { return `${bestIndex}:${innerFraction.toFixed(6)}`; } +// Reading progress as a share of the book's text (characters), from a +// position anchor. Independent of layout — font size, screen width, which +// chapters are rendered (the Android app's e-reader mode lays out one chapter +// at a time) — so every client shows the same percentage for the same place. +// The pixel share (scrollTop / scrollHeight) used before differed between +// layouts and counted margins, headings and images as reading. +let _charIndex = null; // {content, starts[], lens[], total} for the open book + +function _bookCharIndex(contentEl) { + if (_charIndex && _charIndex.content === contentEl) return _charIndex; + const offsets = new Map(); + let pos = 0; + const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_TEXT); + let node; + while ((node = walker.nextNode())) { + offsets.set(node, pos); + pos += node.nodeValue.length; + } + const blocks = contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR); + const starts = new Array(blocks.length); + const lens = new Array(blocks.length); + for (let i = 0; i < blocks.length; i++) { + const first = document.createTreeWalker(blocks[i], NodeFilter.SHOW_TEXT).nextNode(); + starts[i] = first ? offsets.get(first) : (i ? starts[i - 1] + lens[i - 1] : 0); + lens[i] = blocks[i].textContent.length; + } + _charIndex = {content: contentEl, starts, lens, total: pos || 1}; + return _charIndex; +} + +// Share of the text before the anchored place (0..1), or null. +function _fractionFromAnchor(contentEl, anchor) { + const [block, inner] = _anchorParts(anchor); + if (block < 0) return null; + const idx = _bookCharIndex(contentEl); + if (block >= idx.starts.length) return null; + return Math.max(0, Math.min(1, (idx.starts[block] + inner * idx.lens[block]) / idx.total)); +} + +// Anchor of the place where `fraction` of the text has been read. +function _anchorForFraction(contentEl, fraction) { + const idx = _bookCharIndex(contentEl); + if (!idx.starts.length) return ''; + const target = Math.max(0, Math.min(1, fraction)) * idx.total; + let lo = 0, hi = idx.starts.length - 1; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if (idx.starts[mid] <= target) lo = mid; else hi = mid - 1; + } + const inner = idx.lens[lo] ? Math.max(0, Math.min(1, (target - idx.starts[lo]) / idx.lens[lo])) : 0; + return `${lo}:${inner.toFixed(6)}`; +} + function restoreFromAnchor(contentEl, anchor) { if (!anchor) return false; const parts = anchor.split(':'); @@ -4464,6 +4517,7 @@ async function openBook(bookId) { try { loadReaderSettings(bookId); + _charIndex = null; const key = await getOrCreateEncKey(); let data_ct, data_iv; const cached = await _getCachedBook(bookId); @@ -4591,7 +4645,7 @@ async function openBook(bookId) { } else { const pct = Math.min(100, Math.max(0, _parseLocalePercent(this.value))); this.value = _formatBookPercent(pct / 100); - contentEl.scrollTop = (pct / 100) * contentEl.scrollHeight; + restoreFromAnchor(contentEl, _anchorForFraction(contentEl, pct / 100)); } }); @@ -4660,10 +4714,8 @@ async function openBook(bookId) { } } progressInput.value = currentPage; - } else { - const f = contentEl.scrollTop / (contentEl.scrollHeight - contentEl.clientHeight || 1); - progressInput.value = _formatBookPercent(f); } + // EPUB: shown from the position anchor, see the anchor tracking below. }); // Auto-save progress every 10s and on scroll (debounced 2s) @@ -4684,6 +4736,10 @@ async function openBook(bookId) { clearTimeout(_anchorTrackDebounce); _anchorTrackDebounce = setTimeout(() => { _currentPositionAnchor = getPositionAnchor(contentEl); + const f = _fractionFromAnchor(contentEl, _currentPositionAnchor); + if (progressInput && f !== null && document.activeElement !== progressInput) { + progressInput.value = _formatBookPercent(f); + } }, 150); }, {passive: true}); } @@ -4787,13 +4843,16 @@ async function saveReaderProgress(force = false) { } else { fraction = contentEl.scrollTop / (contentEl.scrollHeight - contentEl.clientHeight || 1); } - fraction = Math.min(1.0, Math.max(0.0, fraction)); let anchor = ''; if (!currentPdfDoc) { anchor = getPositionAnchor(contentEl); _currentPositionAnchor = anchor; + // EPUB: share of the text, the same on every device and layout. + const textFraction = _fractionFromAnchor(contentEl, anchor); + if (textFraction !== null) fraction = textFraction; } + fraction = Math.min(1.0, Math.max(0.0, fraction)); _updateCachedBookProgress(currentBookId, fraction, anchor, force); @@ -6556,7 +6615,19 @@ function _tocTitleForChapterSrc(chapterSrc) { // relocate a highlight/note later. function _percentFromRange(range) { const contentEl = $('reader-content'); - if (!contentEl || !range || !contentEl.scrollHeight) return null; + if (!contentEl || !range) return null; + // Share of the text before the range, like the reading progress. + const block = _blockAncestor(range.startContainer); + if (block && contentEl.contains(block)) { + const blocks = Array.from(contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR)); + const i = blocks.indexOf(block); + if (i >= 0) { + const idx = _bookCharIndex(contentEl); + const at = idx.starts[i] + _charOffsetInBlock(block, range.startContainer, range.startOffset); + return Math.max(0, Math.min(100, (at / idx.total) * 100)); + } + } + if (!contentEl.scrollHeight) return null; const contentRect = contentEl.getBoundingClientRect(); const rangeRect = range.getBoundingClientRect(); if (!rangeRect.width && !rangeRect.height) return null; diff --git a/static/js/sw.js b/static/js/sw.js index f99fe3a..24a7b9c 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v45'; +const CACHE = 'diora-v46'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From 7ee44622ee2de09e7720ac8f85409be41c8c93f0 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Mon, 28 Sep 2026 20:25:59 +0200 Subject: [PATCH 019/176] Reader: Schriftgewicht einstellbar (SW v47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Regler im Einstellungs-Panel, 200-800: bei Source Serif 4 (variabel) in 25er-Schritten, bei Systemschriften in 100ern. null behält das Gewicht aus dem Stylesheet. --reader-weight erlaubt alten WebViews ohne variable Schriften, einen statischen Schnitt zu wählen. "Kein Fett" erbt jetzt das eingestellte Gewicht statt fest normal. Co-Authored-By: Claude Opus 5.5 --- static/css/app.css | 2 +- static/js/app.js | 52 ++++++++++++++++++++++++++++++++++++++++++++-- static/js/sw.js | 2 +- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/static/css/app.css b/static/css/app.css index 5b200a9..fd2bcc6 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -2130,7 +2130,7 @@ body.reader-immersive.reader-show-bottom .reader-overlay { bottom: var(--bar-h) .footnote-popover-close { position:absolute; top:4px; right:6px; background:none; border:none; color:inherit; opacity:0.5; cursor:pointer; font-size:14px; padding:2px 4px; line-height:1; } /* Highlight marks */ -.reader-no-bold * { font-weight: normal !important; } +.reader-no-bold * { font-weight: inherit !important; } .reader-content img { max-width: 100%; height: auto; display: block; } /* color:inherit overrides the browser's UA-default `mark { color: black }` — without it, highlighted text abruptly flips to black regardless of the diff --git a/static/js/app.js b/static/js/app.js index 64a27d3..cf513db 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -3254,7 +3254,7 @@ async function _syncPendingAnnotations() { // Reader settings let readerSettings = { fontSize: 16, lineHeight: 1.8, maxWidth: 65, theme: 'dark', - fontFamily: 'serif', noBold: false, + fontFamily: 'serif', noBold: false, fontWeight: null, pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false }; let readerSettingsPanelOpen = false; let currentPdfDoc = null; @@ -5026,7 +5026,7 @@ function jumpToTocEntry(href) { function loadReaderSettings(bookId) { // Reset to defaults, then apply per-book overrides Object.assign(readerSettings, { fontSize: 16, lineHeight: 1.8, maxWidth: 65, theme: 'dark', - fontFamily: 'serif', noBold: false, + fontFamily: 'serif', noBold: false, fontWeight: null, pdfZoom: 100, pdfInverted: false, pdfPaginated: false, pdfSpread: false }); try { const saved = JSON.parse(localStorage.getItem(`diora_reader_settings_${bookId}`) || '{}'); @@ -5060,6 +5060,16 @@ function applyReaderSettings(isPdf) { }; contentEl.style.fontFamily = fontMap[readerSettings.fontFamily] || fontMap.serif; contentEl.classList.toggle('reader-no-bold', !!readerSettings.noBold); + // Weight: null keeps the stylesheet's. --reader-weight lets old WebViews + // without variable-font support (which drop weights like 425) pick a + // static cut instead. + contentEl.style.fontWeight = ''; + if (readerSettings.fontWeight) { + contentEl.style.fontWeight = String(readerSettings.fontWeight); + contentEl.style.setProperty('--reader-weight', String(readerSettings.fontWeight)); + } else { + contentEl.style.removeProperty('--reader-weight'); + } if (_currentPositionAnchor && currentBookId) { _suppressScrollJumpDetect(); requestAnimationFrame(() => restoreFromAnchor($('reader-content'), _currentPositionAnchor)); @@ -5083,6 +5093,19 @@ function applyReaderSettings(isPdf) { if (isPdf && markerModeActive) toggleMarkerMode(); } +// Font weight slider: the variable Source Serif 4 takes any weight, the +// system fonts only their fixed cuts (multiples of 100). +function _readerWeightStep() { + return readerSettings.fontFamily === 'serif' ? 25 : 100; +} + +function _readerWeight() { + const step = _readerWeightStep(); + const w = readerSettings.fontWeight + || parseInt(getComputedStyle($('reader-content')).fontWeight, 10) || 400; + return Math.round(w / step) * step; +} + function toggleSettingsPanel() { const overlay = $('reader-overlay'); const contentEl = $('reader-content'); @@ -5108,6 +5131,7 @@ function toggleSettingsPanel() { + @@ -5187,12 +5211,36 @@ function toggleSettingsPanel() { saveReaderSettings(); }); + const weightRange = panel.querySelector('#rs-weight'); + const weightVal = panel.querySelector('#rs-weight-val'); + weightRange.addEventListener('input', () => { + readerSettings.fontWeight = parseInt(weightRange.value, 10); + weightVal.textContent = readerSettings.fontWeight; + applyReaderSettings(false); + saveReaderSettings(); + }); + panel.querySelector('#rs-weight-minus').addEventListener('click', () => { + weightRange.value = Math.max(+weightRange.min, parseInt(weightRange.value, 10) - +weightRange.step); + weightRange.dispatchEvent(new Event('input')); + }); + panel.querySelector('#rs-weight-plus').addEventListener('click', () => { + weightRange.value = Math.min(+weightRange.max, parseInt(weightRange.value, 10) + +weightRange.step); + weightRange.dispatchEvent(new Event('input')); + }); + panel.querySelectorAll('[data-rs-font]').forEach(btn => { btn.addEventListener('click', () => { readerSettings.fontFamily = btn.dataset.rsFont; panel.querySelectorAll('[data-rs-font]').forEach(b => b.classList.toggle('active', b === btn)); + // The system fonts only have cuts at multiples of 100. + weightRange.step = _readerWeightStep(); + if (readerSettings.fontWeight) { + readerSettings.fontWeight = Math.round(readerSettings.fontWeight / weightRange.step) * weightRange.step; + } applyReaderSettings(false); saveReaderSettings(); + weightRange.value = _readerWeight(); + weightVal.textContent = _readerWeight(); }); }); diff --git a/static/js/sw.js b/static/js/sw.js index 24a7b9c..2b72b57 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v46'; +const CACHE = 'diora-v47'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From 086ce29020022d39c8fe01b8729cf909dee66b30 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Mon, 28 Sep 2026 21:19:05 +0200 Subject: [PATCH 020/176] =?UTF-8?q?Reader:=20Buch=20nie=20doppelt=20=C3=B6?= =?UTF-8?q?ffnen;=20ung=C3=BCltige=20Anker=20selbst=20reparieren=20(SW=20v?= =?UTF-8?q?48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openBook leert #reader-content und hängt die Kapitel über mehrere awaits an. Ein zweiter Aufruf dazwischen (zweites Tippen, während ein langsamer E-Reader noch entschlüsselt) hing das Buch doppelt an. Daraus gespeicherte Anker lagen hinter dem echten Buchende (tolino: Block 1491 bei 1206 Blöcken) und schlugen beim Server-Vergleich "weitester Stand gewinnt" jede echte Position: Fortschritt wurde nicht mehr gespeichert. - openBook läuft nur einmal gleichzeitig (gleiches Buch: auf den laufenden Aufruf warten; anderes Buch: danach öffnen). - Aus einem doppelt gerenderten Dokument wird kein Fortschritt gespeichert. - Ein gespeicherter Anker, der nicht zum Buch passt, führt zum Textanteil (scroll_fraction) statt an den Anfang, und die nächste Speicherung überschreibt ihn (force), damit der Fortschritt wieder läuft. Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 45 ++++++++++++++++++++++++++++++++++++++++++--- static/js/sw.js | 2 +- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index cf513db..21376a4 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -4505,7 +4505,30 @@ function exitReaderImmersiveMode() { document.removeEventListener('click', _immHandleTap); } +// Serializes openBook: it empties #reader-content, then appends the chapters +// across several awaits. A second call in between (a second tap while a slow +// e-reader is still decrypting) appended the whole book a second time — and +// progress saved from that doubled DOM had block indices beyond the real book, +// which then out-ranked every real position on the server. +let _openBookInFlight = null; +let _repairProgressAnchor = false; // stored anchor didn't fit this book + async function openBook(bookId) { + if (_openBookInFlight) { + if (_openBookInFlight.bookId === bookId) return _openBookInFlight.promise; + await _openBookInFlight.promise.catch(() => {}); + } + const promise = _openBookOnce(bookId); + _openBookInFlight = {bookId, promise}; + try { + return await promise; + } finally { + if (_openBookInFlight && _openBookInFlight.promise === promise) _openBookInFlight = null; + } +} + +async function _openBookOnce(bookId) { + _repairProgressAnchor = false; const overlay = $('reader-overlay'); const contentEl = $('reader-content'); const titleEl = $('reader-title'); @@ -4690,7 +4713,14 @@ async function openBook(bookId) { } await new Promise(r => requestAnimationFrame(r)); if (!restoreFromAnchor(contentEl, anchor) && fraction > 0) { - contentEl.scrollTop = fraction * (contentEl.scrollHeight - contentEl.clientHeight); + restoreFromAnchor(contentEl, _anchorForFraction(contentEl, fraction)); + } + // An anchor past the end of this book can only come from a broken + // render. The server keeps the furthest block index, so it would block + // every real position from now on: overwrite it with the next save. + const [storedBlock] = _anchorParts(anchor); + if (storedBlock >= contentEl.querySelectorAll(EPUB_BLOCK_SELECTOR).length) { + _repairProgressAnchor = true; } } } catch (e) {} @@ -4844,6 +4874,13 @@ async function saveReaderProgress(force = false) { fraction = contentEl.scrollTop / (contentEl.scrollHeight - contentEl.clientHeight || 1); } + // Chapter wrappers carry unique ids; seeing one twice means the book was + // rendered twice. Positions from that DOM are wrong — don't store them. + if (!currentPdfDoc && contentEl.querySelectorAll('#epub-chapter-0').length > 1) { + console.warn('Book rendered twice, progress not saved'); + return; + } + let anchor = ''; if (!currentPdfDoc) { anchor = getPositionAnchor(contentEl); @@ -4854,9 +4891,11 @@ async function saveReaderProgress(force = false) { } fraction = Math.min(1.0, Math.max(0.0, fraction)); - _updateCachedBookProgress(currentBookId, fraction, anchor, force); + _updateCachedBookProgress(currentBookId, fraction, anchor, force || (_repairProgressAnchor && !!anchor)); - const body = JSON.stringify({scroll_fraction: fraction, position_anchor: anchor, ...(force && {force: true})}); + const repair = _repairProgressAnchor && !!anchor; + if (repair) _repairProgressAnchor = false; + const body = JSON.stringify({scroll_fraction: fraction, position_anchor: anchor, ...((force || repair) && {force: true})}); _lastProgressBeacon = {url: `/books/${currentBookId}/progress/`, body}; try { diff --git a/static/js/sw.js b/static/js/sw.js index 2b72b57..c36b133 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v47'; +const CACHE = 'diora-v48'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From 77d662a9cbdb326af24cc445d6c3eff20bcccf39 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Tue, 29 Sep 2026 19:33:18 +0200 Subject: [PATCH 021/176] API-Token: Anfragen mit Bearer-Token brauchen kein CSRF-Token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native Clients (diora desktop) haben kein CSRF-Cookie; ein Browser hängt den Authorization-Header nie von selbst an, daher ist das sicher. Nötig z. B. für den M3U-Import, der nicht csrf_exempt ist. Co-Authored-By: Claude Opus 5.5 --- accounts/middleware.py | 5 +++++ accounts/tests.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/accounts/middleware.py b/accounts/middleware.py index 3ceb447..addb990 100644 --- a/accounts/middleware.py +++ b/accounts/middleware.py @@ -22,4 +22,9 @@ class ApiTokenAuthMiddleware: api_token = ApiToken.objects.select_related('user').filter(token=token).first() if api_token is not None: request.user = api_token.user + # Browsers never attach this header on their own, so a + # token request can't be a cross-site forgery; native + # clients have no CSRF cookie to send. (CsrfViewMiddleware + # checks this flag in process_view, which runs after us.) + request._dont_enforce_csrf_checks = True return self.get_response(request) diff --git a/accounts/tests.py b/accounts/tests.py index 4d4e05a..638ec51 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -42,6 +42,20 @@ class ApiTokenAuthMiddlewareTests(TestCase): resp = self.client.get('/books/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') self.assertEqual(resp.status_code, 200) + def test_bearer_token_needs_no_csrf_token(self): + # import_m3u is not csrf_exempt: without a token it must still be protected. + from django.test import Client + client = Client(enforce_csrf_checks=True) + m3u = '#EXTM3U\n#EXTINF:-1,Test FM\nhttps://example.com/stream\n' + resp = client.post('/radio/import/', data=m3u, content_type='text/plain') + self.assertIn(resp.status_code, (302, 401, 403)) + resp = client.post('/radio/import/', data=m3u, content_type='text/plain', + HTTP_AUTHORIZATION=f'Bearer {self.token.token}') + self.assertNotEqual(resp.status_code, 403) + resp = client.post('/radio/import/', data=m3u, content_type='text/plain', + HTTP_AUTHORIZATION='Bearer not-a-real-token') + self.assertIn(resp.status_code, (302, 401, 403)) + def test_regenerate_view_requires_login_and_rotates_token(self): resp = self.client.post('/accounts/api-token/regenerate/') self.assertEqual(resp.status_code, 302) From bca9d6b5e90d9ad8292a6eb694b5ed31d919099d Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Tue, 29 Sep 2026 20:00:42 +0200 Subject: [PATCH 022/176] =?UTF-8?q?Radio:=20http-Streams=20in=20nativen=20?= =?UTF-8?q?H=C3=BCllen=20abspielen=20lassen;=20Play-Session=20nicht=20mehr?= =?UTF-8?q?=20verlieren=20(SW=20v49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit playStation() öffnet http-Sender auf einer https-Seite in einem neuen Fenster. Native Hüllen (diora Android, diora desktop), die den Stream selbst abspielen, setzen window.DIORA_PLAYS_HTTP_STREAMS und überspringen das. stopPlaySession() wurde von playStation() ohne await aufgerufen und setzte currentPlayId erst nach dem fetch auf null – beim Senderwechsel also die frisch gesetzte ID der neuen Session. Die ID wird jetzt vor dem await übernommen. Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 13 +++++++++---- static/js/sw.js | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 21376a4..804775c 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -157,7 +157,9 @@ function customPrompt(message, defaultValue = '') { function playStation(url, name, stationId) { stopPlayback(false); - if (location.protocol === 'https:' && url.startsWith('http://')) { + // A secure page can't load plain-http streams (mixed content). Native shells + // that play the stream themselves set window.DIORA_PLAYS_HTTP_STREAMS. + if (location.protocol === 'https:' && url.startsWith('http://') && !window.DIORA_PLAYS_HTTP_STREAMS) { window.open(url, '_blank'); return; } @@ -280,15 +282,18 @@ async function startPlaySession(stationName, stationUrl) { } async function stopPlaySession() { - if (!currentPlayId) return; + // Take the id before awaiting: playStation() calls this without waiting and + // starts the next session right away, whose id must not be cleared here. + const playId = currentPlayId; + if (!playId) return; + currentPlayId = null; try { await fetch('/radio/play/stop/', { method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()}, - body: JSON.stringify({play_id: currentPlayId}) + body: JSON.stringify({play_id: playId}) }); } catch (e) {} - currentPlayId = null; } window.addEventListener('beforeunload', () => { diff --git a/static/js/sw.js b/static/js/sw.js index c36b133..e20bf19 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v48'; +const CACHE = 'diora-v49'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', From 3717640a4e4d9f21701b27e7ae5724e45ab1e579 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Tue, 29 Sep 2026 20:33:34 +0200 Subject: [PATCH 023/176] =?UTF-8?q?Radio:=20Stream-Adresse=20ins=20Suchfel?= =?UTF-8?q?d=20einf=C3=BCgen;=20Favoriten-Stern=20reparieren=20(SW=20v50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Eine http(s)-Adresse im Suchfeld wird nicht gesucht, sondern als Treffer mit Play und Save (Name wählbar) angeboten. Der Query-String (oft ein Zugangstoken) wird nicht angezeigt, aber mitgespeichert. - toggleFav gab es zweimal; die zweite Fassung verdeckte die erste und brauchte den Button als Argument, den die serverseitig gerenderten Zeilen nicht übergeben. Folge: Favorit auf dem Server gesetzt, Stern blieb leer, der nächste Klick nahm ihn wieder weg. Jetzt eine Fassung für beide Fälle, inklusive Umsortieren (Favoriten zuerst, wie der Server). Co-Authored-By: Claude Opus 5.5 --- static/js/app.js | 101 ++++++++++++++++++++++++++---------- static/js/sw.js | 2 +- templates/radio/player.html | 2 +- 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index 804775c..3982b5d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -541,10 +541,17 @@ async function doSearch() { const tableEl = $('search-results-table'); const tbody = $('search-results-body'); - statusEl.textContent = 'Searching…'; tableEl.style.display = 'none'; tbody.innerHTML = ''; + // A stream address instead of a name: offer to play or save it directly. + if (/^https?:\/\/\S+$/i.test(query)) { + showStreamAddressResult(query); + return; + } + + statusEl.textContent = 'Searching…'; + try { const url = `https://de1.api.radio-browser.info/json/stations/search?name=${encodeURIComponent(query)}&limit=50&hidebroken=true&order=clickcount&reverse=true`; const res = await fetch(url); @@ -595,6 +602,65 @@ async function doSearch() { } } +// A name to start from for a pasted address: "klassik.mp3" on radio.example.org +// -> "Klassik (radio.example.org)". The query string is left out on purpose — +// it often carries an access token, which shouldn't end up on screen. +function _streamAddressName(url) { + try { + const u = new URL(url); + const file = decodeURIComponent(u.pathname.split('/').filter(Boolean).pop() || ''); + const base = file.replace(/\.(mp3|aac|aacp|ogg|opus|m3u8?|pls|flac)$/i, '').replace(/[-_]+/g, ' ').trim(); + const nice = base ? base.charAt(0).toUpperCase() + base.slice(1) : ''; + return nice ? `${nice} (${u.host})` : u.host; + } catch (e) { + return url; + } +} + +function showStreamAddressResult(url) { + const statusEl = $('search-status'); + const tableEl = $('search-results-table'); + const tbody = $('search-results-body'); + const name = _streamAddressName(url); + const shown = url.split('?')[0]; + const data = {name, url, bitrate: '', country: '', tags: '', favicon_url: ''}; + + statusEl.textContent = 'Stream address'; + const curated = document.getElementById('curated-lists'); + if (curated) curated.style.display = 'none'; + + // Same five columns as a radio-browser result: name, bitrate, country, tags, actions. + const tr = document.createElement('tr'); + tr.className = 'stream-address-row'; + for (let i = 0; i < 4; i++) tr.appendChild(document.createElement('td')); + tr.children[0].textContent = name; + tr.children[0].title = shown; + tr.children[3].textContent = shown; + tr.children[3].title = shown; + + const actions = document.createElement('td'); + const play = document.createElement('button'); + play.className = 'btn btn-sm btn-play'; + play.innerHTML = '▶ Play'; + play.addEventListener('click', () => searchPlay(url, name, data)); + const save = document.createElement('button'); + save.className = 'btn btn-sm'; + save.innerHTML = '★ Save'; + save.style.marginLeft = '4px'; + save.addEventListener('click', async e => { + e.stopPropagation(); + const chosen = await customPrompt('Name for this station', name); + if (chosen == null) return; + const finalName = chosen.trim() || name; + await saveStation({...data, name: finalName}); + statusEl.textContent = `Saved as "${finalName}".`; + }); + actions.append(play, save); + tr.appendChild(actions); + tbody.appendChild(tr); + tableEl.style.display = ''; +} + function searchPlay(url, name, stationData) { // Store station data on the window so saveCurrentStation() can use it window._pendingStationData = stationData; @@ -729,7 +795,11 @@ function addSavedRow(station) { // Remove station // --------------------------------------------------------------------------- -async function toggleFav(pk) { +// The only toggleFav: a second definition further down (expecting the button +// as an argument) used to shadow this one, so the server-rendered rows — which +// pass no button — toggled the favourite on the server without the star ever +// changing, and a second click silently took it back. +async function toggleFav(pk, btnEl) { try { const res = await fetch(`/radio/favorite/${pk}/`, { method: 'POST', @@ -739,8 +809,8 @@ async function toggleFav(pk) { const data = await res.json(); // Flip the star button state - const btn = document.querySelector(`#saved-row-${pk} .fav-btn`); - if (btn) btn.classList.toggle('active', data.is_favorite); + const btn = btnEl || document.querySelector(`#saved-row-${pk} .fav-btn`); + if (btn) btn.classList.toggle('active', !!data.is_favorite); // Re-sort rows in the DOM: favorites first, then alphabetically const tbody = $('saved-tbody'); @@ -781,29 +851,6 @@ async function removeStation(pk) { } } -// --------------------------------------------------------------------------- -// Toggle favorite -// --------------------------------------------------------------------------- - -async function toggleFav(pk, btnEl) { - try { - const res = await fetch(`/radio/favorite/${pk}/`, { - method: 'POST', - headers: { 'X-CSRFToken': getCsrfToken() }, - }); - if (res.ok) { - const data = await res.json(); - if (data.is_favorite) { - btnEl.classList.add('active'); - } else { - btnEl.classList.remove('active'); - } - } - } catch (err) { - console.error('toggleFav error:', err); - } -} - // --------------------------------------------------------------------------- // Focus Timer // --------------------------------------------------------------------------- diff --git a/static/js/sw.js b/static/js/sw.js index e20bf19..0068104 100644 --- a/static/js/sw.js +++ b/static/js/sw.js @@ -2,7 +2,7 @@ * diora service worker — caches the app shell for offline use. */ -const CACHE = 'diora-v49'; +const CACHE = 'diora-v50'; const PODCAST_CACHE = 'diora-podcast-v1'; const SHELL = [ '/static/css/app.css', diff --git a/templates/radio/player.html b/templates/radio/player.html index 9ac3540..b8dbc5c 100644 --- a/templates/radio/player.html +++ b/templates/radio/player.html @@ -81,7 +81,7 @@