Compare commits

..

No commits in common. "e9683fbdbd1792c4456192e644aee89e4f3935cc" and "b1a04d2a6574c5380c38071d6bf64a3789810a2e" have entirely different histories.

7 changed files with 5 additions and 106 deletions

View file

@ -44,9 +44,6 @@ Tastenkürzel:
Der Fließtext ist auf 120 Zeichen Breite begrenzt und horizontal zentriert (lesbarer als Der Fließtext ist auf 120 Zeichen Breite begrenzt und horizontal zentriert (lesbarer als
volle Terminalbreite bei breiten Fenstern). volle Terminalbreite bei breiten Fenstern).
Unten rechts zeigt eine Statusleiste Akkustand (falls vorhanden, via `psutil`) und Uhrzeit,
sekündlich aktualisiert, auf Bibliotheks- und Reader-Ansicht.
Die Bibliotheksansicht ist nach zuletzt geöffnetem Buch sortiert (neueste zuerst; anhand Die Bibliotheksansicht ist nach zuletzt geöffnetem Buch sortiert (neueste zuerst; anhand
des Zeitstempels der zuletzt gespeicherten Position), und blendet gelesene Bücher des Zeitstempels der zuletzt gespeicherten Position), und blendet gelesene Bücher
standardmäßig aus (`EBook.is_read` aus dem Sync-Snapshot, lokal in `library.json` standardmäßig aus (`EBook.is_read` aus dem Sync-Snapshot, lokal in `library.json`

View file

@ -17,7 +17,6 @@ from textual.widgets import Footer, Header, Label, ListItem, ListView
from . import config as config_mod from . import config as config_mod
from . import crypto, epub, library_meta, progress, remote from . import crypto, epub, library_meta, progress, remote
from .reader_screen import ReaderScreen from .reader_screen import ReaderScreen
from .statusbar import StatusBar
DEFAULT_LIBRARY = Path.home() / "Books" DEFAULT_LIBRARY = Path.home() / "Books"
@ -39,7 +38,6 @@ class LibraryScreen(Screen):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
yield ListView(id="library-list") yield ListView(id="library-list")
yield StatusBar()
yield Footer() yield Footer()
def on_mount(self) -> None: def on_mount(self) -> None:

View file

@ -2,15 +2,6 @@
height: 1fr; height: 1fr;
} }
StatusBar {
dock: bottom;
height: 1;
padding: 0 1;
background: $panel;
color: $text-muted;
text-align: right;
}
#reader-scroll { #reader-scroll {
height: 1fr; height: 1fr;
padding: 1 2; padding: 1 2;

View file

@ -15,18 +15,6 @@ from .layout import BookLayout
class ContinuousBookView(ScrollView): 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) -> None: def __init__(self, book_layout: BookLayout) -> None:
super().__init__() super().__init__()
self.book_layout = book_layout self.book_layout = book_layout
@ -36,7 +24,6 @@ class ContinuousBookView(ScrollView):
_scroll_x, scroll_y = self.scroll_offset _scroll_x, scroll_y = self.scroll_offset
row = scroll_y + y row = scroll_y + y
strips = self.book_layout.row_strips strips = self.book_layout.row_strips
width = self.scrollable_content_region.width
if row < 0 or row >= len(strips): if row < 0 or row >= len(strips):
return Strip.blank(width, self.rich_style) return Strip.blank(self.size.width, self.rich_style)
return strips[row].crop_extend(0, width, self.rich_style) return strips[row].crop_extend(0, self.size.width, self.rich_style)

View file

@ -25,15 +25,9 @@ from textual.widgets import Footer, Header, Label, LoadingIndicator, Static
from . import api, blocks, cache, config as config_mod, layout, progress from . import api, blocks, cache, config as config_mod, layout, progress
from .book_view import ContinuousBookView from .book_view import ContinuousBookView
from .statusbar import StatusBar
MAX_LINE_WIDTH = 120 MAX_LINE_WIDTH = 120
AUTOSAVE_INTERVAL = 5.0 AUTOSAVE_INTERVAL = 5.0
# ContinuousBookView forces its vertical scrollbar always-on (see book_view.py)
# so this stays constant — measuring the container's width and subtracting
# this fixed amount avoids a measure-after-constrain race against book_view's
# own (possibly already-constrained-from-a-previous-layout) width.
SCROLLBAR_GUTTER = 2
_EMPTY_LAYOUT = layout.BookLayout(starts=[], heights=[], row_strips=[], total_rows=0, width=1) _EMPTY_LAYOUT = layout.BookLayout(starts=[], heights=[], row_strips=[], total_rows=0, width=1)
@ -86,8 +80,7 @@ class ReaderScreen(Screen):
def compose(self): def compose(self):
yield Header() yield Header()
yield LoadingIndicator(id="reader-loading") yield LoadingIndicator(id="reader-loading")
with Container(id="reader-scroll") as scroll_container: with Container(id="reader-scroll"):
self._scroll_container = scroll_container
# Stays visible (empty) from the start rather than toggling display # Stays visible (empty) from the start rather than toggling display
# on once loaded — a widget that's just been switched from hidden # on once loaded — a widget that's just been switched from hidden
# to visible hasn't been through a layout pass yet, so its `.size` # to visible hasn't been through a layout pass yet, so its `.size`
@ -95,21 +88,9 @@ class ReaderScreen(Screen):
# nothing to clamp against and silently resets to 0. # nothing to clamp against and silently resets to 0.
self._book_view = ContinuousBookView(_EMPTY_LAYOUT) self._book_view = ContinuousBookView(_EMPTY_LAYOUT)
yield self._book_view yield self._book_view
yield StatusBar()
yield Footer() yield Footer()
def on_mount(self) -> None: def on_mount(self) -> None:
# Deferred rather than read synchronously here: right after mount the
# container hasn't been through a layout pass yet, so its size isn't
# reliable — same lesson as the scroll-restore race below.
self.call_after_refresh(self._start_loading)
def _start_loading(self) -> None:
if self._scroll_container.size.width == 0:
# Not sized yet after all — keep deferring instead of guessing a
# fixed number of refresh cycles (mirrors _restore_position below).
self.call_after_refresh(self._start_loading)
return
self._load_book(self._content_width()) self._load_book(self._content_width())
@work(thread=True) @work(thread=True)
@ -133,21 +114,11 @@ class ReaderScreen(Screen):
self.set_interval(AUTOSAVE_INTERVAL, self._autosave) self.set_interval(AUTOSAVE_INTERVAL, self._autosave)
def _content_width(self) -> int: def _content_width(self) -> int:
# Measuring the *container* rather than book_view's own size avoids a return max(20, min(MAX_LINE_WIDTH, self.size.width - 4))
# measure-after-constrain race: once a layout has been applied,
# book_view's width is pinned to a previous value via styles.width
# (below), so re-measuring book_view itself on a later resize would
# just read that stale pinned width back instead of the new
# available space. The container's width is unaffected by that.
available = self._scroll_container.size.width - SCROLLBAR_GUTTER
return max(20, min(MAX_LINE_WIDTH, available))
def _apply_layout(self, book_layout: layout.BookLayout, width: int) -> None: def _apply_layout(self, book_layout: layout.BookLayout, width: int) -> None:
self.book_layout = book_layout self.book_layout = book_layout
# Pin book_view's outer width to content-width-plus-scrollbar so its self._book_view.styles.width = width
# *inner* content region (what render_line actually draws into) ends
# up exactly `width` — matching what block texts were wrapped at.
self._book_view.styles.width = width + SCROLLBAR_GUTTER
self._book_view.book_layout = book_layout self._book_view.book_layout = book_layout
self._book_view.virtual_size = Size(width, book_layout.total_rows) self._book_view.virtual_size = Size(width, book_layout.total_rows)
self._book_view.refresh() self._book_view.refresh()

View file

@ -1,44 +0,0 @@
"""Bottom status bar: battery level + current time, refreshed every second.
Battery is read via psutil (cross-platform); on a desktop machine with no
battery, psutil.sensors_battery() returns None and the bar just shows time.
"""
from __future__ import annotations
from datetime import datetime
from textual.widgets import Static
try:
import psutil
except ImportError:
psutil = None # battery display degrades gracefully to time-only
class StatusBar(Static):
def on_mount(self) -> None:
self._refresh()
self.set_interval(1.0, self._refresh)
def _refresh(self) -> None:
self.update(self._render_text())
def _render_text(self) -> str:
parts = []
battery = self._battery_text()
if battery:
parts.append(battery)
parts.append(datetime.now().strftime("%H:%M:%S"))
return " ".join(parts)
def _battery_text(self) -> str | None:
if psutil is None:
return None
try:
battery = psutil.sensors_battery()
except Exception:
return None
if battery is None:
return None
state = "lädt" if battery.power_plugged else "Akku"
return f"{state} {round(battery.percent)}%"

View file

@ -11,7 +11,6 @@ dependencies = [
"platformdirs>=4.0", "platformdirs>=4.0",
"requests>=2.31", "requests>=2.31",
"cryptography>=42.0", "cryptography>=42.0",
"psutil>=5.9",
] ]
[project.scripts] [project.scripts]