diora-web/tui/diora_tui/app.py
mrwnslz 695037fb9c 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

103 lines
3.6 KiB
Python

"""diora-tui: terminal EPUB reader for diora, syncing books + reading position
against a diora server (see project README)."""
from __future__ import annotations
import argparse
import asyncio
from pathlib import Path
from textual.app import App
from . import config as config_mod
from . import login as login_mod, remote
from .library_screen import LibraryScreen
DEFAULT_LIBRARY = Path.home() / "Books"
class DioraTuiApp(App):
CSS_PATH = "app.tcss"
TITLE = "diora-tui"
def __init__(self, library_dir: Path) -> None:
super().__init__()
self.library_dir = library_dir
def on_mount(self) -> None:
self.push_screen(LibraryScreen(self.library_dir))
async def action_quit(self) -> None:
cfg = config_mod.load()
if cfg is not None:
try:
# Sends what is queued, then pulls: nothing read stays only here.
await asyncio.to_thread(remote.sync_library, self.library_dir, cfg)
except remote.SyncError:
pass # best-effort — don't block quitting on a sync failure
self.exit()
def run_login(server: str | None, *, paste_key: bool) -> None:
try:
cfg = login_mod.login(server, paste_key=paste_key)
except login_mod.LoginError as e:
raise SystemExit(f"Fehler: {e}")
config_mod.save(cfg)
print("Gespeichert (Token und Schlüssel geschützt). Weiter mit diora-tui oder diora-tui sync.")
def run_sync(library_dir: Path) -> None:
cfg = config_mod.load()
if cfg is None:
raise SystemExit("Nicht angemeldet: erst diora-tui login")
print(f"Verbinde zu {cfg.server_url} …")
try:
result = remote.sync_library(library_dir, cfg)
except remote.SyncError as e:
raise SystemExit(f"Fehler: {e}")
print(f"{result.books} EPUB(s) auf dem Server (geladen wird beim Öffnen)." )
if result.pushed:
print(f"Lesestand von {result.pushed} Buch/Bücher an den Server geschickt.")
for title in result.new_titles:
print(f" + {title}")
if result.skipped_pdf:
print(f"{len(result.skipped_pdf)} PDF(s) übersprungen (TUI liest nur EPUB): {', '.join(result.skipped_pdf)}")
if result.failed:
print(f"{len(result.failed)} Buch/Bücher nicht entschlüsselbar (falscher Schlüssel?): {', '.join(result.failed)}")
def main() -> None:
parser = argparse.ArgumentParser(description="diora-tui — EPUB-Reader im Terminal, synct mit diora")
parser.add_argument(
"--library",
type=Path,
default=DEFAULT_LIBRARY,
help=f"Verzeichnis mit EPUB-Dateien (Standard: {DEFAULT_LIBRARY})",
)
subparsers = parser.add_subparsers(dest="command")
login_parser = subparsers.add_parser("login", help="Einmal anmelden (Token + Schlüssel, geschützt gespeichert)")
login_parser.add_argument("--server", help=f"Server-URL (Standard: {login_mod.DEFAULT_SERVER})")
login_parser.add_argument("--paste-key", action="store_true", help="Buchschlüssel einfügen statt aus dem Passwort ableiten")
subparsers.add_parser("logout", help="Gespeicherte Zugangsdaten löschen")
subparsers.add_parser("sync", help="Bücherliste und Lesestand abgleichen (ohne die Oberfläche)")
args = parser.parse_args()
library_dir = args.library.expanduser()
if args.command == "login":
run_login(args.server, paste_key=args.paste_key)
return
if args.command == "logout":
print("Zugangsdaten gelöscht." if config_mod.forget() else "Keine Zugangsdaten gespeichert.")
return
if args.command == "sync":
run_sync(library_dir)
return
DioraTuiApp(library_dir).run()
if __name__ == "__main__":
main()