Compare commits
4 Commits
20ef60b1e4
...
8e41d0b002
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e41d0b002 | |||
| 74691f3322 | |||
| ff1030f4bd | |||
| 1bbd28888b |
@@ -6,10 +6,12 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from textual import work
|
from textual import work
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.containers import Horizontal
|
||||||
from textual.events import Key, Resize
|
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 textual.worker import get_current_worker
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
from .constants import (
|
from .constants import (
|
||||||
PROGRESS_COLUMN_INDEX,
|
PROGRESS_COLUMN_INDEX,
|
||||||
SEEK_SECONDS,
|
SEEK_SECONDS,
|
||||||
@@ -77,7 +79,12 @@ class Auditui(App):
|
|||||||
self.progress_column_index = PROGRESS_COLUMN_INDEX
|
self.progress_column_index = PROGRESS_COLUMN_INDEX
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
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")
|
yield Static("Loading...", id="status")
|
||||||
table: DataTable = DataTable()
|
table: DataTable = DataTable()
|
||||||
table.zebra_stripes = True
|
table.zebra_stripes = True
|
||||||
@@ -85,7 +92,6 @@ class Auditui(App):
|
|||||||
yield table
|
yield table
|
||||||
yield Static("", id="progress_info")
|
yield Static("", id="progress_info")
|
||||||
yield ProgressBar(id="progress_bar", show_eta=False, show_percentage=False, total=100)
|
yield ProgressBar(id="progress_bar", show_eta=False, show_percentage=False, total=100)
|
||||||
yield Footer(show_command_palette=False)
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
"""Initialize the table and start fetching library data."""
|
"""Initialize the table and start fetching library data."""
|
||||||
@@ -161,6 +167,7 @@ class Auditui(App):
|
|||||||
def update_status(self, message: str) -> None:
|
def update_status(self, message: str) -> None:
|
||||||
"""Update the status message in the UI."""
|
"""Update the status message in the UI."""
|
||||||
status = self.query_one("#status", Static)
|
status = self.query_one("#status", Static)
|
||||||
|
status.display = True
|
||||||
status.update(message)
|
status.update(message)
|
||||||
|
|
||||||
def _apply_column_widths(self, table: DataTable) -> None:
|
def _apply_column_widths(self, table: DataTable) -> None:
|
||||||
@@ -171,19 +178,19 @@ class Auditui(App):
|
|||||||
column_keys = list(table.columns.keys())
|
column_keys = list(table.columns.keys())
|
||||||
ratios = [ratio for _, ratio in TABLE_COLUMN_DEFS]
|
ratios = [ratio for _, ratio in TABLE_COLUMN_DEFS]
|
||||||
total_ratio = sum(ratios) or len(column_keys)
|
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:
|
if available_width <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
remaining = available_width
|
|
||||||
widths: list[int] = []
|
widths: list[int] = []
|
||||||
for ratio in ratios:
|
for ratio in ratios:
|
||||||
width = max(1, (available_width * ratio) // total_ratio)
|
width = max(1, (available_width * ratio) // total_ratio)
|
||||||
widths.append(width)
|
widths.append(width)
|
||||||
remaining -= width
|
|
||||||
|
|
||||||
if remaining > 0:
|
remainder = available_width - sum(widths)
|
||||||
widths[0] += remaining
|
for i in range(remainder):
|
||||||
|
widths[i % len(widths)] += 1
|
||||||
|
|
||||||
for column_key, width in zip(column_keys, widths):
|
for column_key, width in zip(column_keys, widths):
|
||||||
column = table.columns[column_key]
|
column = table.columns[column_key]
|
||||||
@@ -235,8 +242,8 @@ class Auditui(App):
|
|||||||
progress, downloaded, key=title)
|
progress, downloaded, key=title)
|
||||||
|
|
||||||
self.current_items = items
|
self.current_items = items
|
||||||
mode = "all" if self.show_all_mode else "unfinished"
|
status = self.query_one("#status", Static)
|
||||||
self.update_status(f"Showing {len(items)} books ({mode})")
|
status.display = False
|
||||||
|
|
||||||
def _refresh_table(self) -> None:
|
def _refresh_table(self) -> None:
|
||||||
"""Refresh the table with current items."""
|
"""Refresh the table with current items."""
|
||||||
@@ -392,8 +399,7 @@ class Auditui(App):
|
|||||||
is_currently_finished = self.library_client.is_finished(selected_item)
|
is_currently_finished = self.library_client.is_finished(selected_item)
|
||||||
|
|
||||||
if is_currently_finished:
|
if is_currently_finished:
|
||||||
success = self.library_client.mark_as_unfinished(
|
success = self.library_client.mark_as_unfinished(asin)
|
||||||
asin, selected_item)
|
|
||||||
message = "Marked as unfinished" if success else "Failed to mark as unfinished"
|
message = "Marked as unfinished" if success else "Failed to mark as unfinished"
|
||||||
else:
|
else:
|
||||||
success = self.library_client.mark_as_finished(asin, selected_item)
|
success = self.library_client.mark_as_finished(asin, selected_item)
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ MIN_FILE_SIZE = 1024 * 1024
|
|||||||
DEFAULT_CHUNK_SIZE = 8192
|
DEFAULT_CHUNK_SIZE = 8192
|
||||||
|
|
||||||
TABLE_COLUMN_DEFS = (
|
TABLE_COLUMN_DEFS = (
|
||||||
("Title", 4),
|
("Title", 2),
|
||||||
("Author", 3),
|
("Author", 2),
|
||||||
("Length", 2),
|
("Length", 1),
|
||||||
("Progress", 1),
|
("Progress", 1),
|
||||||
("Downloaded", 1),
|
("Downloaded", 1),
|
||||||
)
|
)
|
||||||
@@ -24,128 +24,103 @@ SEEK_SECONDS = 30.0
|
|||||||
|
|
||||||
TABLE_CSS = """
|
TABLE_CSS = """
|
||||||
Screen {
|
Screen {
|
||||||
background: #1e1e2e;
|
background: #141622;
|
||||||
}
|
}
|
||||||
|
|
||||||
Header {
|
#top_bar {
|
||||||
background: #181825;
|
background: #10131f;
|
||||||
color: #cdd6f4;
|
color: #d5d9f0;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
height: 1;
|
||||||
|
|
||||||
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 {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #181825;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FooterKey,
|
#top_left,
|
||||||
FooterKey.-grouped,
|
#top_center,
|
||||||
Footer.-compact FooterKey {
|
#top_right {
|
||||||
background: #181825;
|
width: 1fr;
|
||||||
padding: 0;
|
padding: 0 1;
|
||||||
margin: 0 1 0 0;
|
background: #10131f;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
FooterKey .footer-key--key {
|
#top_left {
|
||||||
color: #f9e2af;
|
text-align: left;
|
||||||
background: #181825;
|
|
||||||
text-style: bold;
|
|
||||||
padding: 0 1 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FooterKey .footer-key--description {
|
#top_center {
|
||||||
color: #cdd6f4;
|
text-align: center;
|
||||||
background: #181825;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FooterKey:hover {
|
#top_right {
|
||||||
background: #313244;
|
text-align: right;
|
||||||
color: #cdd6f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
FooterKey:hover .footer-key--key,
|
|
||||||
FooterKey:hover .footer-key--description {
|
|
||||||
background: #313244;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable {
|
DataTable {
|
||||||
height: 1fr;
|
height: 1fr;
|
||||||
background: #1e1e2e;
|
background: #141622;
|
||||||
color: #cdd6f4;
|
color: #d6dbf2;
|
||||||
border: solid #585b70;
|
border: solid #2b2f45;
|
||||||
|
scrollbar-size-horizontal: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable:focus {
|
DataTable:focus {
|
||||||
border: solid #89b4fa;
|
border: solid #7aa2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable > .datatable--header {
|
DataTable > .datatable--header {
|
||||||
background: #45475a;
|
background: #1b2033;
|
||||||
color: #bac2de;
|
color: #b9c3e3;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable > .datatable--cursor {
|
DataTable > .datatable--cursor {
|
||||||
background: #313244;
|
background: #232842;
|
||||||
color: #cdd6f4;
|
color: #e6ebff;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable > .datatable--odd-row {
|
DataTable > .datatable--odd-row {
|
||||||
background: #181825;
|
background: #121422;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable > .datatable--even-row {
|
DataTable > .datatable--even-row {
|
||||||
background: #1e1e2e;
|
background: #15182a;
|
||||||
}
|
}
|
||||||
|
|
||||||
Static {
|
Static {
|
||||||
height: 1;
|
height: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: #181825;
|
background: #10131f;
|
||||||
color: #cdd6f4;
|
color: #c7cfe8;
|
||||||
}
|
}
|
||||||
|
|
||||||
Static#status {
|
Static#status {
|
||||||
color: #bac2de;
|
color: #b6bfdc;
|
||||||
}
|
}
|
||||||
|
|
||||||
Static#progress_info {
|
Static#progress_info {
|
||||||
color: #89b4fa;
|
color: #7aa2f7;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressBar#progress_bar {
|
ProgressBar#progress_bar {
|
||||||
height: 1;
|
height: 1;
|
||||||
background: #181825;
|
background: #10131f;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
width: 100%;
|
|
||||||
align: center middle;
|
align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressBar#progress_bar > .progress-bar--track {
|
ProgressBar#progress_bar > .progress-bar--track {
|
||||||
background: #45475a;
|
background: #262a3f;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressBar#progress_bar > .progress-bar--bar {
|
ProgressBar#progress_bar > .progress-bar--bar {
|
||||||
background: #a6e3a1;
|
background: #8bd5ca;
|
||||||
}
|
}
|
||||||
|
|
||||||
HelpScreen {
|
HelpScreen {
|
||||||
@@ -153,93 +128,69 @@ HelpScreen {
|
|||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HelpScreen Static {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
#help_container {
|
#help_container {
|
||||||
width: 88%;
|
width: 88%;
|
||||||
max-width: 120;
|
max-width: 120;
|
||||||
min-width: 48;
|
min-width: 48;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 90%;
|
max-height: 80%;
|
||||||
min-height: 16;
|
min-height: 14;
|
||||||
background: #181a2a;
|
background: #181a2a;
|
||||||
border: heavy #7aa2f7;
|
border: heavy #7aa2f7;
|
||||||
padding: 1 2;
|
padding: 1 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#help_title {
|
#help_title {
|
||||||
|
width: 100%;
|
||||||
|
height: 3;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
color: #7aa2f7;
|
color: #7aa2f7;
|
||||||
|
content-align: center middle;
|
||||||
margin-bottom: 1;
|
margin-bottom: 1;
|
||||||
padding-bottom: 1;
|
|
||||||
border-bottom: solid #4b5165;
|
border-bottom: solid #4b5165;
|
||||||
height: 3;
|
|
||||||
align: center middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#help_content {
|
#help_content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1fr;
|
height: auto;
|
||||||
padding: 1 0;
|
padding: 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;
|
|
||||||
margin: 0 0 1 0;
|
margin: 0 0 1 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help_list {
|
||||||
|
width: 1fr;
|
||||||
|
height: auto;
|
||||||
|
background: transparent;
|
||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
|
scrollbar-size: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help_list > ListItem {
|
||||||
background: #1b1f33;
|
background: #1b1f33;
|
||||||
border: tall #2b2f45;
|
padding: 0 1;
|
||||||
align: left middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.help_row:hover {
|
.help_list > ListItem:hover {
|
||||||
background: #2a2f45;
|
background: #2a2f45;
|
||||||
border: tall #3b4160;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.help_key {
|
.help_list > ListItem > Label {
|
||||||
width: 22%;
|
width: 100%;
|
||||||
min-width: 12;
|
padding: 0;
|
||||||
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_footer {
|
#help_footer {
|
||||||
|
width: 100%;
|
||||||
|
height: 3;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
content-align: center middle;
|
||||||
color: #bac2de;
|
color: #bac2de;
|
||||||
margin-top: 1;
|
margin-top: 1;
|
||||||
padding-top: 1;
|
|
||||||
border-top: solid #4b5165;
|
border-top: solid #4b5165;
|
||||||
height: 3;
|
|
||||||
align: center middle;
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ class LibraryClient:
|
|||||||
def format_duration(
|
def format_duration(
|
||||||
value: int | None, unit: str = "minutes", default_none: str | None = None
|
value: int | None, unit: str = "minutes", default_none: str | None = None
|
||||||
) -> str | 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:
|
if value is None or value <= 0:
|
||||||
return default_none
|
return default_none
|
||||||
|
|
||||||
@@ -227,13 +227,9 @@ class LibraryClient:
|
|||||||
|
|
||||||
hours, minutes = divmod(total_minutes, 60)
|
hours, minutes = divmod(total_minutes, 60)
|
||||||
|
|
||||||
parts = []
|
if hours > 0:
|
||||||
if hours:
|
return f"{hours}h{minutes:02d}" if minutes else f"{hours}h"
|
||||||
parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
return f"{minutes}m"
|
||||||
if minutes:
|
|
||||||
parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
|
||||||
|
|
||||||
return " ".join(parts) if parts else default_none
|
|
||||||
|
|
||||||
def _get_total_duration(self, asin: str, item: dict | None = None) -> float | None:
|
def _get_total_duration(self, asin: str, item: dict | None = None) -> float | None:
|
||||||
"""Get total duration in seconds, trying item data first, then API."""
|
"""Get total duration in seconds, trying item data first, then API."""
|
||||||
@@ -274,7 +270,7 @@ class LibraryClient:
|
|||||||
except (OSError, ValueError, KeyError):
|
except (OSError, ValueError, KeyError):
|
||||||
return None
|
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."""
|
"""Mark a book as unfinished by restoring saved position."""
|
||||||
saved_position = self._saved_positions.pop(asin, None)
|
saved_position = self._saved_positions.pop(asin, None)
|
||||||
if saved_position is None:
|
if saved_position is None:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""UI components for the Auditui application."""
|
"""UI components for the Auditui application."""
|
||||||
|
|
||||||
from textual.app import ComposeResult
|
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.screen import ModalScreen
|
||||||
from textual.widgets import Static
|
from textual.widgets import Label, ListItem, ListView, Static
|
||||||
|
|
||||||
|
|
||||||
KEY_DISPLAY_MAP = {
|
KEY_DISPLAY_MAP = {
|
||||||
@@ -17,6 +17,7 @@ KEY_DISPLAY_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KEY_COLOR = "#f9e2af"
|
KEY_COLOR = "#f9e2af"
|
||||||
|
DESC_COLOR = "#cdd6f4"
|
||||||
|
|
||||||
|
|
||||||
class HelpScreen(ModalScreen):
|
class HelpScreen(ModalScreen):
|
||||||
@@ -42,17 +43,34 @@ class HelpScreen(ModalScreen):
|
|||||||
description = binding.description
|
description = binding.description
|
||||||
return key, description
|
return key, description
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def _make_item(self, binding: tuple | object) -> ListItem:
|
||||||
with Container(id="help_container"):
|
"""Create a ListItem for a single binding."""
|
||||||
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, description = self._parse_binding(binding)
|
||||||
key_display = self._format_key_display(key)
|
key_display = self._format_key_display(key)
|
||||||
with Horizontal(classes="help_row"):
|
text = f"[bold {KEY_COLOR}]{key_display:>12}[/] [{DESC_COLOR}]{description}[/]"
|
||||||
yield Static(f"[bold {KEY_COLOR}]{key_display}[/]", classes="help_key")
|
return ListItem(Label(text))
|
||||||
yield Static(description, classes="help_action")
|
|
||||||
yield Static(f"Press [bold {KEY_COLOR}]?[/] or [bold {KEY_COLOR}]Escape[/] to close", id="help_footer")
|
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 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:
|
def action_dismiss(self) -> None:
|
||||||
self.dismiss()
|
self.dismiss()
|
||||||
|
|||||||
Reference in New Issue
Block a user