Add games library with IGDB, file explorer and static files script
This commit is contained in:
1 parent
a16444451a
commit
ec57bd73b3
65 files changed
+2230
-352
No files matched your search
@@ -0,0 +1,195 @@
|
||||
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)
|
||||
Reference in new issue
Block a user