Files
website/tools/content/tui/games_pane.py
T

196 lines
7.5 KiB
Python

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)