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.
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
"""On-disk cache for the (FlatBook, BookLayout) pair a book open computes —
|
|
extraction + row-layout for very large books (tens of thousands of blocks)
|
|
can take several seconds each; reopening the same book at the same terminal
|
|
width should be near-instant instead of paying that cost again every time.
|
|
|
|
Not a correctness-critical cache: any miss (new book, different width, edited
|
|
file) just falls back to recomputing from scratch, so a stale/corrupt cache
|
|
entry is handled by overwriting it, never by crashing the reader.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import pickle
|
|
from pathlib import Path
|
|
|
|
from platformdirs import user_cache_dir
|
|
|
|
from .blocks import FlatBook
|
|
from .layout import BookLayout
|
|
|
|
_CACHE_DIR = Path(user_cache_dir("diora-tui", "diora")) / "layout_cache"
|
|
|
|
|
|
def _cache_key(path: Path, width: int) -> str:
|
|
stat = path.stat()
|
|
raw = f"{path.resolve()}|{stat.st_size}|{stat.st_mtime_ns}|{width}"
|
|
return hashlib.sha256(raw.encode()).hexdigest()[:32]
|
|
|
|
|
|
def load(path: Path, width: int) -> tuple[FlatBook, BookLayout] | None:
|
|
cache_file = _CACHE_DIR / f"{_cache_key(path, width)}.pickle"
|
|
if not cache_file.exists():
|
|
return None
|
|
try:
|
|
with cache_file.open("rb") as f:
|
|
book, book_layout = pickle.load(f)
|
|
if not isinstance(book, FlatBook) or not isinstance(book_layout, BookLayout):
|
|
return None
|
|
return book, book_layout
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def save(path: Path, width: int, book: FlatBook, book_layout: BookLayout) -> None:
|
|
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
cache_file = _CACHE_DIR / f"{_cache_key(path, width)}.pickle"
|
|
try:
|
|
with cache_file.open("wb") as f:
|
|
pickle.dump((book, book_layout), f, protocol=pickle.HIGHEST_PROTOCOL)
|
|
except Exception:
|
|
pass # best-effort — a failed cache write shouldn't break reading
|