collection-details: Support adding collections

This commit is contained in:
Jamie Gravendeel
2025-12-22 14:24:39 +01:00
parent 26a09c6782
commit 62004753b6
33 changed files with 263 additions and 5 deletions

View File

@@ -21,6 +21,11 @@ class Collection(GObject.Object):
icon_name = GObject.Property(type=str)
@GObject.Property(type=bool, default=True)
def in_model(self) -> bool:
"""Whether `self` has been added to the model."""
return self in model
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
@@ -54,6 +59,9 @@ def load():
model.splice(0, 0, tuple(_get_collections()))
save()
for collection in model:
collection.notify("in-model")
def save():
"""Save collections to GSettings."""

View File

@@ -0,0 +1,58 @@
using Gtk 4.0;
using Adw 1;
template $CollectionDetails: Adw.Dialog {
title: _("New Collection");
content-width: 360;
default-widget: apply_button;
focus-widget: name_entry;
child: Adw.ToolbarView {
[top]
Adw.HeaderBar {
show-start-title-buttons: false;
show-end-title-buttons: false;
[start]
Button {
action-name: "window.close";
icon-name: "cancel-symbolic";
tooltip-text: _("Cancel");
}
[end]
Button apply_button {
action-name: "details.apply";
icon-name: "apply-symbolic";
tooltip-text: _("Add");
styles [
"suggested-action",
]
}
}
content: Adw.PreferencesPage {
Adw.PreferencesGroup {
Adw.EntryRow name_entry {
title: _("Name");
activates-default: true;
}
}
Adw.PreferencesGroup {
FlowBox icons_box {
min-children-per-line: 7;
styles [
"navigation-sidebar",
]
}
styles [
"card",
]
}
};
};
}

View File

@@ -0,0 +1,90 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from typing import Any, cast
from gi.repository import Adw, Gio, GObject, Gtk
from cartridges import collections
from cartridges.collections import Collection
from cartridges.config import PREFIX
ICONS = (
"collection",
"star",
"heart",
"music",
"people",
"skull",
"private",
"globe",
"map",
"city",
"car",
"horse",
"sprout",
"step-over",
"gamepad",
"ball",
"puzzle",
"flashlight",
"knife",
"gun",
"fist",
)
@Gtk.Template.from_resource(f"{PREFIX}/collection-details.ui")
class CollectionDetails(Adw.Dialog):
"""The details of a category."""
__gtype_name__ = __qualname__
name_entry: Adw.EntryRow = Gtk.Template.Child()
icons_box: Gtk.FlowBox = Gtk.Template.Child()
collection = GObject.Property(type=Collection)
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
self.insert_action_group("details", group := Gio.SimpleActionGroup())
group.add_action(apply := Gio.SimpleAction.new("apply"))
apply.connect("activate", lambda *_: self._apply())
self.name_entry.bind_property(
"text",
apply,
"enabled",
GObject.BindingFlags.SYNC_CREATE,
transform_to=lambda _, text: bool(text),
)
icons = Gtk.StringList.new(tuple(f"{icon}-symbolic" for icon in ICONS))
self.icons_box.bind_model(
icons,
lambda string: Gtk.FlowBoxChild(
name="collection-icon-child",
child=Gtk.Image.new_from_icon_name(string.props.string),
halign=Gtk.Align.CENTER,
),
)
self.icons_box.select_child(
cast(
Gtk.FlowBoxChild,
self.icons_box.get_child_at_index(ICONS.index(self.collection.icon)),
)
)
def _apply(self):
self.collection.name = self.name_entry.props.text
self.collection.icon = ICONS[
self.icons_box.get_selected_children()[0].get_index()
]
if not self.collection.in_model:
collections.model.append(self.collection)
self.collection.notify("in-model")
collections.save()
self.close()

View File

@@ -5,6 +5,7 @@ from typing import Any, override
from gi.repository import Adw, GObject, Gtk
from cartridges import collections
from cartridges.collections import Collection
from cartridges.games import Game
@@ -51,3 +52,17 @@ class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttribute
super().__init__(**kwargs)
self.bind_property("title", self, "tooltip", GObject.BindingFlags.SYNC_CREATE)
sorter = Gtk.StringSorter.new(Gtk.PropertyExpression.new(Collection, None, "name"))
model = Gtk.SortListModel.new(
Gtk.FilterListModel(
model=collections.model,
filter=Gtk.BoolFilter(
expression=Gtk.PropertyExpression.new(Collection, None, "removed"),
invert=True,
),
watch_items=True, # pyright: ignore[reportCallIssue]
),
sorter,
)

View File

@@ -1,6 +1,7 @@
python.install_sources(
files(
'__init__.py',
'collection_details.py',
'collections.py',
'cover.py',
'game_details.py',
@@ -13,6 +14,7 @@ python.install_sources(
blueprints = custom_target(
input: files(
'collection-details.blp',
'cover.blp',
'game-details.blp',
'game-item.blp',

View File

@@ -63,6 +63,10 @@
filter: saturate(300%) opacity(50%);
}
#collection-icon-child {
border-radius: 9999px;
}
@media (prefers-color-scheme: dark) {
#details list {
background: rgb(from white r g b / 10%);

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<gresources>
<gresource prefix="@PREFIX@">
<file>collection-details.ui</file>
<file>cover.ui</file>
<file>game-details.ui</file>
<file>game-item.ui</file>

View File

@@ -78,6 +78,8 @@ template $Window: Adw.ApplicationWindow {
content: Adw.Sidebar sidebar {
activated => $_navigate();
setup-menu => $_setup_sidebar_menu();
notify::selected => $_update_selection();
Adw.SidebarSection {
Adw.SidebarItem {
@@ -89,6 +91,13 @@ template $Window: Adw.ApplicationWindow {
Adw.SidebarSection collections {
title: _("Collections");
}
Adw.SidebarSection {
Adw.SidebarItem new_collection_item {
icon-name: "list-add-symbolic";
title: _("New Collection");
}
}
};
};
};

View File

@@ -10,11 +10,13 @@ from typing import Any, TypeVar, cast
from gi.repository import Adw, Gio, GLib, GObject, Gtk
from cartridges import STATE_SETTINGS, collections, games
from cartridges import STATE_SETTINGS, games
from cartridges.collections import Collection
from cartridges.config import PREFIX, PROFILE
from cartridges.games import Game
from cartridges.ui import collections
from .collection_details import CollectionDetails
from .collections import (
CollectionFilter, # noqa: F401
CollectionSidebarItem,
@@ -47,6 +49,7 @@ class Window(Adw.ApplicationWindow):
split_view: Adw.OverlaySplitView = Gtk.Template.Child()
collections: Adw.SidebarSection = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
new_collection_item: Adw.SidebarItem = 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()
@@ -64,6 +67,8 @@ class Window(Adw.ApplicationWindow):
settings = GObject.Property(type=Gtk.Settings)
_selected_sidebar_item = 0
@GObject.Property(type=Gio.ListStore)
def games(self) -> Gio.ListStore:
"""Model of the user's games."""
@@ -128,9 +133,24 @@ class Window(Adw.ApplicationWindow):
@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
)
match item:
case self.new_collection_item:
self._add_collection()
sidebar.props.selected = self._selected_sidebar_item
case CollectionSidebarItem():
self.collection = item.collection
case _:
self.collection = None
if item is not self.new_collection_item:
self._selected_sidebar_item = index
@Gtk.Template.Callback()
def _update_selection(self, sidebar: Adw.Sidebar, *_args): # pyright: ignore[reportAttributeAccessIssue]
if sidebar.props.selected_item is self.new_collection_item:
sidebar.props.selected = self._selected_sidebar_item
self._selected_sidebar_item = sidebar.props.selected
@Gtk.Template.Callback()
def _setup_gamepad_monitor(self, *_args):
@@ -186,6 +206,10 @@ class Window(Adw.ApplicationWindow):
self.details.edit()
def _add_collection(self):
details = CollectionDetails(collection=Collection())
details.present(self)
def _undo(self, toast: Adw.Toast | None = None):
if toast:
self._history.pop(toast)()