diora-web/tui/diora_tui/book_view.py
mrwnslz 51b7316398 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

63 lines
2.5 KiB
Python

"""Line-API scroll view for the continuous reader — renders only the rows
actually visible on screen (via Widget.render_line), reading from the flat
Strip list diora_tui.layout.build_layout() precomputed once. This is what
keeps very large books responsive: nothing here scales with book size at
paint time, only with viewport height.
"""
from __future__ import annotations
from contextlib import contextmanager
from typing import Callable
from textual.geometry import Size
from textual.scroll_view import ScrollView
from textual.strip import Strip
from .layout import BookLayout
class ContinuousBookView(ScrollView):
# We always wrap text to fit exactly, so a horizontal scrollbar should
# never be needed — and "scroll" (not "auto") for the vertical one keeps
# its gutter reserved from the very first (still-empty) layout pass, so
# the width we wrap text at later never has to guess whether a scrollbar
# will appear and steal columns out from under already-wrapped lines.
DEFAULT_CSS = """
ContinuousBookView {
overflow-x: hidden;
overflow-y: scroll;
}
"""
def __init__(self, book_layout: BookLayout, on_read_scroll: Callable[[], None] | None = None) -> None:
super().__init__()
self.book_layout = book_layout
self.virtual_size = Size(book_layout.width, book_layout.total_rows)
# Called when the reader moves the view (keys, wheel, scrollbar), not when the program does
# (restoring a position, re-wrapping after a resize, jumping on an offer): only the first
# is "arriving at a place by reading" for the reading position (docs/progress-sync.md).
self.on_read_scroll = on_read_scroll
self._program_scrolls = 0
@contextmanager
def program_scroll(self):
self._program_scrolls += 1
try:
yield
finally:
self._program_scrolls -= 1
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
super().watch_scroll_y(old_value, new_value)
if old_value != new_value and not self._program_scrolls and self.on_read_scroll is not None:
self.on_read_scroll()
def render_line(self, y: int) -> Strip:
_scroll_x, scroll_y = self.scroll_offset
row = scroll_y + y
strips = self.book_layout.row_strips
width = self.scrollable_content_region.width
if row < 0 or row >= len(strips):
return Strip.blank(width, self.rich_style)
return strips[row].crop_extend(0, width, self.rich_style)