Compare commits

...

4 Commits

Author SHA1 Message Date
8e41d0b002 feat: redesign help as two-column cheat sheet 2026-01-04 11:21:52 +01:00
74691f3322 feat: compact duration formatting 2026-01-04 11:21:35 +01:00
ff1030f4bd feat: massive UI revamp 2026-01-04 11:21:12 +01:00
1bbd28888b feat: replace footer with custom top bar 2026-01-04 11:21:00 +01:00
4 changed files with 119 additions and 148 deletions

View File

@@ -6,10 +6,12 @@ from typing import TYPE_CHECKING
from textual import work
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.events import Key, Resize
from textual.widgets import DataTable, Footer, Header, ProgressBar, Static
from textual.widgets import DataTable, ProgressBar, Static
from textual.worker import get_current_worker
from . import __version__
from .constants import (
PROGRESS_COLUMN_INDEX,
SEEK_SECONDS,
@@ -77,7 +79,12 @@ class Auditui(App):
self.progress_column_index = PROGRESS_COLUMN_INDEX
def compose(self) -> ComposeResult:
yield Header()
yield Horizontal(
Static("? Help", id="top_left"),
Static(f"Auditui v{__version__}", id="top_center"),
Static("q Quit", id="top_right"),
id="top_bar",
)
yield Static("Loading...", id="status")
table: DataTable = DataTable()
table.zebra_stripes = True
@@ -85,7 +92,6 @@ class Auditui(App):
yield table
yield Static("", id="progress_info")
yield ProgressBar(id="progress_bar", show_eta=False, show_percentage=False, total=100)
yield Footer(show_command_palette=False)
def on_mount(self) -> None:
"""Initialize the table and start fetching library data."""
@@ -161,6 +167,7 @@ class Auditui(App):
def update_status(self, message: str) -> None:
"""Update the status message in the UI."""
status = self.query_one("#status", Static)
status.display = True
status.update(message)
def _apply_column_widths(self, table: DataTable) -> None:
@@ -171,19 +178,19 @@ class Auditui(App):
column_keys = list(table.columns.keys())
ratios = [ratio for _, ratio in TABLE_COLUMN_DEFS]
total_ratio = sum(ratios) or len(column_keys)
available_width = table.size.width - 2 - (len(column_keys) - 1)
content_width = table.scrollable_content_region.width
available_width = content_width
if available_width <= 0:
return
remaining = available_width
widths: list[int] = []
for ratio in ratios:
width = max(1, (available_width * ratio) // total_ratio)
widths.append(width)
remaining -= width
if remaining > 0:
widths[0] += remaining
remainder = available_width - sum(widths)
for i in range(remainder):
widths[i % len(widths)] += 1
for column_key, width in zip(column_keys, widths):
column = table.columns[column_key]
@@ -235,8 +242,8 @@ class Auditui(App):
progress, downloaded, key=title)
self.current_items = items
mode = "all" if self.show_all_mode else "unfinished"
self.update_status(f"Showing {len(items)} books ({mode})")
status = self.query_one("#status", Static)
status.display = False
def _refresh_table(self) -> None:
"""Refresh the table with current items."""
@@ -392,8 +399,7 @@ class Auditui(App):
is_currently_finished = self.library_client.is_finished(selected_item)
if is_currently_finished:
success = self.library_client.mark_as_unfinished(
asin, selected_item)
success = self.library_client.mark_as_unfinished(asin)
message = "Marked as unfinished" if success else "Failed to mark as unfinished"
else:
success = self.library_client.mark_as_finished(asin, selected_item)

View File

@@ -10,9 +10,9 @@ MIN_FILE_SIZE = 1024 * 1024
DEFAULT_CHUNK_SIZE = 8192
TABLE_COLUMN_DEFS = (
("Title", 4),
("Author", 3),
("Length", 2),
("Title", 2),
("Author", 2),
("Length", 1),
("Progress", 1),
("Downloaded", 1),
)
@@ -24,128 +24,103 @@ SEEK_SECONDS = 30.0
TABLE_CSS = """
Screen {
background: #1e1e2e;
background: #141622;
}
Header {
background: #181825;
color: #cdd6f4;
#top_bar {
background: #10131f;
color: #d5d9f0;
text-style: bold;
}
Footer {
background: #181825;
color: #bac2de;
height: 2;
padding: 0 1;
scrollbar-size: 0 0;
overflow-x: auto;
overflow-y: hidden;
}
Footer > HorizontalGroup > KeyGroup,
Footer > HorizontalGroup > KeyGroup.-compact {
height: 1;
margin: 0;
padding: 0;
background: #181825;
}
FooterKey,
FooterKey.-grouped,
Footer.-compact FooterKey {
background: #181825;
padding: 0;
margin: 0 1 0 0;
#top_left,
#top_center,
#top_right {
width: 1fr;
padding: 0 1;
background: #10131f;
margin: 0;
}
FooterKey .footer-key--key {
color: #f9e2af;
background: #181825;
text-style: bold;
padding: 0 1 0 0;
#top_left {
text-align: left;
}
FooterKey .footer-key--description {
color: #cdd6f4;
background: #181825;
padding: 0;
#top_center {
text-align: center;
}
FooterKey:hover {
background: #313244;
color: #cdd6f4;
}
FooterKey:hover .footer-key--key,
FooterKey:hover .footer-key--description {
background: #313244;
#top_right {
text-align: right;
}
DataTable {
height: 1fr;
background: #1e1e2e;
color: #cdd6f4;
border: solid #585b70;
background: #141622;
color: #d6dbf2;
border: solid #2b2f45;
scrollbar-size-horizontal: 0;
}
DataTable:focus {
border: solid #89b4fa;
border: solid #7aa2f7;
}
DataTable > .datatable--header {
background: #45475a;
color: #bac2de;
background: #1b2033;
color: #b9c3e3;
text-style: bold;
}
DataTable > .datatable--cursor {
background: #313244;
color: #cdd6f4;
background: #232842;
color: #e6ebff;
}
DataTable > .datatable--odd-row {
background: #181825;
background: #121422;
}
DataTable > .datatable--even-row {
background: #1e1e2e;
background: #15182a;
}
Static {
height: 1;
text-align: center;
background: #181825;
color: #cdd6f4;
background: #10131f;
color: #c7cfe8;
}
Static#status {
color: #bac2de;
color: #b6bfdc;
}
Static#progress_info {
color: #89b4fa;
color: #7aa2f7;
text-style: bold;
margin: 0;
padding: 0;
width: 100%;
}
ProgressBar#progress_bar {
height: 1;
background: #181825;
background: #10131f;
border: none;
margin: 0;
padding: 0 1;
width: 100%;
align: center middle;
}
ProgressBar#progress_bar > .progress-bar--track {
background: #45475a;
background: #262a3f;
}
ProgressBar#progress_bar > .progress-bar--bar {
background: #a6e3a1;
background: #8bd5ca;
}
HelpScreen {
@@ -153,93 +128,69 @@ HelpScreen {
background: rgba(0, 0, 0, 0.7);
}
HelpScreen Static {
background: transparent;
}
#help_container {
width: 88%;
max-width: 120;
min-width: 48;
height: auto;
max-height: 90%;
min-height: 16;
max-height: 80%;
min-height: 14;
background: #181a2a;
border: heavy #7aa2f7;
padding: 1 2;
}
#help_title {
width: 100%;
height: 3;
text-align: center;
text-style: bold;
color: #7aa2f7;
content-align: center middle;
margin-bottom: 1;
padding-bottom: 1;
border-bottom: solid #4b5165;
height: 3;
align: center middle;
}
#help_content {
width: 100%;
height: 1fr;
padding: 1 0;
margin: 1 0;
overflow-y: auto;
scrollbar-size: 0 1;
}
#help_content > .scrollbar--vertical {
background: #313244;
}
#help_content > .scrollbar--vertical > .scrollbar--track {
background: #181825;
}
#help_content > .scrollbar--vertical > .scrollbar--handle {
background: #585b70;
}
#help_content > .scrollbar--vertical > .scrollbar--handle:hover {
background: #45475a;
}
.help_row {
height: 3;
height: auto;
padding: 0;
margin: 0 0 1 0;
}
.help_list {
width: 1fr;
height: auto;
background: transparent;
padding: 0 1;
scrollbar-size: 0 0;
}
.help_list > ListItem {
background: #1b1f33;
border: tall #2b2f45;
align: left middle;
padding: 0 1;
}
.help_row:hover {
.help_list > ListItem:hover {
background: #2a2f45;
border: tall #3b4160;
}
.help_key {
width: 22%;
min-width: 12;
text-align: right;
padding: 0 1 0 0;
color: #f9e2af;
text-style: bold;
align: right middle;
}
.help_action {
width: 78%;
text-align: left;
padding: 0 0 0 2;
color: #cdd6f4;
align: left middle;
.help_list > ListItem > Label {
width: 100%;
padding: 0;
}
#help_footer {
width: 100%;
height: 3;
text-align: center;
content-align: center middle;
color: #bac2de;
margin-top: 1;
padding-top: 1;
border-top: solid #4b5165;
height: 3;
align: center middle;
}
"""

View File

@@ -217,7 +217,7 @@ class LibraryClient:
def format_duration(
value: int | None, unit: str = "minutes", default_none: str | None = None
) -> str | None:
"""Format duration value into a human-readable string."""
"""Format duration value into a compact string."""
if value is None or value <= 0:
return default_none
@@ -227,13 +227,9 @@ class LibraryClient:
hours, minutes = divmod(total_minutes, 60)
parts = []
if hours:
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
if minutes:
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
return " ".join(parts) if parts else default_none
if hours > 0:
return f"{hours}h{minutes:02d}" if minutes else f"{hours}h"
return f"{minutes}m"
def _get_total_duration(self, asin: str, item: dict | None = None) -> float | None:
"""Get total duration in seconds, trying item data first, then API."""
@@ -274,7 +270,7 @@ class LibraryClient:
except (OSError, ValueError, KeyError):
return None
def mark_as_unfinished(self, asin: str, item: dict | None = None) -> bool:
def mark_as_unfinished(self, asin: str) -> bool:
"""Mark a book as unfinished by restoring saved position."""
saved_position = self._saved_positions.pop(asin, None)
if saved_position is None:

View File

@@ -1,9 +1,9 @@
"""UI components for the Auditui application."""
from textual.app import ComposeResult
from textual.containers import Container, Horizontal, ScrollableContainer
from textual.containers import Container, Horizontal
from textual.screen import ModalScreen
from textual.widgets import Static
from textual.widgets import Label, ListItem, ListView, Static
KEY_DISPLAY_MAP = {
@@ -17,6 +17,7 @@ KEY_DISPLAY_MAP = {
}
KEY_COLOR = "#f9e2af"
DESC_COLOR = "#cdd6f4"
class HelpScreen(ModalScreen):
@@ -42,17 +43,34 @@ class HelpScreen(ModalScreen):
description = binding.description
return key, description
def _make_item(self, binding: tuple | object) -> ListItem:
"""Create a ListItem for a single binding."""
key, description = self._parse_binding(binding)
key_display = self._format_key_display(key)
text = f"[bold {KEY_COLOR}]{key_display:>12}[/] [{DESC_COLOR}]{description}[/]"
return ListItem(Label(text))
def compose(self) -> ComposeResult:
bindings = list(self.app.BINDINGS)
mid = (len(bindings) + 1) // 2
left_bindings = bindings[:mid]
right_bindings = bindings[mid:]
with Container(id="help_container"):
yield Static("Key Bindings", id="help_title")
with ScrollableContainer(id="help_content"):
for binding in self.app.BINDINGS:
key, description = self._parse_binding(binding)
key_display = self._format_key_display(key)
with Horizontal(classes="help_row"):
yield Static(f"[bold {KEY_COLOR}]{key_display}[/]", classes="help_key")
yield Static(description, classes="help_action")
yield Static(f"Press [bold {KEY_COLOR}]?[/] or [bold {KEY_COLOR}]Escape[/] to close", id="help_footer")
with Horizontal(id="help_content"):
yield ListView(
*[self._make_item(b) for b in left_bindings],
classes="help_list",
)
yield ListView(
*[self._make_item(b) for b in right_bindings],
classes="help_list",
)
yield Static(
f"Press [bold {KEY_COLOR}]?[/] or [bold {KEY_COLOR}]Escape[/] to close",
id="help_footer",
)
def action_dismiss(self) -> None:
self.dismiss()