diora-web/tui/README.md
marwin db3f520632 TUI: durchgehende Leseansicht mit web-kompatiblen Anchors, Fußnoten, bidirektionalem Progress-Sync
Reader zeigt Bücher jetzt als eine fortlaufende Ansicht über alle Kapitel
statt Kapitel für Kapitel (diora_tui/blocks.py, layout.py, book_view.py,
reader_screen.py). blockIndex wird exakt wie app.js' EPUB_BLOCK_SELECTOR
gezählt (p/h1-6/li/blockquote/dt/dd/figcaption + kindlose divs), inklusive
linear="no"-Spine-Einträgen — app.js filtert die nicht, und Skippen hätte
sowohl Fußnoten-Ziele verfehlt als auch alle folgenden Blockindizes gegen
den Web-Reader verschoben. innerFraction ist eine zeilenbasierte Näherung
(Terminal hat keine Pixel-Geometrie), was funktioniert, weil der
Furthest-Wins-Vergleich primär nach blockIndex sortiert.

Rendering nutzt Textuals Line-API (ContinuousBookView.render_line) statt
eines einzelnen riesigen Static — bei großen Büchern (mehrere reale
heruntergeladene Bücher haben zehntausende Blocks) hätte ein Static den
Layout/Paint-Pass auf über eine Minute gebracht. Zusätzlich cached
diora_tui/cache.py das (Buch, Layout)-Paar pro (Datei, Breite) auf Platte
für schnelles Wiederöffnen. Text ist auf 120 Zeichen begrenzt und
zentriert.

Fußnoten (f-Taste, FootnoteScreen): Erkennung wie app.js'
_looksLikeFootnoteLink; Ziel-Auflösung sammelt IDs aus dem ganzen
Block-Teilbaum (nicht nur vom Block-Tag selbst), weil Fußnoten-Ziele
häufig auf einem inneren <a> statt dem umschließenden <p> sitzen.

Progress-Sync ist jetzt bidirektional, ohne Übersetzungsschicht nötig, da
beide Seiten dasselbe Anchor-Format nutzen: sync zieht book_progress aus
dem Snapshot in den lokalen Store (furthest-wins); der Reader schickt bei
offenen server-verknüpften Büchern Updates zurück (force: false, im
Hintergrund-Worker).

Verschlüsselungs-Key-Beschaffung ergänzt um den Fallback
localStorage.getItem(...) falls die Clipboard-API in der Konsole
verweigert wird.

Getestet: Blockindex-/Fußnoten-Korrektheit gegen reale Bücher (u.a.
3686/3686 aufgelöste Fußnoten bei einem Zizek-Band), Anchor-Mathematik
per Unit-Test, vollständiger Pilot-Test (Navigation, Scroll,
Kapitelsprung, Fußnoten-Peek, Resize), Performance-Messung über mehrere
Buchgrößen inkl. Cache-Effekt (größtes Buch: ~34k Blocks, kalt ~20-30s,
warm ~5s), Save/Restore-Round-Trip 5x wiederholt gegen eine
Race-Condition beim ersten Post-Load-Scroll, und Progress-Push
End-to-End gegen einen echten Dev-Server verifiziert.
2026-08-15 17:59:46 +02:00

110 lines
5.9 KiB
Markdown

# diora-tui
EPUB-Reader im Terminal — die erste Stufe einer TUI-Version von diora.
Zeigt ein Buch als **eine durchgehende Ansicht** über alle Kapitel hinweg (wie diora's
Web-Reader), nicht Kapitel für Kapitel. Lesefortschritt wird als dieselbe
`"blockIndex:innerFraction"`-Positionsangabe geführt wie der Web-Reader (`books/models.py`,
`EBookProgress`) — `blockIndex` zählt dabei exakt wie `static/js/app.js`s
`EPUB_BLOCK_SELECTOR` (`p, h1-h6, li, blockquote, dt, dd, figcaption` + textbasierte
`div`s ohne Element-Kinder), fortlaufend über das ganze Buch. Dadurch ist die Position
zwischen TUI und Web-Reader direkt vergleichbar, und `diora-tui sync` kann Fortschritt in
beide Richtungen synchronisieren (siehe unten).
Fortschritt wird — genau wie im Web-Reader (`save_progress`) — nur vorwärts überschrieben
("furthest wins"), lokal in `progress.json` (`~/.local/share/diora-tui/`, via
`platformdirs`), damit ein älterer/gestaffelter Lauf nie eine bereits weiter gelesene
Position zurücksetzt.
## Setup
```bash
cd tui
pip install -e .
```
## Nutzung
```bash
diora-tui --library ~/Books # Standard: ~/Books
```
Tastenkürzel:
- `↑`/`k`, `↓`/`j` — zeilenweise scrollen
- `n` — nächstes Kapitel, `p` — vorheriges Kapitel
- `f` — Fußnote in der Nähe der aktuellen Position anzeigen (Peek-Overlay, `Escape`/`f`/`q`
zum Schließen); Erkennung folgt derselben Heuristik wie `app.js`s
`_looksLikeFootnoteLink` (Link in/um `<sup>`, Klassenname mit note/footnote/fn, oder
`epub:type="noteref"`)
- `Enter` — markiertes Buch aus der Bibliothek öffnen
- `Escape` / `q` — zurück zur Bibliothek (im Reader) bzw. beenden (in der Bibliothek)
Der Fließtext ist auf 120 Zeichen Breite begrenzt und horizontal zentriert (lesbarer als
volle Terminalbreite bei breiten Fenstern).
## Bücher + Fortschritt vom Server holen (`diora-tui sync`)
```bash
diora-tui sync # nutzt gespeicherte Zugangsdaten, sonst interaktive Abfrage
diora-tui sync --server https://diora.creamfresh.xyz --save # einmalig einrichten + speichern
```
Lädt alle EPUBs des Accounts über `GET /api/sync/` + `GET /books/<id>/data/` herunter,
entschlüsselt sie lokal (AES-256-GCM, kompatibel zu `static/js/app.js`) und legt sie als
normale `.epub`-Dateien in `--library` ab (Dateiname `<id> - <Titel>.epub`) — von da an
funktionieren sie wie jedes andere lokale Buch. Bereits heruntergeladene Bücher werden
beim nächsten Lauf übersprungen (kein erneuter Download). Der Fortschritt aus dem Snapshot
wird dabei ebenfalls übernommen (nur vorwärts, wie lokal auch).
Während ein so heruntergeladenes Buch geöffnet ist (erkennbar am `<id> - `-Dateinamens-
Präfix), schickt der Reader Fortschritts-Updates zusätzlich zurück an den Server
(`POST /books/<id>/progress/`, `force: false` — überschreibt also nie eine weiter
gelesene Position, egal ob die vom Web-Reader oder einem anderen Gerät stammt). Rein
lokale Bücher (ohne dieses Präfix) bleiben unangetastet, kein Netzwerkzugriff.
Dafür nötig, beim ersten Lauf abgefragt (danach optional lokal gespeichert unter
`~/.config/diora-tui/config.json`, `chmod 600`):
- **Server-URL** — z.B. `https://diora.creamfresh.xyz`.
- **API-Token** — diora → Einstellungen (`/accounts/settings/`) → "Personal Access Token".
- **Verschlüsselungs-Key** — der AES-256-Schlüssel, mit dem deine Bücher clientseitig
verschlüsselt wurden. Der `sync`-Prompt bietet zwei Wege:
1. **Aus Benutzername + Passwort ableiten** (Standard) — reproduziert exakt, was diora's
"Unlock with password"-Formular im Browser tut (PBKDF2-HMAC-SHA256, 200.000
Iterationen, Salt `"diora:" + username`; siehe `static/js/app.js:deriveAndStoreKey`).
Funktioniert nur, wenn der Account diesen Weg im Browser mindestens einmal benutzt
hat — sonst wurde der Key ursprünglich zufällig im Browser erzeugt, und diese
Ableitung trifft ihn nicht. `sync` meldet einen falschen Key als
Entschlüsselungsfehler (harmlos, kein Datenverlust), nie als falsches Ergebnis.
2. **Base64-Key direkt einfügen** — für den Fall, dass Weg 1 nicht passt. Der
Export-Button dafür ist im Browser-UI aktuell nicht verdrahtet (`exportEncKey()` in
`app.js` existiert, hat aber keinen sichtbaren Button); bis das nachgezogen ist, in
der Browser-Devtools-Konsole auf der diora-Seite (nicht `/accounts/settings/` — die
lädt `app.js` nicht) ausführen: `await exportEncKey()` — kopiert den Key ins
Clipboard, von dort ins `sync`-Prompt einfügen. Schlägt das mit einem
`NotAllowedError`/`InvalidAccessError` fehl, stattdessen direkt aus `localStorage`
lesen: `localStorage.getItem('diora_enc_key_' + window.USER_ID)`.
Der Key/Token wird genauso vertrauensvoll behandelt wie im Web-Client (dort liegt der
Schlüssel unverschlüsselt in `localStorage`): lokal als Klartext in einer 0600-Datei.
## Performance bei großen Büchern
Sehr große Bücher (mehrstellige Tausend Absätze — z.B. Sammelbände) können beim
*ersten* Öffnen mehrere Sekunden bis niedrige zweistellige Sekunden brauchen (Parsing +
Zeilenumbruch-Berechnung für die durchgehende Ansicht). Ein zweites Öffnen desselben
Buchs bei gleicher Terminalbreite ist dank Cache (`~/.cache/diora-tui/layout_cache/`)
deutlich schneller. Das Rendering selbst skaliert nicht mit der Buchgröße — nur die
tatsächlich sichtbaren Zeilen werden gezeichnet (Textual Line API), nicht das ganze Buch
auf einmal.
## Grenzen der aktuellen Version
- Nur EPUB, kein PDF — `sync` lädt PDFs im Account gar nicht erst herunter (übersprungen,
wird gemeldet), da der Reader sie ohnehin nicht darstellen kann.
- Text wird als Fließtext ohne Bild-/Layout-Rendering dargestellt.
- `innerFraction` (die Position *innerhalb* eines Blocks) ist eine Terminal-Näherung
(zeilenbasiert statt pixelbasiert wie im Browser) — für die Fortschritts-Sortierung
zählt primär `blockIndex`, der exakt mit dem Web-Reader übereinstimmt; `innerFraction`
ist nur ein Tiebreaker innerhalb desselben Blocks.