Python + Textual, unter tui/. Liest EPUBs aus einem Bibliotheksordner, zeigt Kapitel als Fließtext, j/k/Pfeiltasten zum Scrollen, n/p für Kapitelwechsel. Fortschritt wird lokal (progress.json via platformdirs) nach demselben "furthest wins"-Prinzip wie diora's Web-Reader gespeichert, damit ein Sync-Layer gegen die künftige diora-API später ohne Rewrite andocken kann. Vorerst rein lokal, kein Server-Kontakt, keine Verschlüsselung.
63 lines
1.9 KiB
Python
63 lines
1.9 KiB
Python
"""Local reading-progress storage.
|
|
|
|
Mirrors diora's web-reader "furthest wins" semantics (see books/models.py,
|
|
EBookProgress.save_progress): a saved position only ever advances, so an
|
|
older or offline run can't regress a further-along read position. The
|
|
position is kept anchor-shaped (chapter index + fraction within it) so a
|
|
future sync layer against diora's API can adopt it without a rewrite.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
from platformdirs import user_data_dir
|
|
|
|
_DATA_DIR = Path(user_data_dir("diora-tui", "diora"))
|
|
_PROGRESS_FILE = _DATA_DIR / "progress.json"
|
|
|
|
|
|
@dataclass
|
|
class Position:
|
|
chapter_index: int
|
|
scroll_fraction: float # 0..1 within the chapter's paragraph list
|
|
|
|
|
|
def _load_all() -> dict[str, dict]:
|
|
if not _PROGRESS_FILE.exists():
|
|
return {}
|
|
try:
|
|
return json.loads(_PROGRESS_FILE.read_text())
|
|
except (json.JSONDecodeError, OSError):
|
|
return {}
|
|
|
|
|
|
def _save_all(data: dict[str, dict]) -> None:
|
|
_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
_PROGRESS_FILE.write_text(json.dumps(data, indent=2))
|
|
|
|
|
|
def load(book_id: str) -> Position | None:
|
|
entry = _load_all().get(book_id)
|
|
if entry is None:
|
|
return None
|
|
return Position(chapter_index=entry["chapter_index"], scroll_fraction=entry["scroll_fraction"])
|
|
|
|
|
|
def _is_further(new: Position, old: Position) -> bool:
|
|
if new.chapter_index != old.chapter_index:
|
|
return new.chapter_index > old.chapter_index
|
|
return new.scroll_fraction > old.scroll_fraction
|
|
|
|
|
|
def save(book_id: str, position: Position, *, force: bool = False) -> None:
|
|
data = _load_all()
|
|
existing = data.get(book_id)
|
|
if existing is not None and not force:
|
|
old = Position(chapter_index=existing["chapter_index"], scroll_fraction=existing["scroll_fraction"])
|
|
if not _is_further(position, old):
|
|
return
|
|
data[book_id] = asdict(position)
|
|
_save_all(data)
|