from rich.text import Text from textual.app import ComposeResult from textual.binding import Binding from textual.widgets import DataTable import games from tui.cache_indicator import cache_state, cache_text from tui.dialogs import ConfirmDialog, FormDialog, FormField from tui.library_pane import LibraryPane STATUS_STYLES = {"playing": "blue", "played": "green", "planning": "magenta", "replayable": "cyan"} def rating_text(game: dict) -> str: return f"{game['rating']}/10" if game.get("rating") is not None else "-" def image_state(game: dict) -> str: has_details = bool(game.get("released") or game.get("genres")) return cache_state(has_details, bool(game.get("image"))) class GamesPane(LibraryPane): BINDINGS = [ Binding("a", "add", "Add"), Binding("e", "rate", "Rate"), Binding("s", "status", "Status"), Binding("l", "links", "Links"), Binding("f", "toggle('favorite')", "Favorite"), Binding("k", "toggle('liked')", "Like"), Binding("g", "tags", "Tags"), Binding("h", "dates", "Dates"), Binding("d", "delete", "Delete"), Binding("u", "refresh", "Refresh"), Binding("r", "reload", "Reload"), ] def __init__(self, **kwargs): super().__init__(**kwargs) self.border_subtitle = "games" self.games: dict[str, dict] = {} def compose(self) -> ComposeResult: yield DataTable(cursor_type="row", zebra_stripes=True) def on_mount(self): table = self.table() table.add_column("Status", key="status") table.add_column("Rating", key="rating") table.add_column("Name", key="name") table.add_column("Released", key="released") table.add_column("Developer", key="developer") table.add_column("Profile", key="profile") table.add_column("Details", key="details") table.add_column("IGDB", key="igdb") self.reload() def reload(self, focus_key: str | None = None): table = self.table() table.clear() loaded = self.load_entries(games.list_games) self.games = {game["igdbId"]: game for game in loaded} for game in loaded: table.add_row( Text(game["status"], style=STATUS_STYLES[game["status"]]), rating_text(game), game["name"], game.get("released") or "-", (game.get("developers") or ["-"])[0], games.profile_text(game), cache_text(image_state(game)), game["igdbId"], key=game["igdbId"], ) self.restore_cursor(focus_key) def selected_game(self) -> dict | None: return self.games.get(self.selected_key()) def action_add(self): fields = [ FormField("link", "Store or IGDB link", placeholder="https://store.steampowered.com/app/2868840/Slay_the_Spire_2/"), FormField("name", "Name (searched on IGDB when the link has no match)"), FormField("status", "Status", kind="select", options=games.STATUSES, value="played"), FormField("rating", "Your rating (0 to 10, optional)", kind="rating"), FormField("image", "Image (optional site path or URL, IGDB artwork when empty)", placeholder="/img/minecraft.png"), ] self.app.push_screen(FormDialog("Add a game", fields, "Add"), self.add_game) def add_game(self, values: dict | None): if not values: return def added(game: dict): self.reload(game["igdbId"]) self.notify(f"Added {game['name']} ({game['status']})") self.run_in_background( "Looking up the game on IGDB...", lambda: games.add_game(values["link"], values["name"], values["status"], values["rating"], values["image"]), added, ) def action_rate(self): game = self.selected_game() if not game: return current = "" if game.get("rating") is None else str(game["rating"]) fields = [FormField("rating", f"Rating for {game['name']} (empty to clear)", kind="rating", value=current)] def rated(values: dict | None): if values is not None: self.run_now(lambda: games.rate_game(game["igdbId"], values["rating"]), f"Updated {game['name']}", game["igdbId"]) self.app.push_screen(FormDialog("Change rating", fields), rated) def action_status(self): game = self.selected_game() if not game: return fields = [FormField("status", f"Status for {game['name']}", kind="select", options=games.STATUSES, value=game["status"])] def changed(values: dict | None): if values: status = values["status"] self.run_now(lambda: games.set_status(game["igdbId"], status), f"{game['name']} is now {status}", game["igdbId"]) self.app.push_screen(FormDialog("Change status", fields, "Save"), changed) def action_links(self): game = self.selected_game() if not game: return fields = [ FormField("url", "Store link (empty to remove)", value=game.get("url") or ""), FormField("image", "New image (site path or URL, empty keeps the current one)", placeholder="/img/minecraft.png"), ] def saved(values: dict | None): if values is not None: self.run_now(lambda: games.set_links(game["igdbId"], values["url"], values["image"]), f"Saved links for {game['name']}", game["igdbId"]) self.app.push_screen(FormDialog("Store link and image", fields), saved) def action_toggle(self, flag: str): game = self.selected_game() if not game: return label = games.PROFILE_FLAGS[flag] message = f"{game['name']} is no longer {label}" if game.get(flag) else f"{game['name']} is now {label}" self.run_now(lambda: games.toggle_flag(game, flag), message, game["igdbId"]) def action_tags(self): game = self.selected_game() if not game: return fields = [ FormField("tags", "Tags, separated by commas", value=", ".join(game.get("tags") or []), placeholder=", ".join(games.SUGGESTED_TAGS)), FormField("note", "Why you like it (shown with your favorite)", value=game.get("note") or ""), ] def saved(values: dict | None): if values is not None: self.run_now(lambda: games.set_tags(game["igdbId"], values["tags"], values["note"]), f"Saved tags for {game['name']}", game["igdbId"]) self.app.push_screen(FormDialog("Tags and note", fields), saved) def action_dates(self): game = self.selected_game() if not game: return identifier = game["igdbId"] self.edit_dates(game["name"], game, lambda started, finished: games.set_dates(identifier, started, finished), identifier) def action_delete(self): game = self.selected_game() if not game: return def confirmed(should_delete: bool | None): if should_delete: self.run_now(lambda: games.remove_game(game["igdbId"]), f"Removed {game['name']}") self.app.push_screen(ConfirmDialog(f"Remove {game['name']}?"), confirmed) def action_refresh(self): game = self.selected_game() if not game: return def refreshed(_saved: dict): self.reload(game["igdbId"]) self.notify(f"Refreshed {game['name']} from IGDB") self.run_in_background(f"Refreshing {game['name']}...", lambda: games.refresh_game(game), refreshed)