196 lines
7.5 KiB
Python
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)
|