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>
63 lines
2.5 KiB
Python
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)
|