From fe8e41ecb7edd7884edca53b158dc2cca3f8d0b1 Mon Sep 17 00:00:00 2001 From: Jamie Gravendeel Date: Wed, 3 Dec 2025 00:41:48 +0100 Subject: [PATCH] game-details: Support adding games --- cartridges/games.py | 28 ++++++++++++++++++++++++++-- cartridges/ui/game-details.blp | 5 +++-- cartridges/ui/game_details.py | 33 ++++++++++++++++++++++----------- cartridges/ui/window.blp | 15 +++++++++++++-- cartridges/ui/window.py | 11 ++++++++--- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/cartridges/games.py b/cartridges/games.py index 2128318..7ce61e6 100644 --- a/cartridges/games.py +++ b/cartridges/games.py @@ -3,15 +3,16 @@ # SPDX-FileCopyrightText: Copyright 2025 kramo # SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel +import itertools import json import os import subprocess -from collections.abc import Generator +from collections.abc import Generator, Iterable from json import JSONDecodeError from pathlib import Path from shlex import quote from types import UnionType -from typing import Any, NamedTuple, Self +from typing import Any, NamedTuple, Self, cast from gi.repository import Gdk, Gio, GLib, GObject, Gtk @@ -43,6 +44,7 @@ _GAMES_DIR = DATA_DIR / "games" _COVERS_DIR = DATA_DIR / "covers" _SPEC_VERSION = 2.0 +_MANUALLY_ADDED_ID = "imported" class Game(Gio.SimpleActionGroup): @@ -108,6 +110,14 @@ class Game(Gio.SimpleActionGroup): return game + @classmethod + def for_editing(cls) -> Self: + """Create a game for the user to manually set its properties.""" + return cls( + game_id=f"{_MANUALLY_ADDED_ID}_{_increment_manually_added_id()}", + source=_MANUALLY_ADDED_ID, + ) + def play(self): """Run the executable command in a shell.""" if Path("/.flatpak-info").exists(): @@ -133,6 +143,20 @@ class Game(Gio.SimpleActionGroup): json.dump(properties, f, indent=4) +def _increment_manually_added_id() -> int: + numbers = { + game.game_id.split("_")[1] + for game in cast(Iterable[Game], model) + if game.game_id.startswith(_MANUALLY_ADDED_ID) + } + + for count in itertools.count(): + if count not in numbers: + return count + + raise ValueError + + def _load() -> Generator[Game]: for path in _GAMES_DIR.glob("*.json"): try: diff --git a/cartridges/ui/game-details.blp b/cartridges/ui/game-details.blp index 3b3d400..f7e6676 100644 --- a/cartridges/ui/game-details.blp +++ b/cartridges/ui/game-details.blp @@ -5,7 +5,8 @@ using Adw 1; template $GameDetails: Adw.NavigationPage { name: "details"; tag: "details"; - title: bind template.game as <$Game>.name; + title: bind $_or(template.game as <$Game>.name, _("Add Game")) as ; + hidden => $_exit(); child: Adw.BreakpointBin { width-request: bind template.root as .width-request; @@ -297,7 +298,7 @@ template $GameDetails: Adw.NavigationPage { Button { action-name: "details.edit-done"; - label: _("Apply"); + label: bind $_if_else(template.game as <$Game>.added, _("Apply"), _("Add")) as ; halign: center; styles [ diff --git a/cartridges/ui/game_details.py b/cartridges/ui/game_details.py index 1589737..2df78e7 100644 --- a/cartridges/ui/game_details.py +++ b/cartridges/ui/game_details.py @@ -3,9 +3,10 @@ import sys +import time from datetime import UTC, datetime from gettext import gettext as _ -from typing import Any, cast +from typing import Any, TypeVar, cast from urllib.parse import quote from gi.repository import Adw, Gdk, Gio, GObject, Gtk @@ -21,6 +22,8 @@ _REQUIRED_PROPERTIES = { prop.name for prop in games.PROPERTIES if prop.editable and prop.required } +_T = TypeVar("_T") + @Gtk.Template.from_resource(f"{PREFIX}/game-details.ui") class GameDetails(Adw.NavigationPage): @@ -84,31 +87,39 @@ class GameDetails(Adw.NavigationPage): self.stack.props.visible_child_name = "edit" self.name_entry.grab_focus() - def edit_done(self): - """Save edits and exit edit mode.""" - if self.stack.props.visible_child_name != "edit": - return - + def _edit_done(self): for prop in _EDITABLE_PROPERTIES: entry = getattr(self, f"{prop}_entry") value = entry.props.text previous_value = getattr(self.game, prop) - if not value and prop in _REQUIRED_PROPERTIES: - entry.props.text = previous_value - continue - if value != previous_value: setattr(self.game, prop, value) - if prop == "name": + if prop == "name" and not self.game.added: self.emit("sort-changed") + if not self.game.added: + self.game.added = int(time.time()) + games.model.append(self.game) + + self._exit() + + @Gtk.Template.Callback() + def _exit(self, *_args): self.stack.props.visible_child_name = "details" @Gtk.Template.Callback() def _activate_edit_done(self, _entry): self.activate_action("details.edit-done") + @Gtk.Template.Callback() + def _or(self, _obj, first: _T, second: _T) -> _T: + return first or second + + @Gtk.Template.Callback() + def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T: + return first if condition else second + @Gtk.Template.Callback() def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None: if cover and (renderer := cast(Gtk.Native, self.props.root).get_renderer()): diff --git a/cartridges/ui/window.blp b/cartridges/ui/window.blp index 46de10a..c1585c1 100644 --- a/cartridges/ui/window.blp +++ b/cartridges/ui/window.blp @@ -15,6 +15,11 @@ template $Window: Adw.ApplicationWindow { action: "action(win.show-hidden)"; } + Shortcut { + trigger: "n"; + action: "action(win.add)"; + } + Shortcut { trigger: "w"; action: "action(window.close)"; @@ -22,8 +27,6 @@ template $Window: Adw.ApplicationWindow { } content: Adw.NavigationView navigation_view { - popped => $_edit_done(); - Adw.NavigationPage { title: bind template.title; @@ -97,6 +100,13 @@ template $Window: Adw.ApplicationWindow { }; }; + [start] + Button { + icon-name: "list-add-symbolic"; + tooltip-text: _("Add Game"); + action-name: "win.add"; + } + [end] MenuButton { icon-name: "open-menu-symbolic"; @@ -203,6 +213,7 @@ template $Window: Adw.ApplicationWindow { child: Adw.StatusPage { icon-name: bind template.application as .application-id; title: _("No Games"); + description: _("Use the + button to add games"); }; } }; diff --git a/cartridges/ui/window.py b/cartridges/ui/window.py index 737652b..2b2841e 100644 --- a/cartridges/ui/window.py +++ b/cartridges/ui/window.py @@ -71,6 +71,7 @@ class Window(Adw.ApplicationWindow): state_settings.get_value("sort-mode").print_(False), ), ("edit", lambda _action, param, *_: self._edit(param.get_uint32()), "u"), + ("add", lambda *_: self._add()), )) @Gtk.Template.Callback() @@ -138,6 +139,10 @@ class Window(Adw.ApplicationWindow): self.navigation_view.push_by_tag("details") self.details.edit() - @Gtk.Template.Callback() - def _edit_done(self, *_args): - self.details.edit_done() + def _add(self): + self.details.game = Game.for_editing() + + if self.navigation_view.props.visible_page_tag != "details": + self.navigation_view.push_by_tag("details") + + self.details.edit()