from rich.text import Text from textual.app import ComposeResult from textual.binding import Binding from textual.screen import Screen from textual.widgets import DataTable, Footer, Label SKIP = "skip" CHOICE_STYLES = {"read": "green", "reading": "blue", "want-to-read": "magenta", SKIP: "dim"} CHOICE_ORDER = ["read", "reading", "want-to-read", SKIP] class SeriesPicker(Screen[dict[str, str] | None]): BINDINGS = [ Binding("r", "choose('read')", "Read"), Binding("c", "choose('reading')", "Reading"), Binding("w", "choose('want-to-read')", "Want to read"), Binding("x", "choose('skip')", "Skip"), Binding("space", "cycle", "Cycle"), Binding("R", "choose_all('read')", "All read"), Binding("W", "choose_all('want-to-read')", "All want"), Binding("ctrl+s", "save", "Save"), Binding("escape", "cancel", "Cancel"), ] def __init__(self, series: dict, current_shelves: dict[str, str]): super().__init__() self.series = series self.choices = {entry["goodreadsId"]: current_shelves.get(entry["goodreadsId"], SKIP) for entry in series["books"]} self.already_listed = set(current_shelves) def compose(self) -> ComposeResult: yield Label(f"{self.series['name']} [dim]{len(self.series['books'])} books, pick a shelf for each one[/dim]", classes="editor-title") yield DataTable(cursor_type="row", zebra_stripes=True, id="series-table") yield Label("", id="series-summary", classes="series-summary") yield Footer() def on_mount(self): table = self.query_one(DataTable) table.add_column("Shelf", key="shelf") table.add_column("#", key="position") table.add_column("Title", key="title") table.add_column("Author", key="author") table.add_column("", key="listed") for entry in self.series["books"]: identifier = entry["goodreadsId"] listed = "in library" if identifier in self.already_listed else "" table.add_row(self.choice_text(identifier), entry["position"] or "-", entry["title"], ", ".join(entry["authors"]), listed, key=identifier) table.focus() self.update_summary() def choice_text(self, identifier: str) -> Text: choice = self.choices[identifier] return Text(choice, style=CHOICE_STYLES[choice]) def update_summary(self): counts = {choice: list(self.choices.values()).count(choice) for choice in CHOICE_ORDER} parts = [f"[{CHOICE_STYLES[choice]}]{counts[choice]} {choice}[/]" for choice in CHOICE_ORDER] self.query_one("#series-summary", Label).update(" ".join(parts) + " [dim]ctrl+s to save[/dim]") def selected_id(self) -> str | None: table = self.query_one(DataTable) if table.row_count == 0: return None row_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate) return row_key.value def set_choice(self, identifier: str, choice: str): self.choices[identifier] = choice self.query_one(DataTable).update_cell(identifier, "shelf", self.choice_text(identifier)) def action_choose(self, choice: str): identifier = self.selected_id() if not identifier: return self.set_choice(identifier, choice) self.update_summary() self.query_one(DataTable).action_cursor_down() def action_cycle(self): identifier = self.selected_id() if not identifier: return next_choice = CHOICE_ORDER[(CHOICE_ORDER.index(self.choices[identifier]) + 1) % len(CHOICE_ORDER)] self.set_choice(identifier, next_choice) self.update_summary() def action_choose_all(self, choice: str): for identifier in self.choices: self.set_choice(identifier, choice) self.update_summary() def action_save(self): picked = {identifier: choice for identifier, choice in self.choices.items() if choice != SKIP} self.dismiss(picked) def action_cancel(self): self.dismiss(None)