collections: Support adding games

This commit is contained in:
Jamie Gravendeel
2025-12-22 15:00:33 +01:00
parent 7d7999d8a9
commit 94db6acf37
8 changed files with 202 additions and 24 deletions

View File

@@ -1,7 +1,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from typing import Any, override
from collections.abc import Iterable
from typing import Any, cast, override
from gi.repository import Adw, GObject, Gtk
@@ -54,6 +55,76 @@ class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttribute
self.bind_property("title", self, "tooltip", GObject.BindingFlags.SYNC_CREATE)
class CollectionButton(Gtk.ToggleButton):
"""A toggle button representing a collection."""
collection = GObject.Property(type=Collection)
def __init__(self, collection: Collection, **kwargs: Any):
super().__init__(**kwargs)
self.collection = collection
self.props.child = Adw.ButtonContent(
icon_name=collection.icon_name,
label=collection.name,
can_shrink=True,
)
class CollectionsBox(Adw.Bin):
"""A wrap box for adding games to collections."""
__gtype_name__ = __qualname__
game = GObject.Property(type=Game)
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
self.props.child = self.box = Adw.WrapBox(
child_spacing=6,
line_spacing=6,
justify=Adw.JustifyMode.FILL,
justify_last_line=True,
natural_line_length=240,
)
model.bind_property(
"n-items",
self,
"visible",
GObject.BindingFlags.SYNC_CREATE,
)
def build(self):
"""Populate the box with collections."""
for collection in cast(Iterable[Collection], model):
button = CollectionButton(collection)
button.props.active = self.game.game_id in collection.game_ids
self.box.append(button)
def finish(self):
"""Clear the box and save changes."""
filter_changed = False
for button in cast(Iterable[CollectionButton], self.box):
game_ids = button.collection.game_ids
old_game_ids = game_ids.copy()
in_collection = self.game.game_id in game_ids
if button.props.active and not in_collection:
game_ids.append(self.game.game_id)
elif not button.props.active and in_collection:
game_ids.remove(self.game.game_id)
if game_ids != old_game_ids:
filter_changed = True
self.box.remove_all() # pyright: ignore[reportAttributeAccessIssue]
collections.save()
if filter_changed:
self.activate_action("win.notify-collection-filter")
sorter = Gtk.StringSorter.new(Gtk.PropertyExpression.new(Collection, None, "name"))
model = Gtk.SortListModel.new(
Gtk.FilterListModel(

View File

@@ -164,6 +164,52 @@ template $GameDetails: Adw.NavigationPage {
]
}
MenuButton {
icon-name: "collection-symbolic";
tooltip-text: _("Collections");
valign: center;
notify::active => $_setup_collections();
popover: PopoverMenu {
menu-model: menu {
item (_("New Collection"), "win.add-collection")
item {
custom: "collections";
}
};
[collections]
Box {
orientation: vertical;
visible: bind collections_box.visible;
Separator {}
Label {
label: _("Collections");
halign: start;
margin-top: 6;
margin-bottom: 9;
margin-start: 12;
margin-end: 12;
styles [
"heading",
]
}
$CollectionsBox collections_box {
game: bind template.game;
}
}
};
styles [
"circular",
]
}
Button hide_button {
visible: bind hide_button.sensitive;
action-name: "game.hide";

View File

@@ -29,23 +29,61 @@ template $GameItem: Box {
margin-top: 6;
margin-end: 6;
notify::active => $_reveal_buttons();
notify::active => $_setup_collections();
menu-model: menu {
item (_("Edit"), "item.edit")
popover: PopoverMenu {
menu-model: menu {
section {
item (_("Edit"), "item.edit")
item {
label: _("Hide");
action: "game.hide";
hidden-when: "action-disabled";
item {
label: _("Hide");
action: "game.hide";
hidden-when: "action-disabled";
}
item {
label: _("Unhide");
action: "game.unhide";
hidden-when: "action-disabled";
}
item (_("Remove"), "game.remove")
}
section {
item (_("New Collection"), "win.add-collection")
item {
custom: "collections";
}
}
};
[collections]
Box {
orientation: vertical;
visible: bind collections_box.visible;
Separator {}
Label {
label: _("Collections");
halign: start;
margin-top: 6;
margin-bottom: 9;
margin-start: 12;
margin-end: 12;
styles [
"heading",
]
}
$CollectionsBox collections_box {
game: bind template.game;
}
}
item {
label: _("Unhide");
action: "game.unhide";
hidden-when: "action-disabled";
}
item (_("Remove"), "game.remove")
};
styles [

View File

@@ -16,6 +16,7 @@ from cartridges import games
from cartridges.config import PREFIX
from cartridges.games import Game
from .collections import CollectionsBox
from .cover import Cover # noqa: F401
_POP_ON_ACTION = "hide", "unhide", "remove"
@@ -35,6 +36,7 @@ class GameDetails(Adw.NavigationPage):
stack: Adw.ViewStack = Gtk.Template.Child()
actions: Gtk.Box = Gtk.Template.Child()
collections_box: CollectionsBox = Gtk.Template.Child()
name_entry: Adw.EntryRow = Gtk.Template.Child()
developer_entry: Adw.EntryRow = Gtk.Template.Child()
executable_entry: Adw.EntryRow = Gtk.Template.Child()
@@ -131,6 +133,13 @@ class GameDetails(Adw.NavigationPage):
self.stack.props.visible_child_name = "details"
@Gtk.Template.Callback()
def _setup_collections(self, button: Gtk.MenuButton, *_args):
if button.props.active:
self.collections_box.build()
else:
self.collections_box.finish()
@Gtk.Template.Callback()
def _or(self, _obj, first: _T, second: _T) -> _T:
return first or second

View File

@@ -8,6 +8,7 @@ from gi.repository import Gio, GLib, GObject, Gtk
from cartridges.config import PREFIX
from cartridges.games import Game
from .collections import CollectionsBox
from .cover import Cover # noqa: F401
@@ -19,6 +20,7 @@ class GameItem(Gtk.Box):
motion: Gtk.EventControllerMotion = Gtk.Template.Child()
options: Gtk.MenuButton = Gtk.Template.Child()
collections_box: CollectionsBox = Gtk.Template.Child()
play: Gtk.Button = Gtk.Template.Child()
position = GObject.Property(type=int)
@@ -56,3 +58,10 @@ class GameItem(Gtk.Box):
):
widget.props.can_focus = widget.props.can_target = reveal
(widget.remove_css_class if reveal else widget.add_css_class)("hidden")
@Gtk.Template.Callback()
def _setup_collections(self, button: Gtk.MenuButton, *_args):
if button.props.active:
self.collections_box.build()
else:
self.collections_box.finish()

View File

@@ -24,7 +24,8 @@
padding: 12px;
}
#game-item button {
#game-item overlay > button,
#game-item overlay > menubutton > button {
color: white;
backdrop-filter: blur(9px) brightness(30%) saturate(600%);
box-shadow:
@@ -37,12 +38,12 @@
opacity, transform;
}
#game-item button.hidden {
#game-item overlay > button.hidden {
opacity: 0;
transform: translateY(3px);
}
#game-item menubutton.hidden button {
#game-item overlay > menubutton.hidden > button {
opacity: 0;
transform: translateY(-6px);
}
@@ -83,7 +84,8 @@
outline: 5px solid var(--accent-color);
}
#game-item button {
#game-item overlay > button,
#game-item overlay > menubutton > button {
backdrop-filter: blur(9px) brightness(20%) saturate(600%);
}

View File

@@ -237,7 +237,7 @@ template $Window: Adw.ApplicationWindow {
watch-items: true;
filter: EveryFilter {
$CollectionFilter {
$CollectionFilter collection_filter {
collection: bind template.collection;
}

View File

@@ -17,10 +17,7 @@ from cartridges.games import Game
from cartridges.ui import collections
from .collection_details import CollectionDetails
from .collections import (
CollectionFilter, # noqa: F401
CollectionSidebarItem,
)
from .collections import CollectionFilter, CollectionSidebarItem
from .game_details import GameDetails
from .game_item import GameItem # noqa: F401
from .games import GameSorter
@@ -61,6 +58,7 @@ class Window(Adw.ApplicationWindow):
toast_overlay: Adw.ToastOverlay = Gtk.Template.Child()
grid: Gtk.GridView = Gtk.Template.Child()
sorter: GameSorter = Gtk.Template.Child()
collection_filter: CollectionFilter = Gtk.Template.Child()
details: GameDetails = Gtk.Template.Child()
search_text = GObject.Property(type=str)
@@ -130,6 +128,7 @@ class Window(Adw.ApplicationWindow):
"u",
),
("add", lambda *_: self._add()),
("add-collection", lambda *_: self._add_collection()),
(
"edit-collection",
lambda _action, param, *_: self._edit_collection(param.get_uint32()),
@@ -140,6 +139,10 @@ class Window(Adw.ApplicationWindow):
lambda _action, param, *_: self._remove_collection(param.get_uint32()),
"u",
),
(
"notify-collection-filter",
lambda *_: self.collection_filter.changed(Gtk.FilterChange.DIFFERENT),
),
("undo", lambda *_: self._undo()),
))