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 kramo
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
||||||
|
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator, Iterable
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shlex import quote
|
from shlex import quote
|
||||||
from types import UnionType
|
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
|
from gi.repository import Gdk, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ _GAMES_DIR = DATA_DIR / "games"
|
|||||||
_COVERS_DIR = DATA_DIR / "covers"
|
_COVERS_DIR = DATA_DIR / "covers"
|
||||||
|
|
||||||
_SPEC_VERSION = 2.0
|
_SPEC_VERSION = 2.0
|
||||||
|
_MANUALLY_ADDED_ID = "imported"
|
||||||
|
|
||||||
|
|
||||||
class Game(Gio.SimpleActionGroup):
|
class Game(Gio.SimpleActionGroup):
|
||||||
@@ -108,6 +110,14 @@ class Game(Gio.SimpleActionGroup):
|
|||||||
|
|
||||||
return game
|
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):
|
def play(self):
|
||||||
"""Run the executable command in a shell."""
|
"""Run the executable command in a shell."""
|
||||||
if Path("/.flatpak-info").exists():
|
if Path("/.flatpak-info").exists():
|
||||||
@@ -133,6 +143,20 @@ class Game(Gio.SimpleActionGroup):
|
|||||||
json.dump(properties, f, indent=4)
|
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]:
|
def _load() -> Generator[Game]:
|
||||||
for path in _GAMES_DIR.glob("*.json"):
|
for path in _GAMES_DIR.glob("*.json"):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ using Adw 1;
|
|||||||
template $GameDetails: Adw.NavigationPage {
|
template $GameDetails: Adw.NavigationPage {
|
||||||
name: "details";
|
name: "details";
|
||||||
tag: "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 {
|
child: Adw.BreakpointBin {
|
||||||
width-request: bind template.root as <Widget>.width-request;
|
width-request: bind template.root as <Widget>.width-request;
|
||||||
@@ -297,7 +298,7 @@ template $GameDetails: Adw.NavigationPage {
|
|||||||
|
|
||||||
Button {
|
Button {
|
||||||
action-name: "details.edit-done";
|
action-name: "details.edit-done";
|
||||||
label: _("Apply");
|
label: bind $_if_else(template.game as <$Game>.added, _("Apply"), _("Add")) as <string>;
|
||||||
halign: center;
|
halign: center;
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from typing import Any, cast
|
from typing import Any, TypeVar, cast
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from gi.repository import Adw, Gdk, Gio, GObject, Gtk
|
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
|
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")
|
@Gtk.Template.from_resource(f"{PREFIX}/game-details.ui")
|
||||||
class GameDetails(Adw.NavigationPage):
|
class GameDetails(Adw.NavigationPage):
|
||||||
@@ -84,31 +87,39 @@ class GameDetails(Adw.NavigationPage):
|
|||||||
self.stack.props.visible_child_name = "edit"
|
self.stack.props.visible_child_name = "edit"
|
||||||
self.name_entry.grab_focus()
|
self.name_entry.grab_focus()
|
||||||
|
|
||||||
def edit_done(self):
|
def _edit_done(self):
|
||||||
"""Save edits and exit edit mode."""
|
|
||||||
if self.stack.props.visible_child_name != "edit":
|
|
||||||
return
|
|
||||||
|
|
||||||
for prop in _EDITABLE_PROPERTIES:
|
for prop in _EDITABLE_PROPERTIES:
|
||||||
entry = getattr(self, f"{prop}_entry")
|
entry = getattr(self, f"{prop}_entry")
|
||||||
value = entry.props.text
|
value = entry.props.text
|
||||||
previous_value = getattr(self.game, prop)
|
previous_value = getattr(self.game, prop)
|
||||||
|
|
||||||
if not value and prop in _REQUIRED_PROPERTIES:
|
|
||||||
entry.props.text = previous_value
|
|
||||||
continue
|
|
||||||
|
|
||||||
if value != previous_value:
|
if value != previous_value:
|
||||||
setattr(self.game, prop, value)
|
setattr(self.game, prop, value)
|
||||||
if prop == "name":
|
if prop == "name" and not self.game.added:
|
||||||
self.emit("sort-changed")
|
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"
|
self.stack.props.visible_child_name = "details"
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _activate_edit_done(self, _entry):
|
def _activate_edit_done(self, _entry):
|
||||||
self.activate_action("details.edit-done")
|
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()
|
@Gtk.Template.Callback()
|
||||||
def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None:
|
def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None:
|
||||||
if cover and (renderer := cast(Gtk.Native, self.props.root).get_renderer()):
|
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)";
|
action: "action(win.show-hidden)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
trigger: "<Control>n";
|
||||||
|
action: "action(win.add)";
|
||||||
|
}
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
trigger: "<Control>w";
|
trigger: "<Control>w";
|
||||||
action: "action(window.close)";
|
action: "action(window.close)";
|
||||||
@@ -22,8 +27,6 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content: Adw.NavigationView navigation_view {
|
content: Adw.NavigationView navigation_view {
|
||||||
popped => $_edit_done();
|
|
||||||
|
|
||||||
Adw.NavigationPage {
|
Adw.NavigationPage {
|
||||||
title: bind template.title;
|
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]
|
[end]
|
||||||
MenuButton {
|
MenuButton {
|
||||||
icon-name: "open-menu-symbolic";
|
icon-name: "open-menu-symbolic";
|
||||||
@@ -203,6 +213,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
child: Adw.StatusPage {
|
child: Adw.StatusPage {
|
||||||
icon-name: bind template.application as <Application>.application-id;
|
icon-name: bind template.application as <Application>.application-id;
|
||||||
title: _("No Games");
|
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),
|
state_settings.get_value("sort-mode").print_(False),
|
||||||
),
|
),
|
||||||
("edit", lambda _action, param, *_: self._edit(param.get_uint32()), "u"),
|
("edit", lambda _action, param, *_: self._edit(param.get_uint32()), "u"),
|
||||||
|
("add", lambda *_: self._add()),
|
||||||
))
|
))
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
@@ -138,6 +139,10 @@ class Window(Adw.ApplicationWindow):
|
|||||||
self.navigation_view.push_by_tag("details")
|
self.navigation_view.push_by_tag("details")
|
||||||
self.details.edit()
|
self.details.edit()
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
def _add(self):
|
||||||
def _edit_done(self, *_args):
|
self.details.game = Game.for_editing()
|
||||||
self.details.edit_done()
|
|
||||||
|
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