game-details: Support adding games

This commit is contained in:
Jamie Gravendeel
2025-12-03 00:41:48 +01:00
committed by Laura Kramolis
parent 1383384cb5
commit fe8e41ecb7
5 changed files with 72 additions and 20 deletions

View File

@@ -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:

View File

@@ -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 <string>;
hidden => $_exit();
child: Adw.BreakpointBin {
width-request: bind template.root as <Widget>.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 <string>;
halign: center;
styles [

View File

@@ -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()):

View File

@@ -15,6 +15,11 @@ template $Window: Adw.ApplicationWindow {
action: "action(win.show-hidden)";
}
Shortcut {
trigger: "<Control>n";
action: "action(win.add)";
}
Shortcut {
trigger: "<Control>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>.application-id;
title: _("No Games");
description: _("Use the + button to add games");
};
}
};

View File

@@ -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()