window: Add details page

This commit is contained in:
kramo
2025-11-30 17:29:15 +01:00
committed by Laura Kramolis
parent d627748b53
commit 52a01f9225
8 changed files with 355 additions and 135 deletions

28
cartridges/ui/cover.blp Normal file
View 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
View 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)

View File

@@ -1,10 +1,10 @@
python.install_sources(
files('__init__.py', 'window.py'),
files('__init__.py', 'cover.py', 'window.py'),
subdir: 'cartridges' / 'ui',
)
blueprints = custom_target(
input: files('window.blp'),
input: files('cover.blp', 'window.blp'),
output: '.',
command: [
blueprint_compiler,

View File

@@ -10,3 +10,13 @@
padding: 12px;
border-radius: 24px;
}
#background {
filter: saturate(300%) opacity(0.5);
}
@media (prefers-contrast: more) {
#background {
filter: saturate(300%) opacity(0.2);
}
}

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<gresources>
<gresource prefix="@PREFIX@">
<file>cover.ui</file>
<file>window.ui</file>
<file>style.css</file>
</gresource>

View File

@@ -1,4 +1,5 @@
using Gtk 4.0;
using Gdk 4.0;
using Adw 1;
template $Window: Adw.ApplicationWindow {
@@ -23,155 +24,267 @@ template $Window: Adw.ApplicationWindow {
}
}
content: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.Clamp clamp {
tightening-threshold: bind clamp.maximum-size;
Adw.Breakpoint {
condition ("max-width: 700px")
child: CenterBox {
hexpand: true;
center-widget: SearchEntry search_entry {
hexpand: true;
placeholder-text: _("Search games");
search-started => $_search_started();
search-changed => $_search_changed();
stop-search => $_stop_search();
};
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")
};
}
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;
}
}
content: ScrolledWindow {
child: GridView grid {
name: "grid";
content: Adw.NavigationView navigation_view {
Adw.NavigationPage {
title: bind template.title;
model: NoSelection {
model: SortListModel {
sorter: CustomSorter sorter {};
child: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.Clamp clamp {
tightening-threshold: bind clamp.maximum-size;
model: FilterListModel {
filter: EveryFilter {
AnyFilter {
StringFilter {
expression: expr item as <$Game>.name;
search: bind template.search-text;
}
child: CenterBox {
hexpand: true;
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;
}
center-widget: SearchEntry search_entry {
hexpand: true;
placeholder-text: _("Search games");
search-started => $_search_started();
search-changed => $_search_changed();
stop-search => $_stop_search();
};
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 {
template ListItem {
child: Box {
orientation: vertical;
spacing: 12;
styles [
"view",
]
};
}
Picture {
paintable: bind template.item as <$Game>.cover;
width-request: 200;
height-request: 300;
halign: center;
Adw.NavigationPage {
title: bind template.active-game as <$Game>.name;
tag: "details";
styles [
"card",
]
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;
}
Label {
label: bind template.item as <$Game>.name;
ellipsize: middle;
Box {
orientation: vertical;
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",
]
}
};
}

View File

@@ -1,17 +1,21 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed
# SPDX-FileCopyrightText: Copyright 2025 kramo
# SPDX-FileCopyrightText: Copyright 2022-2025 kramo
import locale
from collections.abc import Generator
from datetime import UTC, datetime
from gettext import gettext as _
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.config import PREFIX, PROFILE
from cartridges.games import Game
from .cover import Cover # noqa: F401
SORT_MODES = {
"last_played": ("last-played", True),
"a-z": ("name", False),
@@ -27,10 +31,12 @@ class Window(Adw.ApplicationWindow):
__gtype_name__ = __qualname__
navigation_view: Adw.NavigationView = Gtk.Template.Child()
search_entry: Gtk.SearchEntry = Gtk.Template.Child()
grid: Gtk.GridView = Gtk.Template.Child()
sorter: Gtk.CustomSorter = Gtk.Template.Child()
active_game = GObject.Property(type=Game)
search_text = GObject.Property(type=str)
show_hidden = GObject.Property(type=bool, default=False)
@@ -58,6 +64,51 @@ class Window(Adw.ApplicationWindow):
("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()
def _search_started(self, entry: Gtk.SearchEntry):
entry.grab_focus()

View File

@@ -1,5 +1,7 @@
cartridges/application.py
cartridges/games.py
cartridges/ui/cover.blp
cartridges/ui/cover.py
cartridges/ui/window.blp
cartridges/ui/window.py
data/page.kramo.Cartridges.desktop.in