game-details: Support adding games
This commit is contained in:
committed by
Laura Kramolis
parent
1383384cb5
commit
fe8e41ecb7
@@ -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:
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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()):
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user