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
|
|
|
"""Continuous whole-book reader screen — mirrors the web reader's single
|
|
|
|
|
scrollable view (see blocks.py's module docstring) instead of the old
|
|
|
|
|
per-chapter pagination, so reading position is expressed in the same
|
|
|
|
|
"blockIndex:innerFraction" anchor space as the server and the web client.
|
|
|
|
|
|
|
|
|
|
Uses book_view.ContinuousBookView (a Line-API widget) rather than dumping the
|
|
|
|
|
whole book into one Static: for large books (tens of thousands of blocks) a
|
|
|
|
|
single giant renderable made Textual's layout/paint pass take upwards of a
|
|
|
|
|
minute, whereas the Line API only ever renders the rows on screen.
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
|
|
|
|
|
Reading position (docs/progress-sync.md, rules in progress.py): the place last read wins.
|
|
|
|
|
The reader saves a position only when the *reader* moved it (keys, wheel), stamped with the
|
|
|
|
|
moment of arriving; restoring, re-wrapping on resize and jumping on an offer change nothing.
|
|
|
|
|
While a book is open the server is asked now and then; a newer position from another device
|
|
|
|
|
is offered ("Auf X weitergelesen — springen?"), never applied by itself.
|
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
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import bisect
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
import re
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
import threading
|
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
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from rich.console import Console
|
|
|
|
|
from textual import work
|
|
|
|
|
from textual.binding import Binding
|
|
|
|
|
from textual.containers import Container, VerticalScroll
|
|
|
|
|
from textual.geometry import Size
|
|
|
|
|
from textual.screen import ModalScreen, Screen
|
|
|
|
|
from textual.widgets import Footer, Header, Label, LoadingIndicator, Static
|
|
|
|
|
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
from . import annotations, blocks, cache, config as config_mod, epub, layout, library, library_meta, marks, progress, remote
|
|
|
|
|
from .annotation_screens import AnnotationsScreen, NoteScreen
|
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
|
|
|
from .book_view import ContinuousBookView
|
2026-08-15 20:36:54 +02:00
|
|
|
from .statusbar import StatusBar
|
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
|
|
|
|
|
|
|
|
MAX_LINE_WIDTH = 120
|
|
|
|
|
AUTOSAVE_INTERVAL = 5.0
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
# How often the server is asked for a newer position while the book is open (the apps pull
|
|
|
|
|
# at most every 2 minutes).
|
|
|
|
|
REMOTE_CHECK_INTERVAL = 120.0
|
|
|
|
|
# "Weiteste Stelle" is offered when the furthest place is this much of the book ahead.
|
|
|
|
|
FURTHEST_OFFER_SHARE = 0.01
|
TUI: Zeichen am rechten Rand bei schmalen Terminals nicht mehr abgeschnitten
ContinuousBookViews vertikale Scrollbar reserviert 2 Spalten, die vorher
nicht aus der Wrap-Breite herausgerechnet wurden — der Text wurde also
2 Zeichen breiter gewrappt, als tatsächlich sichtbar war, wodurch die
Scrollbar die letzten 1-2 Buchstaben jeder Zeile überdeckt hat. Bei
schmalen Terminals (großer Font, wenig Spalten) war das besonders
auffällig, betraf strukturell aber jede Breite.
Fix: overflow-x: hidden (eine ungewollte horizontale Scrollbar hat
zusätzlich eine Zeile unten geklaut) + overflow-y: scroll (hält die
Scrollbar-Breite von Anfang an konstant, kein Rätselraten je nach
Inhaltsgröße) in book_view.py. reader_screen.py misst die Wrap-Breite
jetzt am Container statt an book_view selbst (vermeidet einen
Miss-nach-Einschränken-Zirkelbezug bei wiederholten Resizes) und rechnet
die feste Scrollbar-Breite (SCROLLBAR_GUTTER=2) heraus; beim Anwenden
des Layouts wird sie wieder daraufgerechnet, damit book_view.styles.width
weiterhin fürs Zentrieren passt.
Getestet: content_region-Breite stimmt jetzt exakt mit der Wrap-Breite
über sechs verschiedene Terminalbreiten (60-200 Spalten, inkl. des
120-Zeichen-Cap-Bereichs) überein — vorher lag sie durchgehend 2 Spalten
darunter. Voller Regressionstest (Scroll, Fußnote, Resize) läuft weiter
fehlerfrei.
2026-08-15 21:07:41 +02:00
|
|
|
# ContinuousBookView forces its vertical scrollbar always-on (see book_view.py)
|
|
|
|
|
# so this stays constant — measuring the container's width and subtracting
|
|
|
|
|
# this fixed amount avoids a measure-after-constrain race against book_view's
|
|
|
|
|
# own (possibly already-constrained-from-a-previous-layout) width.
|
|
|
|
|
SCROLLBAR_GUTTER = 2
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
# A selection may span this many paragraphs (each is drawn again at every cursor move).
|
|
|
|
|
MAX_MARK_BLOCKS = 200
|
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
|
|
|
|
|
|
|
|
_EMPTY_LAYOUT = layout.BookLayout(starts=[], heights=[], row_strips=[], total_rows=0, width=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FootnoteScreen(ModalScreen[None]):
|
|
|
|
|
BINDINGS = [Binding("escape,f,q", "dismiss_self", "Schließen")]
|
|
|
|
|
|
|
|
|
|
def __init__(self, notes: list[str]) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self._notes = notes
|
|
|
|
|
|
|
|
|
|
def compose(self):
|
|
|
|
|
with VerticalScroll(id="footnote-body"):
|
|
|
|
|
for note in self._notes:
|
|
|
|
|
yield Static(note, classes="footnote-note")
|
|
|
|
|
yield Label("Escape/f zum Schließen", id="footnote-hint")
|
|
|
|
|
|
|
|
|
|
def action_dismiss_self(self) -> None:
|
|
|
|
|
self.dismiss(None)
|
|
|
|
|
|
|
|
|
|
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
class JumpOfferScreen(ModalScreen[bool]):
|
|
|
|
|
"""A question the reader answers with a key; nothing moves unless the answer is yes."""
|
|
|
|
|
|
|
|
|
|
BINDINGS = [
|
|
|
|
|
Binding("y,enter", "answer(True)", "Springen"),
|
|
|
|
|
Binding("n,escape,q", "answer(False)", "Hier bleiben"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __init__(self, text: str) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self._text = text
|
|
|
|
|
|
|
|
|
|
def compose(self):
|
|
|
|
|
with Container(id="offer-body"):
|
|
|
|
|
yield Static(self._text)
|
|
|
|
|
yield Label("y/Enter: springen n/Escape: hier bleiben", id="offer-hint")
|
|
|
|
|
|
|
|
|
|
def action_answer(self, yes: bool) -> None:
|
|
|
|
|
self.dismiss(yes)
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
class ReaderScreen(Screen):
|
|
|
|
|
BINDINGS = [
|
|
|
|
|
Binding("j,down", "scroll_down_line", "Runter", show=False),
|
|
|
|
|
Binding("k,up", "scroll_up_line", "Hoch", show=False),
|
|
|
|
|
Binding("n", "next_chapter", "Nächstes Kapitel"),
|
|
|
|
|
Binding("p", "prev_chapter", "Vorheriges Kapitel"),
|
|
|
|
|
Binding("f", "peek_footnote", "Fußnote"),
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
Binding("w", "goto_furthest", "Weiteste Stelle"),
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
Binding("v", "select", "Markieren"),
|
|
|
|
|
Binding("b", "bookmark", "Lesezeichen"),
|
|
|
|
|
Binding("a", "annotations", "Markierungen"),
|
|
|
|
|
Binding("h,left", "word_back", "Wort zurück", show=False),
|
|
|
|
|
Binding("l,right", "word_forward", "Wort vor", show=False),
|
|
|
|
|
Binding("enter,space", "confirm", "Weiter", show=False),
|
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
|
|
|
Binding("q,escape", "back", "Zurück zur Bibliothek"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __init__(self, path: Path) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self.path = path
|
|
|
|
|
self.book: blocks.FlatBook | None = None
|
|
|
|
|
self.book_layout: layout.BookLayout = _EMPTY_LAYOUT
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
self._key = epub._book_id(path)
|
|
|
|
|
self._server_id = epub.server_id_of(path)
|
|
|
|
|
self._cfg = config_mod.load() if self._server_id is not None else None
|
|
|
|
|
self._store = progress.default()
|
|
|
|
|
# The position the view is on, as last saved or restored, and when the reader last
|
|
|
|
|
# moved it (this machine's clock). A change of the anchor is a reading position only
|
|
|
|
|
# when there is such a move.
|
|
|
|
|
self._last_anchor: str | None = None
|
|
|
|
|
self._last_move_ms: int | None = None
|
|
|
|
|
# read_at (server clock) of the state this view is based on: a newer one from the
|
|
|
|
|
# server is what gets offered.
|
|
|
|
|
self._held_read_at: int | None = None
|
|
|
|
|
self._offer_open = False
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
self._marks: marks.MarkIndex | None = None
|
|
|
|
|
self._ann = annotations.default()
|
|
|
|
|
self._styled: set[int] = set() # blocks drawn with marks at the moment
|
|
|
|
|
self._sel: dict | None = None # the selection being made: phase, cursor, start
|
|
|
|
|
self._ann_timer = None
|
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
|
|
|
|
|
|
|
|
def compose(self):
|
|
|
|
|
yield Header()
|
|
|
|
|
yield LoadingIndicator(id="reader-loading")
|
TUI: Zeichen am rechten Rand bei schmalen Terminals nicht mehr abgeschnitten
ContinuousBookViews vertikale Scrollbar reserviert 2 Spalten, die vorher
nicht aus der Wrap-Breite herausgerechnet wurden — der Text wurde also
2 Zeichen breiter gewrappt, als tatsächlich sichtbar war, wodurch die
Scrollbar die letzten 1-2 Buchstaben jeder Zeile überdeckt hat. Bei
schmalen Terminals (großer Font, wenig Spalten) war das besonders
auffällig, betraf strukturell aber jede Breite.
Fix: overflow-x: hidden (eine ungewollte horizontale Scrollbar hat
zusätzlich eine Zeile unten geklaut) + overflow-y: scroll (hält die
Scrollbar-Breite von Anfang an konstant, kein Rätselraten je nach
Inhaltsgröße) in book_view.py. reader_screen.py misst die Wrap-Breite
jetzt am Container statt an book_view selbst (vermeidet einen
Miss-nach-Einschränken-Zirkelbezug bei wiederholten Resizes) und rechnet
die feste Scrollbar-Breite (SCROLLBAR_GUTTER=2) heraus; beim Anwenden
des Layouts wird sie wieder daraufgerechnet, damit book_view.styles.width
weiterhin fürs Zentrieren passt.
Getestet: content_region-Breite stimmt jetzt exakt mit der Wrap-Breite
über sechs verschiedene Terminalbreiten (60-200 Spalten, inkl. des
120-Zeichen-Cap-Bereichs) überein — vorher lag sie durchgehend 2 Spalten
darunter. Voller Regressionstest (Scroll, Fußnote, Resize) läuft weiter
fehlerfrei.
2026-08-15 21:07:41 +02:00
|
|
|
with Container(id="reader-scroll") as scroll_container:
|
|
|
|
|
self._scroll_container = scroll_container
|
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
|
|
|
# Stays visible (empty) from the start rather than toggling display
|
|
|
|
|
# on once loaded — a widget that's just been switched from hidden
|
|
|
|
|
# to visible hasn't been through a layout pass yet, so its `.size`
|
|
|
|
|
# is still (0, 0) and an immediate scroll_to() right after has
|
|
|
|
|
# nothing to clamp against and silently resets to 0.
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
self._book_view = ContinuousBookView(_EMPTY_LAYOUT, on_read_scroll=self._on_read_scroll)
|
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
|
|
|
yield self._book_view
|
2026-08-15 20:36:54 +02:00
|
|
|
yield StatusBar()
|
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
|
|
|
yield Footer()
|
|
|
|
|
|
|
|
|
|
def on_mount(self) -> None:
|
TUI: Zeichen am rechten Rand bei schmalen Terminals nicht mehr abgeschnitten
ContinuousBookViews vertikale Scrollbar reserviert 2 Spalten, die vorher
nicht aus der Wrap-Breite herausgerechnet wurden — der Text wurde also
2 Zeichen breiter gewrappt, als tatsächlich sichtbar war, wodurch die
Scrollbar die letzten 1-2 Buchstaben jeder Zeile überdeckt hat. Bei
schmalen Terminals (großer Font, wenig Spalten) war das besonders
auffällig, betraf strukturell aber jede Breite.
Fix: overflow-x: hidden (eine ungewollte horizontale Scrollbar hat
zusätzlich eine Zeile unten geklaut) + overflow-y: scroll (hält die
Scrollbar-Breite von Anfang an konstant, kein Rätselraten je nach
Inhaltsgröße) in book_view.py. reader_screen.py misst die Wrap-Breite
jetzt am Container statt an book_view selbst (vermeidet einen
Miss-nach-Einschränken-Zirkelbezug bei wiederholten Resizes) und rechnet
die feste Scrollbar-Breite (SCROLLBAR_GUTTER=2) heraus; beim Anwenden
des Layouts wird sie wieder daraufgerechnet, damit book_view.styles.width
weiterhin fürs Zentrieren passt.
Getestet: content_region-Breite stimmt jetzt exakt mit der Wrap-Breite
über sechs verschiedene Terminalbreiten (60-200 Spalten, inkl. des
120-Zeichen-Cap-Bereichs) überein — vorher lag sie durchgehend 2 Spalten
darunter. Voller Regressionstest (Scroll, Fußnote, Resize) läuft weiter
fehlerfrei.
2026-08-15 21:07:41 +02:00
|
|
|
# Deferred rather than read synchronously here: right after mount the
|
|
|
|
|
# container hasn't been through a layout pass yet, so its size isn't
|
|
|
|
|
# reliable — same lesson as the scroll-restore race below.
|
|
|
|
|
self.call_after_refresh(self._start_loading)
|
|
|
|
|
|
|
|
|
|
def _start_loading(self) -> None:
|
|
|
|
|
if self._scroll_container.size.width == 0:
|
|
|
|
|
# Not sized yet after all — keep deferring instead of guessing a
|
|
|
|
|
# fixed number of refresh cycles (mirrors _restore_position below).
|
|
|
|
|
self.call_after_refresh(self._start_loading)
|
|
|
|
|
return
|
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
|
|
|
self._load_book(self._content_width())
|
|
|
|
|
|
|
|
|
|
@work(thread=True)
|
|
|
|
|
def _load_book(self, width: int) -> None:
|
|
|
|
|
cached = cache.load(self.path, width)
|
|
|
|
|
if cached is not None:
|
|
|
|
|
book, book_layout = cached
|
|
|
|
|
else:
|
|
|
|
|
book = blocks.load_flat_book(self.path)
|
|
|
|
|
console = Console(width=width)
|
|
|
|
|
book_layout = layout.build_layout([b.text for b in book.blocks], width, console)
|
|
|
|
|
cache.save(self.path, width, book, book_layout)
|
|
|
|
|
self.app.call_from_thread(self._on_book_loaded, book, book_layout, width)
|
|
|
|
|
|
|
|
|
|
def _on_book_loaded(self, book: blocks.FlatBook, book_layout: layout.BookLayout, width: int) -> None:
|
|
|
|
|
self.book = book
|
|
|
|
|
self.sub_title = book.title
|
|
|
|
|
self._apply_layout(book_layout, width)
|
|
|
|
|
self.query_one("#reader-loading", LoadingIndicator).display = False
|
TUI: Bibliothek wie im Web (Weiterlesen-Karte, Status- und Ordner-Chips, Suche, Sortierung, Länge)
library.py hält die Regeln der Web-Bibliothek (53-library.js) als reine Funktionen:
Status Angefangen/Ungelesen/Gelesen, Ordner (mehrere je Buch, Reihenfolge aus den
verschlüsselten Bibliothekseinstellungen, "Ohne Ordner"), Suche ohne Akzente, sechs
Sortierungen mit natürlicher Zahlenordnung, Weiterlesen-Karte aus den zuletzt gelesenen,
Bücher/Zeitschriften. library_screen.py zeigt sie an (Tasten 1-4, o, /, s, m, c; Chips und
Karte klickbar). Metadaten (Ordner, Seitenzahl, Zeitschrift) und die Ordnerreihenfolge
kommen beim Abgleich; fehlt die Länge, rechnet die TUI sie für geladene Bücher aus dem Text
(1800 Zeichen je Seite). crypto.encrypt für Stufe 3. 54 Tests, Smoke prüft Ordner/Länge/Prefs
gegen den lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:18:49 +01:00
|
|
|
library_meta.set_local_pages(self._key, library.pages_of_chars(book.total_chars))
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
st = self._store.get(self._key)
|
|
|
|
|
self._held_read_at = st.read_at if st else None
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
self._marks = marks.MarkIndex(book)
|
|
|
|
|
self._apply_marks()
|
|
|
|
|
if self._cfg is not None:
|
|
|
|
|
self._pull_annotations()
|
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
|
|
|
self.call_after_refresh(self._restore_position)
|
|
|
|
|
self.set_interval(AUTOSAVE_INTERVAL, self._autosave)
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
if self._cfg is not None:
|
|
|
|
|
self.set_interval(REMOTE_CHECK_INTERVAL, self._check_remote)
|
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
|
|
|
|
|
|
|
|
def _content_width(self) -> int:
|
TUI: Zeichen am rechten Rand bei schmalen Terminals nicht mehr abgeschnitten
ContinuousBookViews vertikale Scrollbar reserviert 2 Spalten, die vorher
nicht aus der Wrap-Breite herausgerechnet wurden — der Text wurde also
2 Zeichen breiter gewrappt, als tatsächlich sichtbar war, wodurch die
Scrollbar die letzten 1-2 Buchstaben jeder Zeile überdeckt hat. Bei
schmalen Terminals (großer Font, wenig Spalten) war das besonders
auffällig, betraf strukturell aber jede Breite.
Fix: overflow-x: hidden (eine ungewollte horizontale Scrollbar hat
zusätzlich eine Zeile unten geklaut) + overflow-y: scroll (hält die
Scrollbar-Breite von Anfang an konstant, kein Rätselraten je nach
Inhaltsgröße) in book_view.py. reader_screen.py misst die Wrap-Breite
jetzt am Container statt an book_view selbst (vermeidet einen
Miss-nach-Einschränken-Zirkelbezug bei wiederholten Resizes) und rechnet
die feste Scrollbar-Breite (SCROLLBAR_GUTTER=2) heraus; beim Anwenden
des Layouts wird sie wieder daraufgerechnet, damit book_view.styles.width
weiterhin fürs Zentrieren passt.
Getestet: content_region-Breite stimmt jetzt exakt mit der Wrap-Breite
über sechs verschiedene Terminalbreiten (60-200 Spalten, inkl. des
120-Zeichen-Cap-Bereichs) überein — vorher lag sie durchgehend 2 Spalten
darunter. Voller Regressionstest (Scroll, Fußnote, Resize) läuft weiter
fehlerfrei.
2026-08-15 21:07:41 +02:00
|
|
|
# Measuring the *container* rather than book_view's own size avoids a
|
|
|
|
|
# measure-after-constrain race: once a layout has been applied,
|
|
|
|
|
# book_view's width is pinned to a previous value via styles.width
|
|
|
|
|
# (below), so re-measuring book_view itself on a later resize would
|
|
|
|
|
# just read that stale pinned width back instead of the new
|
|
|
|
|
# available space. The container's width is unaffected by that.
|
|
|
|
|
available = self._scroll_container.size.width - SCROLLBAR_GUTTER
|
|
|
|
|
return max(20, min(MAX_LINE_WIDTH, available))
|
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
|
|
|
|
|
|
|
|
def _apply_layout(self, book_layout: layout.BookLayout, width: int) -> None:
|
|
|
|
|
self.book_layout = book_layout
|
TUI: Zeichen am rechten Rand bei schmalen Terminals nicht mehr abgeschnitten
ContinuousBookViews vertikale Scrollbar reserviert 2 Spalten, die vorher
nicht aus der Wrap-Breite herausgerechnet wurden — der Text wurde also
2 Zeichen breiter gewrappt, als tatsächlich sichtbar war, wodurch die
Scrollbar die letzten 1-2 Buchstaben jeder Zeile überdeckt hat. Bei
schmalen Terminals (großer Font, wenig Spalten) war das besonders
auffällig, betraf strukturell aber jede Breite.
Fix: overflow-x: hidden (eine ungewollte horizontale Scrollbar hat
zusätzlich eine Zeile unten geklaut) + overflow-y: scroll (hält die
Scrollbar-Breite von Anfang an konstant, kein Rätselraten je nach
Inhaltsgröße) in book_view.py. reader_screen.py misst die Wrap-Breite
jetzt am Container statt an book_view selbst (vermeidet einen
Miss-nach-Einschränken-Zirkelbezug bei wiederholten Resizes) und rechnet
die feste Scrollbar-Breite (SCROLLBAR_GUTTER=2) heraus; beim Anwenden
des Layouts wird sie wieder daraufgerechnet, damit book_view.styles.width
weiterhin fürs Zentrieren passt.
Getestet: content_region-Breite stimmt jetzt exakt mit der Wrap-Breite
über sechs verschiedene Terminalbreiten (60-200 Spalten, inkl. des
120-Zeichen-Cap-Bereichs) überein — vorher lag sie durchgehend 2 Spalten
darunter. Voller Regressionstest (Scroll, Fußnote, Resize) läuft weiter
fehlerfrei.
2026-08-15 21:07:41 +02:00
|
|
|
# Pin book_view's outer width to content-width-plus-scrollbar so its
|
|
|
|
|
# *inner* content region (what render_line actually draws into) ends
|
|
|
|
|
# up exactly `width` — matching what block texts were wrapped at.
|
|
|
|
|
self._book_view.styles.width = width + SCROLLBAR_GUTTER
|
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
|
|
|
self._book_view.book_layout = book_layout
|
|
|
|
|
self._book_view.virtual_size = Size(width, book_layout.total_rows)
|
|
|
|
|
self._book_view.refresh()
|
|
|
|
|
|
|
|
|
|
def _build_layout(self) -> None:
|
|
|
|
|
assert self.book is not None
|
|
|
|
|
width = self._content_width()
|
|
|
|
|
console = Console(width=width)
|
|
|
|
|
block_texts = [b.text for b in self.book.blocks]
|
|
|
|
|
new_layout = layout.build_layout(block_texts, width, console)
|
|
|
|
|
cache.save(self.path, width, self.book, new_layout)
|
|
|
|
|
self._apply_layout(new_layout, width)
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
self._styled = set() # a fresh layout is drawn without marks
|
|
|
|
|
self._apply_marks()
|
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
|
|
|
|
|
|
|
|
def on_resize(self) -> None:
|
|
|
|
|
if self.book is None:
|
|
|
|
|
return
|
|
|
|
|
old_y = self._book_view.scroll_y
|
|
|
|
|
anchor_block, anchor_frac = layout.get_position_anchor(self.book_layout, old_y)
|
|
|
|
|
self._build_layout()
|
|
|
|
|
new_y = layout.scroll_y_for_anchor(self.book_layout, anchor_block, anchor_frac)
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
with self._book_view.program_scroll():
|
|
|
|
|
self._book_view.scroll_to(y=new_y, animate=False, immediate=True)
|
|
|
|
|
|
|
|
|
|
def _scroll_to_anchor(self, anchor: str) -> bool:
|
|
|
|
|
parsed = blocks.parse_anchor(anchor)
|
|
|
|
|
if parsed is None:
|
|
|
|
|
return False
|
|
|
|
|
y = layout.scroll_y_for_anchor(self.book_layout, *parsed)
|
|
|
|
|
with self._book_view.program_scroll():
|
|
|
|
|
self._book_view.scroll_to(y=y, animate=False, immediate=True)
|
|
|
|
|
return y > 0
|
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
|
|
|
|
|
|
|
|
def _restore_position(self, attempt: int = 0) -> None:
|
|
|
|
|
if self.book is None:
|
|
|
|
|
return
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
st = self._store.get(self._key)
|
|
|
|
|
if st is None or not st.anchor or blocks.parse_anchor(st.anchor) is None:
|
|
|
|
|
self._settle()
|
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
|
|
|
return
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
parsed = blocks.parse_anchor(st.anchor)
|
|
|
|
|
y = layout.scroll_y_for_anchor(self.book_layout, *parsed)
|
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
|
|
|
if y <= 0:
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
self._settle()
|
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
|
|
|
return
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
with self._book_view.program_scroll():
|
|
|
|
|
self._book_view.scroll_to(y=y, animate=False, immediate=True)
|
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
|
|
|
# A widget that's only just become part of the layout doesn't always
|
|
|
|
|
# honor an immediate scroll on the first attempt (its own size/scroll
|
|
|
|
|
# bounds can still be mid-update) — verify it actually landed and
|
|
|
|
|
# retry a bounded number of times rather than guessing a fixed delay.
|
|
|
|
|
if attempt < 20 and abs(self._book_view.scroll_y - y) > 1:
|
|
|
|
|
self.call_after_refresh(lambda: self._restore_position(attempt + 1))
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
return
|
|
|
|
|
self._settle()
|
|
|
|
|
self._offer_furthest(st)
|
|
|
|
|
|
|
|
|
|
def _settle(self) -> None:
|
|
|
|
|
"""The view is where the saved position put it: from here a changed anchor with a move
|
|
|
|
|
by the reader is a reading position."""
|
|
|
|
|
self._last_anchor = self._current_anchor_str()
|
|
|
|
|
self._last_move_ms = None
|
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
|
|
|
|
|
|
|
|
def _current_anchor_str(self) -> str:
|
|
|
|
|
block_index, inner_fraction = layout.get_position_anchor(self.book_layout, self._book_view.scroll_y)
|
|
|
|
|
return blocks.format_anchor(block_index, inner_fraction)
|
|
|
|
|
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
# ---- saving
|
|
|
|
|
|
|
|
|
|
def _on_read_scroll(self) -> None:
|
|
|
|
|
self._last_move_ms = progress.now_ms()
|
|
|
|
|
|
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
|
|
|
def _autosave(self) -> None:
|
|
|
|
|
self._save_progress()
|
|
|
|
|
|
|
|
|
|
def _save_progress(self) -> None:
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
if self.book is None or not self.book_layout.heights or self._last_anchor is None:
|
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
|
|
|
return
|
|
|
|
|
anchor = self._current_anchor_str()
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
if anchor == self._last_anchor:
|
|
|
|
|
self._last_move_ms = None # moved and came back: nothing new
|
|
|
|
|
return
|
|
|
|
|
moved_at = self._last_move_ms
|
|
|
|
|
self._last_anchor = anchor
|
|
|
|
|
self._last_move_ms = None
|
|
|
|
|
if moved_at is None:
|
|
|
|
|
return # the layout moved it, not the reader
|
|
|
|
|
block_index, inner = blocks.parse_anchor(anchor)
|
|
|
|
|
fraction = blocks.fraction_for_anchor(self.book, block_index, inner)
|
|
|
|
|
self._store.write(self._key, anchor, fraction, moved_at, queue=self._cfg is not None)
|
|
|
|
|
st = self._store.get(self._key)
|
|
|
|
|
if st is not None:
|
|
|
|
|
self._held_read_at = st.read_at
|
|
|
|
|
if self._cfg is not None:
|
|
|
|
|
self._push()
|
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
|
|
|
|
|
|
|
|
@work(thread=True, exclusive=True, group="progress-push")
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
def _push(self) -> None:
|
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
|
|
|
try:
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
remote.flush_pending(self._cfg)
|
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
|
|
|
except Exception:
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
return # best-effort — the position is saved locally and stays queued
|
|
|
|
|
self.app.call_from_thread(self._after_push)
|
|
|
|
|
|
|
|
|
|
def _after_push(self) -> None:
|
|
|
|
|
# What the server stored of ours is no news; a newer position of another device in its
|
|
|
|
|
# answer (our write was refused) is.
|
|
|
|
|
st = self._store.get(self._key)
|
|
|
|
|
if st is not None and st.echo_read_at is not None and st.echo_read_at >= (self._held_read_at or 0):
|
|
|
|
|
self._held_read_at = st.echo_read_at
|
|
|
|
|
self._offer_newer()
|
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
|
|
|
|
|
|
|
|
def on_unmount(self) -> None:
|
|
|
|
|
self._save_progress()
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
if self._cfg is not None and (self._store.pending_keys() or self._ann.dirty_keys()):
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
# The screen's workers die with it; the queue still has to go out.
|
|
|
|
|
threading.Thread(target=self._flush_quietly, args=(self._cfg,), daemon=False).start()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _flush_quietly(cfg) -> None:
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
for send in (remote.flush_pending, remote.push_annotations):
|
|
|
|
|
try:
|
|
|
|
|
send(cfg)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.
Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00
|
|
|
|
|
|
|
|
# ---- the server's newer positions
|
|
|
|
|
|
|
|
|
|
@work(thread=True, exclusive=True, group="progress-check")
|
|
|
|
|
def _check_remote(self) -> None:
|
|
|
|
|
try:
|
|
|
|
|
remote.sync_library(self.path.parent, self._cfg)
|
|
|
|
|
except Exception:
|
|
|
|
|
return # offline etc.: ask again later
|
|
|
|
|
self.app.call_from_thread(self._offer_newer)
|
|
|
|
|
|
|
|
|
|
def _offer_newer(self) -> None:
|
|
|
|
|
if self.book is None or self._offer_open or self._last_anchor is None:
|
|
|
|
|
return
|
|
|
|
|
st = self._store.get(self._key)
|
|
|
|
|
if st is None or not st.anchor or st.read_at is None:
|
|
|
|
|
return
|
|
|
|
|
if self._held_read_at is not None and st.read_at <= self._held_read_at:
|
|
|
|
|
return
|
|
|
|
|
parsed = blocks.parse_anchor(st.anchor)
|
|
|
|
|
if parsed is None:
|
|
|
|
|
return
|
|
|
|
|
shown_block, _ = layout.get_position_anchor(self.book_layout, self._book_view.scroll_y)
|
|
|
|
|
self._held_read_at = st.read_at # asked once per newer state
|
|
|
|
|
if parsed[0] == shown_block:
|
|
|
|
|
return
|
|
|
|
|
pct = round(blocks.fraction_for_anchor(self.book, *parsed) * 100)
|
|
|
|
|
who = st.device or "einem anderen Gerät"
|
|
|
|
|
self._ask(f"Auf {who} weitergelesen — bei {pct} %.\nSpringen?", st.anchor)
|
|
|
|
|
|
|
|
|
|
def _offer_furthest(self, st: progress.BookState) -> None:
|
|
|
|
|
if self.book is None or not st.furthest_anchor:
|
|
|
|
|
return
|
|
|
|
|
here, there = blocks.parse_anchor(st.anchor), blocks.parse_anchor(st.furthest_anchor)
|
|
|
|
|
if there is None:
|
|
|
|
|
return
|
|
|
|
|
share = blocks.fraction_for_anchor(self.book, *there) - (blocks.fraction_for_anchor(self.book, *here) if here else 0.0)
|
|
|
|
|
if share >= FURTHEST_OFFER_SHARE:
|
|
|
|
|
pct = round(blocks.fraction_for_anchor(self.book, *there) * 100)
|
|
|
|
|
self.notify(f"Weiteste Stelle: {pct} % — mit w springen.", timeout=6)
|
|
|
|
|
|
|
|
|
|
def _ask(self, text: str, anchor: str) -> None:
|
|
|
|
|
self._offer_open = True
|
|
|
|
|
|
|
|
|
|
def answered(yes: bool | None) -> None:
|
|
|
|
|
self._offer_open = False
|
|
|
|
|
if yes:
|
|
|
|
|
self._scroll_to_anchor(anchor)
|
|
|
|
|
self._last_anchor = self._current_anchor_str()
|
|
|
|
|
self._last_move_ms = None
|
|
|
|
|
|
|
|
|
|
self.app.push_screen(JumpOfferScreen(text), answered)
|
|
|
|
|
|
|
|
|
|
def action_goto_furthest(self) -> None:
|
|
|
|
|
st = self._store.get(self._key)
|
|
|
|
|
if self.book is None or st is None or not st.furthest_anchor:
|
|
|
|
|
self.notify("Keine weiteste Stelle bekannt.", timeout=3)
|
|
|
|
|
return
|
|
|
|
|
if blocks.parse_anchor(st.furthest_anchor) is None:
|
|
|
|
|
return
|
|
|
|
|
self._scroll_to_anchor(st.furthest_anchor)
|
|
|
|
|
# A jump, not reading: nothing is saved until the reader carries on from there.
|
|
|
|
|
self._last_anchor = self._current_anchor_str()
|
|
|
|
|
self._last_move_ms = None
|
|
|
|
|
|
|
|
|
|
# ---- navigation
|
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
|
|
|
|
|
|
|
|
def action_scroll_down_line(self) -> None:
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
if self._sel is not None:
|
|
|
|
|
self._move_block(1)
|
|
|
|
|
return
|
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
|
|
|
self._book_view.scroll_relative(y=1, animate=False)
|
|
|
|
|
|
|
|
|
|
def action_scroll_up_line(self) -> None:
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
if self._sel is not None:
|
|
|
|
|
self._move_block(-1)
|
|
|
|
|
return
|
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
|
|
|
self._book_view.scroll_relative(y=-1, animate=False)
|
|
|
|
|
|
|
|
|
|
def _current_chapter_index(self) -> int:
|
|
|
|
|
if self.book is None:
|
|
|
|
|
return 0
|
|
|
|
|
block_index, _ = layout.get_position_anchor(self.book_layout, self._book_view.scroll_y)
|
|
|
|
|
return bisect.bisect_right(self.book.chapter_start_block, block_index) - 1
|
|
|
|
|
|
|
|
|
|
def _jump_to_block(self, block_index: int) -> None:
|
|
|
|
|
y = layout.scroll_y_for_anchor(self.book_layout, block_index, 0.0)
|
|
|
|
|
self._book_view.scroll_to(y=y, animate=False, immediate=True)
|
|
|
|
|
|
|
|
|
|
def action_next_chapter(self) -> None:
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
if self._sel is not None:
|
|
|
|
|
self._selection_note() # n: a note, while marking
|
|
|
|
|
return
|
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
|
|
|
if self.book is None:
|
|
|
|
|
return
|
|
|
|
|
chapter = self._current_chapter_index()
|
|
|
|
|
if chapter + 1 < len(self.book.chapter_start_block):
|
|
|
|
|
self._jump_to_block(self.book.chapter_start_block[chapter + 1])
|
|
|
|
|
self._save_progress()
|
|
|
|
|
|
|
|
|
|
def action_prev_chapter(self) -> None:
|
|
|
|
|
if self.book is None:
|
|
|
|
|
return
|
|
|
|
|
chapter = self._current_chapter_index()
|
|
|
|
|
if chapter > 0:
|
|
|
|
|
self._jump_to_block(self.book.chapter_start_block[chapter - 1])
|
|
|
|
|
self._save_progress()
|
|
|
|
|
|
|
|
|
|
def action_peek_footnote(self) -> None:
|
|
|
|
|
if self.book is None or not self.book_layout.heights:
|
|
|
|
|
return
|
|
|
|
|
current_block, _ = layout.get_position_anchor(self.book_layout, self._book_view.scroll_y)
|
|
|
|
|
|
|
|
|
|
forward = [(i, b) for i, b in enumerate(self.book.blocks) if i >= current_block and b.footnotes]
|
|
|
|
|
backward = [(i, b) for i, b in enumerate(self.book.blocks) if i < current_block and b.footnotes]
|
|
|
|
|
candidate = forward[0] if forward else (backward[-1] if backward else None)
|
|
|
|
|
if candidate is None:
|
|
|
|
|
self.notify("Keine Fußnote in diesem Buch gefunden.", timeout=3)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
_, block = candidate
|
|
|
|
|
notes: list[str] = []
|
|
|
|
|
for ref in block.footnotes:
|
|
|
|
|
target_idx = self.book.footnote_targets.get(ref.target_id)
|
|
|
|
|
if target_idx is None:
|
|
|
|
|
continue
|
|
|
|
|
notes.append(self.book.blocks[target_idx].text)
|
|
|
|
|
if not notes:
|
|
|
|
|
self.notify("Fußnote konnte nicht aufgelöst werden.", timeout=3)
|
|
|
|
|
return
|
|
|
|
|
self.app.push_screen(FootnoteScreen(notes))
|
|
|
|
|
|
|
|
|
|
def action_back(self) -> None:
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
if self._sel is not None:
|
|
|
|
|
self._end_selection()
|
|
|
|
|
return
|
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
|
|
|
self.app.pop_screen()
|
TUI: Markierungen, Notizen und Lesezeichen (anzeigen, anlegen, löschen, abgleichen)
Dieselben verschlüsselten Listen wie der Web-Reader (annotations.py). Die Anker des Webs
(Block-Index plus Zeichenposition im Rohtext) werden auf den angezeigten Text umgerechnet
(marks.py, blocks.collapse_map), alte XPath-Anker über das Zitat gefunden. Im Reader: v markiert
(Cursor je Wort, Anfang/Ende, Farbe oder Notiz), n legt eine Notiz an, b ein Lesezeichen,
a öffnet die Liste (springen, Notiz ändern, löschen).
Der Server ersetzt die Liste als Ganzes; die TUI legt ihre Änderungen vor dem Schreiben
dreiwege je Eintrag-ID auf die aktuelle Liste des Servers, offline bleiben sie vorgemerkt.
Geprüft: die echten resolveAnchorRange des Webs lösen 81 TUI-Anker exakt auf
(tools/parity/web_anchors.cjs), 34 von 34 echten Web-Markierungen werden angezeigt, der
Server-Smoke schreibt und mischt zwei Geräte. 75 Tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 16:42:32 +01:00
|
|
|
|
|
|
|
|
# ---- marks on screen
|
|
|
|
|
|
|
|
|
|
def _marks_by_block(self) -> dict[int, list]:
|
|
|
|
|
spans: dict[int, list] = {}
|
|
|
|
|
if self._marks is not None:
|
|
|
|
|
for bi, lst in self._marks.spans(self._ann.get(self._key).highlights).items():
|
|
|
|
|
spans.setdefault(bi, []).extend(lst)
|
|
|
|
|
for bi, lst in self._selection_spans().items():
|
|
|
|
|
spans.setdefault(bi, []).extend(lst)
|
|
|
|
|
return spans
|
|
|
|
|
|
|
|
|
|
def _apply_marks(self) -> None:
|
|
|
|
|
"""Draw the highlights (and the selection) in place; blocks that lost theirs are drawn plain."""
|
|
|
|
|
if self.book is None or not self.book_layout.heights:
|
|
|
|
|
return
|
|
|
|
|
spans = self._marks_by_block()
|
|
|
|
|
console = Console(width=self.book_layout.width)
|
|
|
|
|
for bi in sorted(self._styled | set(spans)):
|
|
|
|
|
if 0 <= bi < len(self.book.blocks):
|
|
|
|
|
layout.restyle_block(self.book_layout, bi, self.book.blocks[bi].text, console, spans.get(bi, ()))
|
|
|
|
|
self._styled = set(spans)
|
|
|
|
|
self._book_view.refresh()
|
|
|
|
|
|
|
|
|
|
@work(thread=True, exclusive=True, group="ann-pull")
|
|
|
|
|
def _pull_annotations(self) -> None:
|
|
|
|
|
if remote.pull_annotations(self._cfg, self._server_id, self._key):
|
|
|
|
|
self.app.call_from_thread(self._apply_marks)
|
|
|
|
|
|
|
|
|
|
def _annotations_changed(self) -> None:
|
|
|
|
|
"""A highlight, note or bookmark was made, changed or deleted: draw it, and send it soon."""
|
|
|
|
|
self._apply_marks()
|
|
|
|
|
if self._cfg is None:
|
|
|
|
|
return
|
|
|
|
|
if self._ann_timer is not None:
|
|
|
|
|
self._ann_timer.stop()
|
|
|
|
|
self._ann_timer = self.set_timer(2.0, self._push_annotations) # like the web's debounce
|
|
|
|
|
|
|
|
|
|
@work(thread=True, exclusive=True, group="ann-push")
|
|
|
|
|
def _push_annotations(self) -> None:
|
|
|
|
|
try:
|
|
|
|
|
remote.push_annotations(self._cfg)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass # stays queued
|
|
|
|
|
|
|
|
|
|
# ---- bookmarks and the list
|
|
|
|
|
|
|
|
|
|
def action_bookmark(self) -> None:
|
|
|
|
|
if self.book is None or not self.book_layout.heights or self._sel is not None:
|
|
|
|
|
return
|
|
|
|
|
block, inner = layout.get_position_anchor(self.book_layout, self._book_view.scroll_y)
|
|
|
|
|
chapter = max(0, self._current_chapter_index())
|
|
|
|
|
src = self.book.chapter_srcs[chapter] if chapter < len(self.book.chapter_srcs) else ""
|
|
|
|
|
label = re.sub(r"\.x?html?$", "", src.rsplit("/", 1)[-1], flags=re.I) or "Lesezeichen" # as the web names it
|
|
|
|
|
bm = annotations.new_bookmark(label, src, blocks.fraction_for_anchor(self.book, block, inner))
|
|
|
|
|
self._ann.add(self._key, "bookmarks", bm)
|
|
|
|
|
self.notify(f"★ Lesezeichen: {label}", timeout=2.5)
|
|
|
|
|
self._annotations_changed()
|
|
|
|
|
|
|
|
|
|
def action_annotations(self) -> None:
|
|
|
|
|
if self.book is None or self._sel is not None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def picked(block: int | None) -> None:
|
|
|
|
|
if block is not None:
|
|
|
|
|
self._jump_quiet(block)
|
|
|
|
|
|
|
|
|
|
self.app.push_screen(AnnotationsScreen(self.book, self._ann, self._key, self._annotations_changed, self._marks), picked)
|
|
|
|
|
|
|
|
|
|
def _jump_quiet(self, block: int) -> None:
|
|
|
|
|
"""Go to a block as a jump, not as reading: nothing is saved until the reader moves on."""
|
|
|
|
|
y = layout.scroll_y_for_anchor(self.book_layout, block, 0.0)
|
|
|
|
|
with self._book_view.program_scroll():
|
|
|
|
|
self._book_view.scroll_to(y=y, animate=False, immediate=True)
|
|
|
|
|
self._last_anchor = self._current_anchor_str()
|
|
|
|
|
self._last_move_ms = None
|
|
|
|
|
|
|
|
|
|
# ---- selecting text (v): a cursor by word, Enter sets the start, Enter again the end
|
|
|
|
|
|
|
|
|
|
def _first_block_in_view(self) -> int:
|
|
|
|
|
y = self._book_view.scroll_y
|
|
|
|
|
i = bisect.bisect_left(self.book_layout.starts, y)
|
|
|
|
|
return min(i, len(self.book.blocks) - 1)
|
|
|
|
|
|
|
|
|
|
def _word_block(self, block: int, step: int) -> int | None:
|
|
|
|
|
"""The nearest block from `block` (inclusive) in direction `step` that has a word."""
|
|
|
|
|
while 0 <= block < len(self.book.blocks):
|
|
|
|
|
if marks.word_starts(self.book.blocks[block].text):
|
|
|
|
|
return block
|
|
|
|
|
block += step
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def action_select(self) -> None:
|
|
|
|
|
if self.book is None or not self.book_layout.heights:
|
|
|
|
|
return
|
|
|
|
|
if self._sel is not None:
|
|
|
|
|
self.action_confirm()
|
|
|
|
|
return
|
|
|
|
|
block = self._word_block(self._first_block_in_view(), 1)
|
|
|
|
|
if block is None:
|
|
|
|
|
return
|
|
|
|
|
self._sel = {"phase": "start", "cursor": (block, marks.word_starts(self.book.blocks[block].text)[0]), "start": None}
|
|
|
|
|
self.notify("Markieren: h/l Wort, j/k Absatz, Enter = Anfang, n = Notiz, Esc = Abbrechen", timeout=6)
|
|
|
|
|
self._selection_moved()
|
|
|
|
|
|
|
|
|
|
def _end_selection(self) -> None:
|
|
|
|
|
self._sel = None
|
|
|
|
|
self._apply_marks()
|
|
|
|
|
|
|
|
|
|
def _selection_moved(self) -> None:
|
|
|
|
|
self._apply_marks()
|
|
|
|
|
block, char = self._sel["cursor"]
|
|
|
|
|
text = self.book.blocks[block].text
|
|
|
|
|
row = self.book_layout.starts[block] + int(char / max(1, len(text)) * self.book_layout.heights[block])
|
|
|
|
|
top, height = self._book_view.scroll_y, self._book_view.size.height
|
|
|
|
|
if not (top <= row < top + height - 1):
|
|
|
|
|
with self._book_view.program_scroll():
|
|
|
|
|
self._book_view.scroll_to(y=max(0, row - height // 3), animate=False, immediate=True)
|
|
|
|
|
self._last_anchor = self._current_anchor_str()
|
|
|
|
|
|
|
|
|
|
def action_word_forward(self) -> None:
|
|
|
|
|
self._move_word(1)
|
|
|
|
|
|
|
|
|
|
def action_word_back(self) -> None:
|
|
|
|
|
self._move_word(-1)
|
|
|
|
|
|
|
|
|
|
def _move_word(self, step: int) -> None:
|
|
|
|
|
if self._sel is None:
|
|
|
|
|
return
|
|
|
|
|
block, char = self._sel["cursor"]
|
|
|
|
|
text = self.book.blocks[block].text
|
|
|
|
|
target = marks.next_word(text, char) if step > 0 else marks.prev_word(text, char)
|
|
|
|
|
if target is not None:
|
|
|
|
|
self._sel["cursor"] = (block, target)
|
|
|
|
|
else:
|
|
|
|
|
nb = self._word_block(block + step, step)
|
|
|
|
|
if nb is None:
|
|
|
|
|
return
|
|
|
|
|
ws = marks.word_starts(self.book.blocks[nb].text)
|
|
|
|
|
self._sel["cursor"] = (nb, ws[0] if step > 0 else ws[-1])
|
|
|
|
|
self._selection_moved()
|
|
|
|
|
|
|
|
|
|
def _move_block(self, step: int) -> None:
|
|
|
|
|
nb = self._word_block(self._sel["cursor"][0] + step, step)
|
|
|
|
|
if nb is not None:
|
|
|
|
|
self._sel["cursor"] = (nb, marks.word_starts(self.book.blocks[nb].text)[0])
|
|
|
|
|
self._selection_moved()
|
|
|
|
|
|
|
|
|
|
def _cursor_word(self) -> tuple[int, int, int]:
|
|
|
|
|
block, char = self._sel["cursor"]
|
|
|
|
|
w = marks.word_at(self.book.blocks[block].text, char) or (char, char)
|
|
|
|
|
return block, w[0], w[1]
|
|
|
|
|
|
|
|
|
|
def _selection_range(self) -> tuple[tuple[int, int], tuple[int, int]]:
|
|
|
|
|
"""(start, end) of what is selected, end exclusive, whichever way the cursor went."""
|
|
|
|
|
sb, sc = self._sel["start"]
|
|
|
|
|
sw = marks.word_at(self.book.blocks[sb].text, sc) or (sc, sc)
|
|
|
|
|
cb, cs, ce = self._cursor_word()
|
|
|
|
|
if (cb, cs) >= (sb, sw[0]):
|
|
|
|
|
return (sb, sw[0]), (cb, ce)
|
|
|
|
|
return (cb, cs), (sb, sw[1])
|
|
|
|
|
|
|
|
|
|
def _selection_spans(self) -> dict[int, list]:
|
|
|
|
|
if self._sel is None or self.book is None:
|
|
|
|
|
return {}
|
|
|
|
|
if self._sel["phase"] == "start":
|
|
|
|
|
block, s, e = self._cursor_word()
|
|
|
|
|
return {block: [(s, e, marks.SELECTION_STYLE)]}
|
|
|
|
|
(sb, sc), (eb, ec) = self._selection_range()
|
|
|
|
|
out: dict[int, list] = {}
|
|
|
|
|
for bi in range(sb, eb + 1):
|
|
|
|
|
text_len = len(self.book.blocks[bi].text)
|
|
|
|
|
out[bi] = [(sc if bi == sb else 0, ec if bi == eb else text_len, marks.SELECTION_STYLE)]
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
def action_confirm(self) -> None:
|
|
|
|
|
if self._sel is None:
|
|
|
|
|
return
|
|
|
|
|
if self._sel["phase"] == "start":
|
|
|
|
|
block, s, _e = self._cursor_word()
|
|
|
|
|
self._sel["start"] = (block, s)
|
|
|
|
|
self._sel["phase"] = "end"
|
|
|
|
|
self.notify("Ende wählen (h/l/j/k), Enter = fertig.", timeout=4)
|
|
|
|
|
self._selection_moved()
|
|
|
|
|
return
|
|
|
|
|
(sb, sc), (eb, ec) = self._selection_range()
|
|
|
|
|
if eb - sb > MAX_MARK_BLOCKS:
|
|
|
|
|
self.notify(f"Zu lang zum Markieren (mehr als {MAX_MARK_BLOCKS} Absätze).", severity="warning")
|
|
|
|
|
return
|
|
|
|
|
anchor = self._marks.anchor((sb, sc), (eb, ec))
|
|
|
|
|
if anchor is None:
|
|
|
|
|
self.notify("Diese Stelle lässt sich nicht genau zuordnen (Fußnotenmarker mit Leerraum).", severity="warning")
|
|
|
|
|
return
|
|
|
|
|
names = [annotations.COLOR_NAMES[c] for c in annotations.COLORS] + ["Gelb mit Notiz …"]
|
|
|
|
|
colors = list(annotations.COLORS)
|
|
|
|
|
|
|
|
|
|
def picked(i: int | None) -> None:
|
|
|
|
|
if i is None:
|
|
|
|
|
return # the selection stays: pick again or Esc
|
|
|
|
|
if i < len(colors):
|
|
|
|
|
self._add_highlight(anchor, colors[i], "")
|
|
|
|
|
else:
|
|
|
|
|
self.app.push_screen(NoteScreen("Notiz zur Markierung"), lambda text: self._add_highlight(anchor, "yellow", text) if text is not None else None)
|
|
|
|
|
|
|
|
|
|
from .library_screen import ChoiceScreen
|
|
|
|
|
|
|
|
|
|
self.app.push_screen(ChoiceScreen("Markieren als", names, 0), picked)
|
|
|
|
|
|
|
|
|
|
def _add_highlight(self, anchor: dict, color: str, note: str) -> None:
|
|
|
|
|
self._ann.add(self._key, "highlights", annotations.new_highlight(anchor, color, note))
|
|
|
|
|
self._sel = None
|
|
|
|
|
self._annotations_changed()
|
|
|
|
|
|
|
|
|
|
def _selection_note(self) -> None:
|
|
|
|
|
"""n while marking: a freeform note at the cursor's word (phase 1) or on the selection."""
|
|
|
|
|
if self._sel["phase"] == "start":
|
|
|
|
|
block, s, e = self._cursor_word()
|
|
|
|
|
start, end = (block, s), (block, e)
|
|
|
|
|
else:
|
|
|
|
|
start, end = self._selection_range()
|
|
|
|
|
anchor = self._marks.anchor(start, end)
|
|
|
|
|
if anchor is None:
|
|
|
|
|
self.notify("Diese Stelle lässt sich nicht genau zuordnen.", severity="warning")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def noted(text: str | None) -> None:
|
|
|
|
|
if text:
|
|
|
|
|
self._ann.add(self._key, "highlights", annotations.new_note(anchor, text))
|
|
|
|
|
self._sel = None
|
|
|
|
|
self._annotations_changed()
|
|
|
|
|
|
|
|
|
|
self.app.push_screen(NoteScreen("Notiz"), noted)
|