details: Implement edit action
This commit is contained in:
315
cartridges/ui/game-details.blp
Normal file
315
cartridges/ui/game-details.blp
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
using Gtk 4.0;
|
||||||
|
using Gdk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $GameDetails: Adw.NavigationPage {
|
||||||
|
name: "details";
|
||||||
|
tag: "details";
|
||||||
|
title: bind template.game as <$Game>.name;
|
||||||
|
|
||||||
|
child: Adw.BreakpointBin {
|
||||||
|
width-request: bind template.root as <Widget>.width-request;
|
||||||
|
height-request: bind template.root as <Widget>.height-request;
|
||||||
|
|
||||||
|
Adw.Breakpoint {
|
||||||
|
condition ("max-width: 700px")
|
||||||
|
|
||||||
|
setters {
|
||||||
|
box.orientation: vertical;
|
||||||
|
name_label.halign: center;
|
||||||
|
name_label.justify: center;
|
||||||
|
developer_label.halign: center;
|
||||||
|
developer_label.justify: center;
|
||||||
|
date_labels.orientation: vertical;
|
||||||
|
date_labels.halign: center;
|
||||||
|
last_played_label.halign: center;
|
||||||
|
last_played_label.justify: center;
|
||||||
|
added_label.halign: center;
|
||||||
|
added_label.justify: center;
|
||||||
|
actions.orientation: vertical;
|
||||||
|
actions.halign: center;
|
||||||
|
actions.spacing: 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
child: Overlay {
|
||||||
|
child: Picture {
|
||||||
|
name: "background";
|
||||||
|
paintable: bind $_downscale_image(template.game as <$Game>.cover) as <Gdk.Texture>;
|
||||||
|
content-fit: cover;
|
||||||
|
};
|
||||||
|
|
||||||
|
[overlay]
|
||||||
|
Adw.ToolbarView {
|
||||||
|
[top]
|
||||||
|
Adw.HeaderBar {
|
||||||
|
show-title: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
content: ScrolledWindow {
|
||||||
|
hscrollbar-policy: never;
|
||||||
|
|
||||||
|
child: Box box {
|
||||||
|
halign: center;
|
||||||
|
valign: center;
|
||||||
|
margin-top: 12;
|
||||||
|
margin-bottom: 48;
|
||||||
|
margin-start: 24;
|
||||||
|
margin-end: 24;
|
||||||
|
spacing: 36;
|
||||||
|
|
||||||
|
$Cover {
|
||||||
|
paintable: bind template.game as <$Game>.cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.ViewStack stack {
|
||||||
|
vhomogeneous: false;
|
||||||
|
enable-transitions: true;
|
||||||
|
|
||||||
|
Adw.ViewStackPage {
|
||||||
|
name: "details";
|
||||||
|
|
||||||
|
child: Box {
|
||||||
|
orientation: vertical;
|
||||||
|
valign: center;
|
||||||
|
spacing: 6;
|
||||||
|
|
||||||
|
Label name_label {
|
||||||
|
label: bind template.game as <$Game>.name;
|
||||||
|
halign: start;
|
||||||
|
max-width-chars: 24;
|
||||||
|
wrap: true;
|
||||||
|
wrap-mode: word_char;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"title-1",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Label developer_label {
|
||||||
|
label: bind template.game as <$Game>.developer;
|
||||||
|
visible: bind $_bool(template.game as <$Game>.developer) as <bool>;
|
||||||
|
halign: start;
|
||||||
|
max-width-chars: 36;
|
||||||
|
wrap: true;
|
||||||
|
wrap-mode: word_char;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"heading",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Box date_labels {
|
||||||
|
halign: start;
|
||||||
|
valign: start;
|
||||||
|
margin-top: 9;
|
||||||
|
spacing: 9;
|
||||||
|
|
||||||
|
Label last_played_label {
|
||||||
|
label: bind $_date_label(_("Last played: {}"), template.game as <$Game>.last_played) as <string>;
|
||||||
|
halign: start;
|
||||||
|
wrap: true;
|
||||||
|
wrap-mode: word_char;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label added_label {
|
||||||
|
label: bind $_date_label(_("Added: {}"), template.game as <$Game>.added) as <string>;
|
||||||
|
halign: start;
|
||||||
|
wrap: true;
|
||||||
|
wrap-mode: word_char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box actions {
|
||||||
|
spacing: 12;
|
||||||
|
margin-top: 15;
|
||||||
|
|
||||||
|
Button {
|
||||||
|
action-name: "game.play";
|
||||||
|
label: _("Play");
|
||||||
|
valign: center;
|
||||||
|
halign: center;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"pill",
|
||||||
|
"suggested-action",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
spacing: 6;
|
||||||
|
halign: center;
|
||||||
|
|
||||||
|
Button {
|
||||||
|
action-name: "details.edit";
|
||||||
|
icon-name: "document-edit-symbolic";
|
||||||
|
tooltip-text: _("Edit");
|
||||||
|
valign: center;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"circular",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Button hide_button {
|
||||||
|
visible: bind hide_button.sensitive;
|
||||||
|
action-name: "game.hide";
|
||||||
|
icon-name: "view-conceal-symbolic";
|
||||||
|
tooltip-text: _("Hide");
|
||||||
|
valign: center;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"circular",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Button unhide_button {
|
||||||
|
visible: bind unhide_button.sensitive;
|
||||||
|
action-name: "game.unhide";
|
||||||
|
icon-name: "view-reveal-symbolic";
|
||||||
|
tooltip-text: _("Unhide");
|
||||||
|
valign: center;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"circular",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
action-name: "game.remove";
|
||||||
|
icon-name: "user-trash-symbolic";
|
||||||
|
tooltip-text: _("Remove");
|
||||||
|
valign: center;
|
||||||
|
clicked => $_pop();
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"circular",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuButton {
|
||||||
|
icon-name: "edit-find-symbolic";
|
||||||
|
tooltip-text: _("Search On");
|
||||||
|
valign: center;
|
||||||
|
|
||||||
|
menu-model: menu {
|
||||||
|
section {
|
||||||
|
label: _("Search On");
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("HowLongToBeat");
|
||||||
|
action: "details.search-on";
|
||||||
|
target: "https://howlongtobeat.com/?q={}";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("IGDB");
|
||||||
|
action: "details.search-on";
|
||||||
|
target: "https://www.igdb.com/search?type=1&q={}";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("Lutris");
|
||||||
|
action: "details.search-on";
|
||||||
|
target: "https://lutris.net/games?q={}";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("PCGamingWiki");
|
||||||
|
action: "details.search-on";
|
||||||
|
target: "https://www.pcgamingwiki.com/w/index.php?search={}";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("ProtonDB");
|
||||||
|
action: "details.search-on";
|
||||||
|
target: "https://www.protondb.com/search?q={}";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("SteamGridDB");
|
||||||
|
action: "details.search-on";
|
||||||
|
target: "https://www.steamgriddb.com/search/grids?term={}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"circular",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.ViewStackPage {
|
||||||
|
name: "edit";
|
||||||
|
|
||||||
|
child: Box {
|
||||||
|
orientation: vertical;
|
||||||
|
valign: center;
|
||||||
|
width-request: 300;
|
||||||
|
spacing: 24;
|
||||||
|
|
||||||
|
Adw.PreferencesGroup {
|
||||||
|
Adw.EntryRow name_entry {
|
||||||
|
title: _("Title");
|
||||||
|
action-name: "details.edit-done";
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.EntryRow developer_entry {
|
||||||
|
title: _("Developer (optional)");
|
||||||
|
action-name: "details.edit-done";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.PreferencesGroup {
|
||||||
|
Adw.EntryRow executable_entry {
|
||||||
|
title: _("Executable");
|
||||||
|
action-name: "details.edit-done";
|
||||||
|
|
||||||
|
[suffix]
|
||||||
|
MenuButton {
|
||||||
|
valign: center;
|
||||||
|
tooltip-text: _("More Info");
|
||||||
|
icon-name: "info-outline-symbolic";
|
||||||
|
|
||||||
|
popover: Popover {
|
||||||
|
child: Label {
|
||||||
|
label: bind $_format_more_info(_("To launch the executable \"{}\", use the command:\n\n<tt>\"{}\"</tt>\n\nTo open the file \"{}\" with the default application, use:\n\n<tt>{} \"{}\"</tt>\n\nIf the path contains spaces, make sure to wrap it in double quotes!")) as <string>;
|
||||||
|
use-markup: true;
|
||||||
|
wrap: true;
|
||||||
|
wrap-mode: word_char;
|
||||||
|
margin-top: 12;
|
||||||
|
margin-bottom: 12;
|
||||||
|
margin-start: 12;
|
||||||
|
margin-end: 12;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"flat",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
action-name: "details.edit-done";
|
||||||
|
label: _("Done");
|
||||||
|
halign: center;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"pill",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -32,15 +32,17 @@ template $GameItem: Box {
|
|||||||
margin-end: 6;
|
margin-end: 6;
|
||||||
|
|
||||||
menu-model: menu {
|
menu-model: menu {
|
||||||
|
item (_("Edit"), "item.edit")
|
||||||
|
|
||||||
item {
|
item {
|
||||||
action: "game.hide";
|
|
||||||
label: _("Hide");
|
label: _("Hide");
|
||||||
|
action: "game.hide";
|
||||||
hidden-when: "action-disabled";
|
hidden-when: "action-disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
action: "game.unhide";
|
|
||||||
label: _("Unhide");
|
label: _("Unhide");
|
||||||
|
action: "game.unhide";
|
||||||
hidden-when: "action-disabled";
|
hidden-when: "action-disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
149
cartridges/ui/game_details.py
Normal file
149
cartridges/ui/game_details.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from gettext import gettext as _
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
from gi.repository import Adw, Gdk, Gio, GObject, Gtk
|
||||||
|
|
||||||
|
from cartridges.config import PREFIX
|
||||||
|
from cartridges.games import Game
|
||||||
|
|
||||||
|
from .cover import Cover # noqa: F401
|
||||||
|
|
||||||
|
_EDITABLE_PROPERTIES = "name", "developer", "executable"
|
||||||
|
|
||||||
|
|
||||||
|
@Gtk.Template.from_resource(f"{PREFIX}/game-details.ui")
|
||||||
|
class GameDetails(Adw.NavigationPage):
|
||||||
|
"""The details of a game."""
|
||||||
|
|
||||||
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
|
stack: Adw.ViewStack = Gtk.Template.Child()
|
||||||
|
name_entry: Adw.EntryRow = Gtk.Template.Child()
|
||||||
|
developer_entry: Adw.EntryRow = Gtk.Template.Child()
|
||||||
|
executable_entry: Adw.EntryRow = Gtk.Template.Child()
|
||||||
|
|
||||||
|
sort_changed = GObject.Signal()
|
||||||
|
|
||||||
|
@GObject.Property(type=Game)
|
||||||
|
def game(self) -> Game | None:
|
||||||
|
"""The game whose details to show."""
|
||||||
|
return self._game
|
||||||
|
|
||||||
|
@game.setter
|
||||||
|
def game(self, game: Game | None):
|
||||||
|
self._game = game
|
||||||
|
self.insert_action_group("game", game)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.insert_action_group("details", group := Gio.SimpleActionGroup())
|
||||||
|
group.add_action_entries((
|
||||||
|
("edit", lambda *_: self.edit()),
|
||||||
|
("edit-done", lambda *_: self.edit_done()),
|
||||||
|
(
|
||||||
|
"search-on",
|
||||||
|
lambda _action, param, *_: Gio.AppInfo.launch_default_for_uri(
|
||||||
|
param.get_string().format(quote(self.game.name))
|
||||||
|
),
|
||||||
|
"s",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
def edit(self):
|
||||||
|
"""Enter edit mode."""
|
||||||
|
for prop in _EDITABLE_PROPERTIES:
|
||||||
|
getattr(self, f"{prop}_entry").props.text = getattr(self.game, prop)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
for prop in _EDITABLE_PROPERTIES:
|
||||||
|
text = getattr(self, f"{prop}_entry").props.text
|
||||||
|
if text != getattr(self.game, prop) and (text or prop == "developer"):
|
||||||
|
setattr(self.game, prop, text)
|
||||||
|
if prop == "name":
|
||||||
|
self.emit("sort-changed")
|
||||||
|
|
||||||
|
self.stack.props.visible_child_name = "details"
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None:
|
||||||
|
if (
|
||||||
|
cover
|
||||||
|
and isinstance(root := self.props.root, Gtk.Native)
|
||||||
|
and (renderer := root.get_renderer())
|
||||||
|
):
|
||||||
|
cover.snapshot(snapshot := Gtk.Snapshot.new(), 3, 3)
|
||||||
|
if node := snapshot.to_node():
|
||||||
|
return renderer.render_texture(node)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _date_label(self, _obj, label: str, timestamp: int) -> str:
|
||||||
|
date = datetime.fromtimestamp(timestamp, UTC)
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
return label.format(
|
||||||
|
_("Never")
|
||||||
|
if not timestamp
|
||||||
|
else _("Today")
|
||||||
|
if (n_days := (now - date).days) == 0
|
||||||
|
else _("Yesterday")
|
||||||
|
if n_days == 1
|
||||||
|
else date.strftime("%A")
|
||||||
|
if n_days <= (day_of_week := now.weekday())
|
||||||
|
else _("Last Week")
|
||||||
|
if n_days <= day_of_week + 7
|
||||||
|
else _("This Month")
|
||||||
|
if n_days <= (day_of_month := now.day)
|
||||||
|
else _("Last Month")
|
||||||
|
if n_days <= day_of_month + 30
|
||||||
|
else date.strftime("%B")
|
||||||
|
if n_days < (day_of_year := now.timetuple().tm_yday)
|
||||||
|
else _("Last Year")
|
||||||
|
if n_days <= day_of_year + 365
|
||||||
|
else date.strftime("%Y")
|
||||||
|
)
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _bool(self, _obj, o: object) -> bool:
|
||||||
|
return bool(o)
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _pop(self, _obj):
|
||||||
|
self.activate_action("navigation.pop")
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _format_more_info(self, _obj, label: str) -> str:
|
||||||
|
executable = _("program")
|
||||||
|
filename = _("file.txt")
|
||||||
|
path = _("/path/to/{}")
|
||||||
|
command = "xdg-open"
|
||||||
|
|
||||||
|
if sys.platform.startswith("darwin"):
|
||||||
|
command = "open"
|
||||||
|
elif sys.platform.startswith("win32"):
|
||||||
|
executable += ".exe"
|
||||||
|
path = _(r"C:\path\to\{}")
|
||||||
|
command = "start"
|
||||||
|
|
||||||
|
return label.format(
|
||||||
|
executable,
|
||||||
|
path.format(executable),
|
||||||
|
filename,
|
||||||
|
command,
|
||||||
|
path.format(filename),
|
||||||
|
)
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
||||||
|
|
||||||
from gi.repository import GObject, Gtk
|
from typing import Any
|
||||||
|
|
||||||
|
from gi.repository import Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from cartridges.config import PREFIX
|
from cartridges.config import PREFIX
|
||||||
from cartridges.games import Game
|
from cartridges.games import Game
|
||||||
@@ -15,6 +17,8 @@ class GameItem(Gtk.Box):
|
|||||||
|
|
||||||
__gtype_name__ = __qualname__
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
|
position = GObject.Property(type=int)
|
||||||
|
|
||||||
@GObject.Property(type=Game)
|
@GObject.Property(type=Game)
|
||||||
def game(self) -> Game | None:
|
def game(self) -> Game | None:
|
||||||
"""The game that `self` represents."""
|
"""The game that `self` represents."""
|
||||||
@@ -25,6 +29,19 @@ class GameItem(Gtk.Box):
|
|||||||
self._game = game
|
self._game = game
|
||||||
self.insert_action_group("game", game)
|
self.insert_action_group("game", game)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs: Any):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.insert_action_group("item", group := Gio.SimpleActionGroup())
|
||||||
|
group.add_action_entries((
|
||||||
|
(
|
||||||
|
"edit",
|
||||||
|
lambda *_: self.activate_action(
|
||||||
|
"win.edit", GLib.Variant.new_uint32(self.position)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _any(self, _obj, *values: bool) -> bool:
|
def _any(self, _obj, *values: bool) -> bool:
|
||||||
return any(values)
|
return any(values)
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
python.install_sources(
|
python.install_sources(
|
||||||
files('__init__.py', 'cover.py', 'game_item.py', 'window.py'),
|
files(
|
||||||
|
'__init__.py',
|
||||||
|
'cover.py',
|
||||||
|
'game_details.py',
|
||||||
|
'game_item.py',
|
||||||
|
'window.py',
|
||||||
|
),
|
||||||
subdir: 'cartridges' / 'ui',
|
subdir: 'cartridges' / 'ui',
|
||||||
)
|
)
|
||||||
|
|
||||||
blueprints = custom_target(
|
blueprints = custom_target(
|
||||||
input: files('cover.blp', 'game-item.blp', 'window.blp'),
|
input: files('cover.blp', 'game-details.blp', 'game-item.blp', 'window.blp'),
|
||||||
output: '.',
|
output: '.',
|
||||||
command: [
|
command: [
|
||||||
blueprint_compiler,
|
blueprint_compiler,
|
||||||
|
|||||||
@@ -27,6 +27,10 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#details list {
|
||||||
|
background: rgb(from var(--card-bg-color) r g b / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
#background {
|
#background {
|
||||||
filter: saturate(300%) opacity(50%);
|
filter: saturate(300%) opacity(50%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="@PREFIX@">
|
<gresource prefix="@PREFIX@">
|
||||||
<file>cover.ui</file>
|
<file>cover.ui</file>
|
||||||
|
<file>game-details.ui</file>
|
||||||
<file>game-item.ui</file>
|
<file>game-item.ui</file>
|
||||||
<file>window.ui</file>
|
<file>window.ui</file>
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Gtk 4.0;
|
using Gtk 4.0;
|
||||||
using Gdk 4.0;
|
|
||||||
using Adw 1;
|
using Adw 1;
|
||||||
|
|
||||||
template $Window: Adw.ApplicationWindow {
|
template $Window: Adw.ApplicationWindow {
|
||||||
@@ -22,28 +21,9 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Adw.Breakpoint {
|
|
||||||
condition ("max-width: 700px")
|
|
||||||
|
|
||||||
setters {
|
|
||||||
details_box.orientation: vertical;
|
|
||||||
name_label.halign: center;
|
|
||||||
name_label.justify: center;
|
|
||||||
developer_label.halign: center;
|
|
||||||
developer_label.justify: center;
|
|
||||||
date_labels.orientation: vertical;
|
|
||||||
date_labels.halign: center;
|
|
||||||
last_played_label.halign: center;
|
|
||||||
last_played_label.justify: center;
|
|
||||||
added_label.halign: center;
|
|
||||||
added_label.justify: center;
|
|
||||||
actions.orientation: vertical;
|
|
||||||
actions.halign: center;
|
|
||||||
actions.spacing: 24;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -142,7 +122,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
child: GridView grid {
|
child: GridView grid {
|
||||||
name: "grid";
|
name: "grid";
|
||||||
single-click-activate: true;
|
single-click-activate: true;
|
||||||
activate => $_activate_game();
|
activate => $_show_details();
|
||||||
|
|
||||||
model: NoSelection {
|
model: NoSelection {
|
||||||
model: SortListModel {
|
model: SortListModel {
|
||||||
@@ -189,6 +169,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
template ListItem {
|
template ListItem {
|
||||||
child: $GameItem {
|
child: $GameItem {
|
||||||
game: bind template.item;
|
game: bind template.item;
|
||||||
|
position: bind template.position;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -228,205 +209,8 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Adw.NavigationPage {
|
$GameDetails details {
|
||||||
name: "details";
|
sort-changed => $_sort_changed();
|
||||||
tag: "details";
|
|
||||||
title: bind template.active-game as <$Game>.name;
|
|
||||||
|
|
||||||
child: Overlay {
|
|
||||||
child: Picture {
|
|
||||||
name: "background";
|
|
||||||
paintable: bind $_downscale_image(template.active-game as <$Game>.cover) as <Gdk.Texture>;
|
|
||||||
content-fit: cover;
|
|
||||||
};
|
|
||||||
|
|
||||||
[overlay]
|
|
||||||
Adw.ToolbarView {
|
|
||||||
[top]
|
|
||||||
Adw.HeaderBar {
|
|
||||||
show-title: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
content: ScrolledWindow {
|
|
||||||
hscrollbar-policy: never;
|
|
||||||
|
|
||||||
child: Box details_box {
|
|
||||||
halign: center;
|
|
||||||
valign: center;
|
|
||||||
margin-top: 12;
|
|
||||||
margin-bottom: 48;
|
|
||||||
margin-start: 24;
|
|
||||||
margin-end: 24;
|
|
||||||
spacing: 36;
|
|
||||||
|
|
||||||
$Cover {
|
|
||||||
paintable: bind template.active-game as <$Game>.cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
|
||||||
orientation: vertical;
|
|
||||||
valign: center;
|
|
||||||
spacing: 6;
|
|
||||||
|
|
||||||
Label name_label {
|
|
||||||
label: bind template.active-game as <$Game>.name;
|
|
||||||
halign: start;
|
|
||||||
max-width-chars: 24;
|
|
||||||
wrap: true;
|
|
||||||
wrap-mode: word_char;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"title-1",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Label developer_label {
|
|
||||||
label: bind template.active-game as <$Game>.developer;
|
|
||||||
visible: bind $_bool(template.active-game as <$Game>.developer) as <bool>;
|
|
||||||
halign: start;
|
|
||||||
max-width-chars: 36;
|
|
||||||
wrap: true;
|
|
||||||
wrap-mode: word_char;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"heading",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Box date_labels {
|
|
||||||
halign: start;
|
|
||||||
valign: start;
|
|
||||||
margin-top: 9;
|
|
||||||
spacing: 9;
|
|
||||||
|
|
||||||
Label last_played_label {
|
|
||||||
label: bind $_date_label(_("Last played: {}"), template.active-game as <$Game>.last_played) as <string>;
|
|
||||||
halign: start;
|
|
||||||
wrap: true;
|
|
||||||
wrap-mode: word_char;
|
|
||||||
}
|
|
||||||
|
|
||||||
Label added_label {
|
|
||||||
label: bind $_date_label(_("Added: {}"), template.active-game as <$Game>.added) as <string>;
|
|
||||||
halign: start;
|
|
||||||
wrap: true;
|
|
||||||
wrap-mode: word_char;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Box actions {
|
|
||||||
spacing: 12;
|
|
||||||
margin-top: 15;
|
|
||||||
|
|
||||||
Button {
|
|
||||||
action-name: "game.play";
|
|
||||||
label: _("Play");
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"pill",
|
|
||||||
"suggested-action",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
|
||||||
spacing: 6;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
Button hide_button {
|
|
||||||
visible: bind hide_button.sensitive;
|
|
||||||
action-name: "game.hide";
|
|
||||||
icon-name: "view-conceal-symbolic";
|
|
||||||
tooltip-text: _("Hide");
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"circular",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Button unhide_button {
|
|
||||||
visible: bind unhide_button.sensitive;
|
|
||||||
action-name: "game.unhide";
|
|
||||||
icon-name: "view-reveal-symbolic";
|
|
||||||
tooltip-text: _("Unhide");
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"circular",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
action-name: "game.remove";
|
|
||||||
icon-name: "user-trash-symbolic";
|
|
||||||
tooltip-text: _("Remove");
|
|
||||||
valign: center;
|
|
||||||
clicked => $_pop();
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"circular",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuButton {
|
|
||||||
icon-name: "edit-find-symbolic";
|
|
||||||
tooltip-text: _("Search On");
|
|
||||||
valign: center;
|
|
||||||
|
|
||||||
menu-model: menu {
|
|
||||||
section {
|
|
||||||
label: _("Search On");
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("HowLongToBeat");
|
|
||||||
action: "win.search-on";
|
|
||||||
target: "https://howlongtobeat.com/?q={}";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("IGDB");
|
|
||||||
action: "win.search-on";
|
|
||||||
target: "https://www.igdb.com/search?type=1&q={}";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("Lutris");
|
|
||||||
action: "win.search-on";
|
|
||||||
target: "https://lutris.net/games?q={}";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("PCGamingWiki");
|
|
||||||
action: "win.search-on";
|
|
||||||
target: "https://www.pcgamingwiki.com/w/index.php?search={}";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("ProtonDB");
|
|
||||||
action: "win.search-on";
|
|
||||||
target: "https://www.protondb.com/search?q={}";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("SteamGridDB");
|
|
||||||
action: "win.search-on";
|
|
||||||
target: "https://www.steamgriddb.com/search/grids?term={}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
styles [
|
|
||||||
"circular",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,15 @@
|
|||||||
|
|
||||||
import locale
|
import locale
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from datetime import UTC, datetime
|
|
||||||
from gettext import gettext as _
|
|
||||||
from typing import Any, TypeVar
|
from typing import Any, TypeVar
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
|
from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import games, state_settings
|
from cartridges import games, state_settings
|
||||||
from cartridges.config import PREFIX, PROFILE
|
from cartridges.config import PREFIX, PROFILE
|
||||||
from cartridges.games import Game
|
from cartridges.games import Game
|
||||||
|
|
||||||
from .cover import Cover # noqa: F401
|
from .game_details import GameDetails
|
||||||
from .game_item import GameItem # noqa: F401
|
from .game_item import GameItem # noqa: F401
|
||||||
|
|
||||||
SORT_MODES = {
|
SORT_MODES = {
|
||||||
@@ -39,6 +36,7 @@ class Window(Adw.ApplicationWindow):
|
|||||||
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
|
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
|
||||||
grid: Gtk.GridView = Gtk.Template.Child()
|
grid: Gtk.GridView = Gtk.Template.Child()
|
||||||
sorter: Gtk.CustomSorter = Gtk.Template.Child()
|
sorter: Gtk.CustomSorter = Gtk.Template.Child()
|
||||||
|
details: GameDetails = Gtk.Template.Child()
|
||||||
|
|
||||||
search_text = GObject.Property(type=str)
|
search_text = GObject.Property(type=str)
|
||||||
show_hidden = GObject.Property(type=bool, default=False)
|
show_hidden = GObject.Property(type=bool, default=False)
|
||||||
@@ -48,16 +46,6 @@ class Window(Adw.ApplicationWindow):
|
|||||||
"""Model of the user's games."""
|
"""Model of the user's games."""
|
||||||
return games.model
|
return games.model
|
||||||
|
|
||||||
@GObject.Property(type=Game)
|
|
||||||
def active_game(self) -> Game | None:
|
|
||||||
"""The game whose details to show."""
|
|
||||||
return self._active_game
|
|
||||||
|
|
||||||
@active_game.setter
|
|
||||||
def active_game(self, active_game: Game | None):
|
|
||||||
self._active_game = active_game
|
|
||||||
self.insert_action_group("game", active_game)
|
|
||||||
|
|
||||||
def __init__(self, **kwargs: Any):
|
def __init__(self, **kwargs: Any):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
@@ -84,13 +72,7 @@ class Window(Adw.ApplicationWindow):
|
|||||||
"s",
|
"s",
|
||||||
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"),
|
||||||
"search-on",
|
|
||||||
lambda _action, param, *_: Gio.AppInfo.launch_default_for_uri(
|
|
||||||
param.get_string().format(quote(self.active_game.name))
|
|
||||||
),
|
|
||||||
"s",
|
|
||||||
),
|
|
||||||
))
|
))
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
@@ -98,54 +80,11 @@ class Window(Adw.ApplicationWindow):
|
|||||||
return first if condition else second
|
return first if condition else second
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _activate_game(self, grid: Gtk.GridView, position: int):
|
def _show_details(self, grid: Gtk.GridView, position: int):
|
||||||
if isinstance(model := grid.props.model, Gio.ListModel):
|
if isinstance(model := grid.props.model, Gio.ListModel):
|
||||||
self.active_game = model.get_item(position)
|
self.details.game = model.get_item(position)
|
||||||
self.navigation_view.push_by_tag("details")
|
self.navigation_view.push_by_tag("details")
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _downscale_image(self, _obj, cover: Gdk.Texture | None) -> Gdk.Texture | None:
|
|
||||||
if cover and (renderer := self.get_renderer()):
|
|
||||||
cover.snapshot(snapshot := Gtk.Snapshot.new(), 3, 3)
|
|
||||||
if node := snapshot.to_node():
|
|
||||||
return renderer.render_texture(node)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _date_label(self, _obj, label: str, timestamp: int) -> str:
|
|
||||||
date = datetime.fromtimestamp(timestamp, UTC)
|
|
||||||
now = datetime.now(UTC)
|
|
||||||
return label.format(
|
|
||||||
_("Never")
|
|
||||||
if not timestamp
|
|
||||||
else _("Today")
|
|
||||||
if (n_days := (now - date).days) == 0
|
|
||||||
else _("Yesterday")
|
|
||||||
if n_days == 1
|
|
||||||
else date.strftime("%A")
|
|
||||||
if n_days <= (day_of_week := now.weekday())
|
|
||||||
else _("Last Week")
|
|
||||||
if n_days <= day_of_week + 7
|
|
||||||
else _("This Month")
|
|
||||||
if n_days <= (day_of_month := now.day)
|
|
||||||
else _("Last Month")
|
|
||||||
if n_days <= day_of_month + 30
|
|
||||||
else date.strftime("%B")
|
|
||||||
if n_days < (day_of_year := now.timetuple().tm_yday)
|
|
||||||
else _("Last Year")
|
|
||||||
if n_days <= day_of_year + 365
|
|
||||||
else date.strftime("%Y")
|
|
||||||
)
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _bool(self, _obj, o: object) -> bool:
|
|
||||||
return bool(o)
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
|
||||||
def _pop(self, _obj):
|
|
||||||
self.navigation_view.pop()
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _search_started(self, entry: Gtk.SearchEntry):
|
def _search_started(self, entry: Gtk.SearchEntry):
|
||||||
entry.grab_focus()
|
entry.grab_focus()
|
||||||
@@ -192,3 +131,20 @@ class Window(Adw.ApplicationWindow):
|
|||||||
def _sortable(*strings: str) -> Generator[str]:
|
def _sortable(*strings: str) -> Generator[str]:
|
||||||
for string in strings:
|
for string in strings:
|
||||||
yield string.lower().removeprefix("the ")
|
yield string.lower().removeprefix("the ")
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _sort_changed(self, *_args):
|
||||||
|
self.sorter.changed(Gtk.SorterChange.DIFFERENT)
|
||||||
|
|
||||||
|
def _edit(self, pos: int):
|
||||||
|
if isinstance(self.grid.props.model, Gio.ListModel) and (
|
||||||
|
game := self.grid.props.model.get_item(pos)
|
||||||
|
):
|
||||||
|
self.details.game = game
|
||||||
|
|
||||||
|
self.navigation_view.push_by_tag("details")
|
||||||
|
self.details.edit()
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _edit_done(self, *_args):
|
||||||
|
self.details.edit_done()
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ cartridges/application.py
|
|||||||
cartridges/games.py
|
cartridges/games.py
|
||||||
cartridges/ui/cover.blp
|
cartridges/ui/cover.blp
|
||||||
cartridges/ui/cover.py
|
cartridges/ui/cover.py
|
||||||
|
cartridges/ui/game-details.blp
|
||||||
|
cartridges/ui/game_details.py
|
||||||
cartridges/ui/game-item.blp
|
cartridges/ui/game-item.blp
|
||||||
cartridges/ui/game_item.py
|
cartridges/ui/game_item.py
|
||||||
cartridges/ui/window.blp
|
cartridges/ui/window.blp
|
||||||
|
|||||||
Reference in New Issue
Block a user