collections: Add initial UI

This commit is contained in:
Jamie Gravendeel
2025-12-19 17:25:11 +01:00
parent e645ade8d6
commit d0b6d6457d
9 changed files with 343 additions and 162 deletions

View File

@@ -0,0 +1,53 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from typing import Any, override
from gi.repository import Adw, GObject, Gtk
from cartridges.collections import Collection
from cartridges.games import Game
class CollectionFilter(Gtk.Filter):
"""Filter games based on a selected collection."""
__gtype_name__ = __qualname__
@GObject.Property(type=Collection)
def collection(self) -> Collection | None:
"""The collection used for filtering."""
return self._collection
@collection.setter
def collection(self, collection: Collection | None):
self._collection = collection
self.changed(Gtk.FilterChange.DIFFERENT)
@override
def do_match(self, game: Game) -> bool: # pyright: ignore[reportIncompatibleMethodOverride]
if not self.collection:
return True
return game.game_id in self.collection.game_ids
class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
"""A sidebar item representing a collection."""
@GObject.Property(type=Collection)
def collection(self) -> Collection:
"""The collection that `self` represents."""
return self._collection
@collection.setter
def collection(self, collection: Collection):
self._collection = collection
flags = GObject.BindingFlags.SYNC_CREATE
collection.bind_property("name", self, "title", flags)
collection.bind_property("icon-name", self, "icon-name", flags)
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
self.bind_property("title", self, "tooltip", GObject.BindingFlags.SYNC_CREATE)

View File

@@ -1,6 +1,7 @@
python.install_sources(
files(
'__init__.py',
'collections.py',
'cover.py',
'game_details.py',
'game_item.py',

View File

@@ -10,6 +10,11 @@ Adw.ShortcutsDialog shortcuts_dialog {
accelerator: "<Control>f";
}
Adw.ShortcutsItem {
title: _("Toggle Sidebar");
accelerator: "F9";
}
Adw.ShortcutsItem {
title: _("Main Menu");
accelerator: "F10";

View File

@@ -26,12 +26,25 @@ template $Window: Adw.ApplicationWindow {
action: "action(win.undo)";
}
Shortcut {
trigger: "F9";
action: "action(win.show-sidebar)";
}
Shortcut {
trigger: "<Control>w";
action: "action(window.close)";
}
}
Adw.Breakpoint {
condition ("max-width: 730px") // 3 columns + 48px of inherent padding
setters {
split_view.collapsed: true;
}
}
content: Adw.NavigationView navigation_view {
Adw.NavigationPage {
title: bind template.title;
@@ -41,6 +54,48 @@ template $Window: Adw.ApplicationWindow {
"view",
]
child: Adw.OverlaySplitView split_view {
sidebar-width-unit: px;
min-sidebar-width: 224; // Width of 1 column
max-sidebar-width: bind split_view.min-sidebar-width;
sidebar: Adw.NavigationPage {
title: bind template.title;
child: Adw.ToolbarView {
[top]
Adw.HeaderBar {
show-title: bind $_show_sidebar_title(template.settings as <Settings>.gtk-decoration-layout) as <bool>;
[start]
Button {
visible: bind split_view.show-sidebar;
action-name: "win.show-sidebar";
icon-name: "sidebar-show-symbolic";
tooltip-text: _("Toggle Sidebar");
}
}
content: Adw.Sidebar sidebar {
activated => $_navigate();
Adw.SidebarSection {
Adw.SidebarItem {
icon-name: "view-grid-symbolic";
title: _("All Games");
}
}
Adw.SidebarSection collections {
title: _("Collections");
}
};
};
};
content: Adw.NavigationPage {
title: _("Games");
child: Adw.ToolbarView {
[top]
Adw.HeaderBar header_bar {
@@ -107,6 +162,14 @@ template $Window: Adw.ApplicationWindow {
};
};
[start]
Button {
visible: bind split_view.show-sidebar inverted;
action-name: "win.show-sidebar";
icon-name: "sidebar-show-symbolic";
tooltip-text: _("Toggle Sidebar");
}
[start]
Button {
icon-name: "list-add-symbolic";
@@ -136,7 +199,11 @@ template $Window: Adw.ApplicationWindow {
$_if_else(
template.search-text,
"empty-search",
$_if_else(template.show-hidden, "empty-hidden", "empty") as <string>
$_if_else(
template.show-hidden,
"empty-hidden",
$_if_else(template.collection, "empty-collection", "empty") as <string>
) as <string>
) as <string>
) as <string>;
@@ -159,6 +226,10 @@ template $Window: Adw.ApplicationWindow {
watch-items: true;
filter: EveryFilter {
$CollectionFilter {
collection: bind template.collection;
}
AnyFilter {
StringFilter {
expression: expr item as <$Game>.name;
@@ -224,6 +295,15 @@ template $Window: Adw.ApplicationWindow {
};
}
Adw.ViewStackPage {
name: "empty-collection";
child: Adw.StatusPage {
icon-name: bind template.collection as <$Collection>.icon-name;
title: bind $_format(_("No Games in {}"), template.collection as <$Collection>.name) as <string>;
};
}
Adw.ViewStackPage {
name: "empty";
@@ -236,6 +316,8 @@ template $Window: Adw.ApplicationWindow {
};
};
};
};
};
}
$GameDetails details {

View File

@@ -10,10 +10,15 @@ from typing import Any, TypeVar, cast
from gi.repository import Adw, Gio, GLib, GObject, Gtk
from cartridges import games, state_settings
from cartridges import collections, games, state_settings
from cartridges.collections import Collection
from cartridges.config import PREFIX, PROFILE
from cartridges.games import Game
from .collections import (
CollectionFilter, # noqa: F401
CollectionSidebarItem,
)
from .game_details import GameDetails
from .game_item import GameItem # noqa: F401
from .games import GameSorter
@@ -40,6 +45,8 @@ class Window(Adw.ApplicationWindow):
__gtype_name__ = __qualname__
split_view: Adw.OverlaySplitView = Gtk.Template.Child()
collections: Adw.SidebarSection = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
navigation_view: Adw.NavigationView = Gtk.Template.Child()
header_bar: Adw.HeaderBar = Gtk.Template.Child()
title_box: Gtk.CenterBox = Gtk.Template.Child()
@@ -53,6 +60,9 @@ class Window(Adw.ApplicationWindow):
search_text = GObject.Property(type=str)
show_hidden = GObject.Property(type=bool, default=False)
collection = GObject.Property(type=Collection)
settings = GObject.Property(type=Gtk.Settings)
@GObject.Property(type=Gio.ListStore)
def games(self) -> Gio.ListStore:
@@ -65,14 +75,22 @@ class Window(Adw.ApplicationWindow):
if PROFILE == "development":
self.add_css_class("devel")
self.settings = self.get_settings()
flags = Gio.SettingsBindFlags.DEFAULT
state_settings.bind("width", self, "default-width", flags)
state_settings.bind("height", self, "default-height", flags)
state_settings.bind("is-maximized", self, "maximized", flags)
state_settings.bind("show-sidebar", self.split_view, "show-sidebar", flags)
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
self.search_entry.set_key_capture_widget(self)
self.collections.bind_model(
collections.model,
lambda collection: CollectionSidebarItem(collection=collection),
)
self.add_action(state_settings.create_action("show-sidebar"))
self.add_action(state_settings.create_action("sort-mode"))
self.add_action(Gio.PropertyAction.new("show-hidden", self, "show-hidden"))
self.add_action_entries((
@@ -102,6 +120,18 @@ class Window(Adw.ApplicationWindow):
self.toast_overlay.add_toast(toast)
@Gtk.Template.Callback()
def _show_sidebar_title(self, _obj, layout: str) -> bool:
right_window_controls = layout.replace("appmenu", "").startswith(":")
return right_window_controls and not sys.platform.startswith("darwin")
@Gtk.Template.Callback()
def _navigate(self, sidebar: Adw.Sidebar, index: int): # pyright: ignore[reportAttributeAccessIssue]
item = sidebar.get_item(index)
self.collection = (
item.collection if isinstance(item, CollectionSidebarItem) else None
)
@Gtk.Template.Callback()
def _setup_gamepad_monitor(self, *_args):
if sys.platform.startswith("linux"):
@@ -112,6 +142,10 @@ class Window(Adw.ApplicationWindow):
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T:
return first if condition else second
@Gtk.Template.Callback()
def _format(self, _obj, string: str, *args: Any) -> str:
return string.format(*args)
@Gtk.Template.Callback()
def _show_details(self, grid: Gtk.GridView, position: int):
self.details.game = cast(Gio.ListModel, grid.props.model).get_item(position)

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<gresources>
<gresource prefix="@PREFIX@/icons/scalable/actions">
<file>collection-symbolic.svg</file>
<file>filter-symbolic.svg</file>
</gresource>
</gresources>

View File

@@ -25,5 +25,8 @@
</choices>
<default>"last_played"</default>
</key>
<key name="show-sidebar" type="b">
<default>false</default>
</key>
</schema>
</schemalist>

View File

@@ -4,6 +4,7 @@ cartridges/gamepads.py
cartridges/games.py
cartridges/sources/__init__.py
cartridges/sources/steam.py
cartridges/ui/collections.py
cartridges/ui/cover.blp
cartridges/ui/cover.py
cartridges/ui/game-details.blp

View File

@@ -22,6 +22,7 @@ reportUnknownLambdaType = "none"
reportUnknownMemberType = "none"
reportUnknownParameterType = "none"
reportUnknownVariableType = "none"
reportUntypedBaseClass = "none"
reportUntypedFunctionDecorator = "none"
reportUnusedImport = "none"
reportUnusedVariable = "none"