window: Add details page
This commit is contained in:
28
cartridges/ui/cover.blp
Normal file
28
cartridges/ui/cover.blp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Gtk 4.0;
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $Cover: Adw.Bin {
|
||||||
|
child: Adw.Clamp {
|
||||||
|
orientation: vertical;
|
||||||
|
unit: px;
|
||||||
|
maximum-size: bind picture.height-request;
|
||||||
|
tightening-threshold: bind picture.height-request;
|
||||||
|
|
||||||
|
Adw.Clamp {
|
||||||
|
unit: px;
|
||||||
|
maximum-size: bind picture.width-request;
|
||||||
|
tightening-threshold: bind picture.width-request;
|
||||||
|
|
||||||
|
Picture picture {
|
||||||
|
paintable: bind template.paintable;
|
||||||
|
width-request: 200;
|
||||||
|
height-request: 300;
|
||||||
|
content-fit: cover;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"card",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
15
cartridges/ui/cover.py
Normal file
15
cartridges/ui/cover.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
||||||
|
|
||||||
|
from gi.repository import Adw, Gdk, GObject, Gtk
|
||||||
|
|
||||||
|
from cartridges.config import PREFIX
|
||||||
|
|
||||||
|
|
||||||
|
@Gtk.Template.from_resource(f"{PREFIX}/cover.ui")
|
||||||
|
class Cover(Adw.Bin):
|
||||||
|
"""Displays a game's cover art."""
|
||||||
|
|
||||||
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
|
paintable = GObject.Property(type=Gdk.Paintable)
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
python.install_sources(
|
python.install_sources(
|
||||||
files('__init__.py', 'window.py'),
|
files('__init__.py', 'cover.py', 'window.py'),
|
||||||
subdir: 'cartridges' / 'ui',
|
subdir: 'cartridges' / 'ui',
|
||||||
)
|
)
|
||||||
|
|
||||||
blueprints = custom_target(
|
blueprints = custom_target(
|
||||||
input: files('window.blp'),
|
input: files('cover.blp', 'window.blp'),
|
||||||
output: '.',
|
output: '.',
|
||||||
command: [
|
command: [
|
||||||
blueprint_compiler,
|
blueprint_compiler,
|
||||||
|
|||||||
@@ -10,3 +10,13 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#background {
|
||||||
|
filter: saturate(300%) opacity(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
#background {
|
||||||
|
filter: saturate(300%) opacity(0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<gresources>
|
<gresources>
|
||||||
<gresource prefix="@PREFIX@">
|
<gresource prefix="@PREFIX@">
|
||||||
|
<file>cover.ui</file>
|
||||||
<file>window.ui</file>
|
<file>window.ui</file>
|
||||||
<file>style.css</file>
|
<file>style.css</file>
|
||||||
</gresource>
|
</gresource>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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 {
|
||||||
@@ -23,155 +24,267 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content: Adw.ToolbarView {
|
Adw.Breakpoint {
|
||||||
[top]
|
condition ("max-width: 700px")
|
||||||
Adw.HeaderBar {
|
|
||||||
title-widget: Adw.Clamp clamp {
|
|
||||||
tightening-threshold: bind clamp.maximum-size;
|
|
||||||
|
|
||||||
child: CenterBox {
|
setters {
|
||||||
hexpand: true;
|
details_box.orientation: vertical;
|
||||||
|
name_label.halign: center;
|
||||||
center-widget: SearchEntry search_entry {
|
name_label.justify: center;
|
||||||
hexpand: true;
|
developer_label.halign: center;
|
||||||
placeholder-text: _("Search games");
|
developer_label.justify: center;
|
||||||
search-started => $_search_started();
|
date_labels.orientation: vertical;
|
||||||
search-changed => $_search_changed();
|
date_labels.halign: center;
|
||||||
stop-search => $_stop_search();
|
last_played_label.halign: center;
|
||||||
};
|
last_played_label.justify: center;
|
||||||
|
added_label.halign: center;
|
||||||
end-widget: MenuButton {
|
added_label.justify: center;
|
||||||
icon-name: "filter-symbolic";
|
|
||||||
tooltip-text: _("Sort & Filter");
|
|
||||||
margin-start: 6;
|
|
||||||
|
|
||||||
menu-model: menu {
|
|
||||||
section {
|
|
||||||
label: _("Sort");
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("Last Played");
|
|
||||||
action: "win.sort";
|
|
||||||
target: "last_played";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("A-Z");
|
|
||||||
action: "win.sort";
|
|
||||||
target: "a-z";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("Z-A");
|
|
||||||
action: "win.sort";
|
|
||||||
target: "z-a";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("Newest");
|
|
||||||
action: "win.sort";
|
|
||||||
target: "newest";
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
label: _("Oldest");
|
|
||||||
action: "win.sort";
|
|
||||||
target: "oldest";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
item (_("Show Hidden Games"), "win.show-hidden")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
[end]
|
|
||||||
MenuButton {
|
|
||||||
icon-name: "open-menu-symbolic";
|
|
||||||
tooltip-text: _("Main Menu");
|
|
||||||
primary: true;
|
|
||||||
|
|
||||||
menu-model: menu {
|
|
||||||
item (_("About Cartridges"), "app.about")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
content: ScrolledWindow {
|
content: Adw.NavigationView navigation_view {
|
||||||
child: GridView grid {
|
Adw.NavigationPage {
|
||||||
name: "grid";
|
title: bind template.title;
|
||||||
|
|
||||||
model: NoSelection {
|
child: Adw.ToolbarView {
|
||||||
model: SortListModel {
|
[top]
|
||||||
sorter: CustomSorter sorter {};
|
Adw.HeaderBar {
|
||||||
|
title-widget: Adw.Clamp clamp {
|
||||||
|
tightening-threshold: bind clamp.maximum-size;
|
||||||
|
|
||||||
model: FilterListModel {
|
child: CenterBox {
|
||||||
filter: EveryFilter {
|
hexpand: true;
|
||||||
AnyFilter {
|
|
||||||
StringFilter {
|
|
||||||
expression: expr item as <$Game>.name;
|
|
||||||
search: bind template.search-text;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringFilter {
|
center-widget: SearchEntry search_entry {
|
||||||
expression: expr item as <$Game>.developer;
|
hexpand: true;
|
||||||
search: bind template.search-text;
|
placeholder-text: _("Search games");
|
||||||
}
|
search-started => $_search_started();
|
||||||
}
|
search-changed => $_search_changed();
|
||||||
|
stop-search => $_stop_search();
|
||||||
BoolFilter {
|
|
||||||
expression: expr item as <$Game>.hidden;
|
|
||||||
invert: bind template.show-hidden inverted;
|
|
||||||
}
|
|
||||||
|
|
||||||
BoolFilter {
|
|
||||||
expression: expr item as <$Game>.removed;
|
|
||||||
invert: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
BoolFilter {
|
|
||||||
expression: expr item as <$Game>.blacklisted;
|
|
||||||
invert: true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
model: bind template.games;
|
end-widget: MenuButton {
|
||||||
|
icon-name: "filter-symbolic";
|
||||||
|
tooltip-text: _("Sort & Filter");
|
||||||
|
margin-start: 6;
|
||||||
|
|
||||||
|
menu-model: menu {
|
||||||
|
section {
|
||||||
|
label: _("Sort");
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("Last Played");
|
||||||
|
action: "win.sort";
|
||||||
|
target: "last_played";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("A-Z");
|
||||||
|
action: "win.sort";
|
||||||
|
target: "a-z";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("Z-A");
|
||||||
|
action: "win.sort";
|
||||||
|
target: "z-a";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("Newest");
|
||||||
|
action: "win.sort";
|
||||||
|
target: "newest";
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("Oldest");
|
||||||
|
action: "win.sort";
|
||||||
|
target: "oldest";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
item (_("Show Hidden Games"), "win.show-hidden")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
[end]
|
||||||
|
MenuButton {
|
||||||
|
icon-name: "open-menu-symbolic";
|
||||||
|
tooltip-text: _("Main Menu");
|
||||||
|
primary: true;
|
||||||
|
|
||||||
|
menu-model: menu {
|
||||||
|
item (_("About Cartridges"), "app.about")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content: ScrolledWindow {
|
||||||
|
hscrollbar-policy: never;
|
||||||
|
|
||||||
|
child: GridView grid {
|
||||||
|
name: "grid";
|
||||||
|
single-click-activate: true;
|
||||||
|
activate => $_activate_game();
|
||||||
|
|
||||||
|
model: NoSelection {
|
||||||
|
model: SortListModel {
|
||||||
|
sorter: CustomSorter sorter {};
|
||||||
|
|
||||||
|
model: FilterListModel {
|
||||||
|
filter: EveryFilter {
|
||||||
|
AnyFilter {
|
||||||
|
StringFilter {
|
||||||
|
expression: expr item as <$Game>.name;
|
||||||
|
search: bind template.search-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringFilter {
|
||||||
|
expression: expr item as <$Game>.developer;
|
||||||
|
search: bind template.search-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BoolFilter {
|
||||||
|
expression: expr item as <$Game>.hidden;
|
||||||
|
invert: bind template.show-hidden inverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
BoolFilter {
|
||||||
|
expression: expr item as <$Game>.removed;
|
||||||
|
invert: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BoolFilter {
|
||||||
|
expression: expr item as <$Game>.blacklisted;
|
||||||
|
invert: true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
model: bind template.games;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
factory: BuilderListItemFactory {
|
||||||
|
template ListItem {
|
||||||
|
child: Box {
|
||||||
|
orientation: vertical;
|
||||||
|
spacing: 12;
|
||||||
|
|
||||||
|
$Cover {
|
||||||
|
paintable: bind template.item as <$Game>.cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
label: bind template.item as <$Game>.name;
|
||||||
|
ellipsize: middle;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
factory: BuilderListItemFactory {
|
styles [
|
||||||
template ListItem {
|
"view",
|
||||||
child: Box {
|
]
|
||||||
orientation: vertical;
|
};
|
||||||
spacing: 12;
|
}
|
||||||
|
|
||||||
Picture {
|
Adw.NavigationPage {
|
||||||
paintable: bind template.item as <$Game>.cover;
|
title: bind template.active-game as <$Game>.name;
|
||||||
width-request: 200;
|
tag: "details";
|
||||||
height-request: 300;
|
|
||||||
halign: center;
|
|
||||||
|
|
||||||
styles [
|
child: Overlay {
|
||||||
"card",
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Box {
|
||||||
label: bind template.item as <$Game>.name;
|
orientation: vertical;
|
||||||
ellipsize: middle;
|
valign: center;
|
||||||
|
spacing: 3;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
styles [
|
|
||||||
"view",
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed
|
# SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
|
||||||
|
|
||||||
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
|
from typing import Any
|
||||||
|
|
||||||
from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import games
|
from cartridges import games
|
||||||
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
|
||||||
|
|
||||||
SORT_MODES = {
|
SORT_MODES = {
|
||||||
"last_played": ("last-played", True),
|
"last_played": ("last-played", True),
|
||||||
"a-z": ("name", False),
|
"a-z": ("name", False),
|
||||||
@@ -27,10 +31,12 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
__gtype_name__ = __qualname__
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
|
navigation_view: Adw.NavigationView = Gtk.Template.Child()
|
||||||
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()
|
||||||
|
|
||||||
|
active_game = GObject.Property(type=Game)
|
||||||
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)
|
||||||
|
|
||||||
@@ -58,6 +64,51 @@ class Window(Adw.ApplicationWindow):
|
|||||||
("sort", self._sort, "s", "'last_played'"),
|
("sort", self._sort, "s", "'last_played'"),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _activate_game(self, grid: Gtk.GridView, position: int):
|
||||||
|
if isinstance(model := grid.props.model, Gio.ListModel):
|
||||||
|
self.active_game = model.get_item(position)
|
||||||
|
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()
|
@Gtk.Template.Callback()
|
||||||
def _search_started(self, entry: Gtk.SearchEntry):
|
def _search_started(self, entry: Gtk.SearchEntry):
|
||||||
entry.grab_focus()
|
entry.grab_focus()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
cartridges/application.py
|
cartridges/application.py
|
||||||
cartridges/games.py
|
cartridges/games.py
|
||||||
|
cartridges/ui/cover.blp
|
||||||
|
cartridges/ui/cover.py
|
||||||
cartridges/ui/window.blp
|
cartridges/ui/window.blp
|
||||||
cartridges/ui/window.py
|
cartridges/ui/window.py
|
||||||
data/page.kramo.Cartridges.desktop.in
|
data/page.kramo.Cartridges.desktop.in
|
||||||
|
|||||||
Reference in New Issue
Block a user