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-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel # 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 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) 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")) sorter = Gtk.StringSorter.new(Gtk.PropertyExpression.new(Collection, None, "name"))
model = Gtk.SortListModel.new( model = Gtk.SortListModel.new(
Gtk.FilterListModel( 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 { Button hide_button {
visible: bind hide_button.sensitive; visible: bind hide_button.sensitive;
action-name: "game.hide"; action-name: "game.hide";

View File

@@ -29,23 +29,61 @@ template $GameItem: Box {
margin-top: 6; margin-top: 6;
margin-end: 6; margin-end: 6;
notify::active => $_reveal_buttons(); notify::active => $_reveal_buttons();
notify::active => $_setup_collections();
menu-model: menu { popover: PopoverMenu {
item (_("Edit"), "item.edit") menu-model: menu {
section {
item (_("Edit"), "item.edit")
item { item {
label: _("Hide"); label: _("Hide");
action: "game.hide"; action: "game.hide";
hidden-when: "action-disabled"; 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 [ styles [

View File

@@ -16,6 +16,7 @@ from cartridges import games
from cartridges.config import PREFIX from cartridges.config import PREFIX
from cartridges.games import Game from cartridges.games import Game
from .collections import CollectionsBox
from .cover import Cover # noqa: F401 from .cover import Cover # noqa: F401
_POP_ON_ACTION = "hide", "unhide", "remove" _POP_ON_ACTION = "hide", "unhide", "remove"
@@ -35,6 +36,7 @@ class GameDetails(Adw.NavigationPage):
stack: Adw.ViewStack = Gtk.Template.Child() stack: Adw.ViewStack = Gtk.Template.Child()
actions: Gtk.Box = Gtk.Template.Child() actions: Gtk.Box = Gtk.Template.Child()
collections_box: CollectionsBox = Gtk.Template.Child()
name_entry: Adw.EntryRow = Gtk.Template.Child() name_entry: Adw.EntryRow = Gtk.Template.Child()
developer_entry: Adw.EntryRow = Gtk.Template.Child() developer_entry: Adw.EntryRow = Gtk.Template.Child()
executable_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" 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() @Gtk.Template.Callback()
def _or(self, _obj, first: _T, second: _T) -> _T: def _or(self, _obj, first: _T, second: _T) -> _T:
return first or second 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.config import PREFIX
from cartridges.games import Game from cartridges.games import Game
from .collections import CollectionsBox
from .cover import Cover # noqa: F401 from .cover import Cover # noqa: F401
@@ -19,6 +20,7 @@ class GameItem(Gtk.Box):
motion: Gtk.EventControllerMotion = Gtk.Template.Child() motion: Gtk.EventControllerMotion = Gtk.Template.Child()
options: Gtk.MenuButton = Gtk.Template.Child() options: Gtk.MenuButton = Gtk.Template.Child()
collections_box: CollectionsBox = Gtk.Template.Child()
play: Gtk.Button = Gtk.Template.Child() play: Gtk.Button = Gtk.Template.Child()
position = GObject.Property(type=int) position = GObject.Property(type=int)
@@ -56,3 +58,10 @@ class GameItem(Gtk.Box):
): ):
widget.props.can_focus = widget.props.can_target = reveal widget.props.can_focus = widget.props.can_target = reveal
(widget.remove_css_class if reveal else widget.add_css_class)("hidden") (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; padding: 12px;
} }
#game-item button { #game-item overlay > button,
#game-item overlay > menubutton > button {
color: white; color: white;
backdrop-filter: blur(9px) brightness(30%) saturate(600%); backdrop-filter: blur(9px) brightness(30%) saturate(600%);
box-shadow: box-shadow:
@@ -37,12 +38,12 @@
opacity, transform; opacity, transform;
} }
#game-item button.hidden { #game-item overlay > button.hidden {
opacity: 0; opacity: 0;
transform: translateY(3px); transform: translateY(3px);
} }
#game-item menubutton.hidden button { #game-item overlay > menubutton.hidden > button {
opacity: 0; opacity: 0;
transform: translateY(-6px); transform: translateY(-6px);
} }
@@ -83,7 +84,8 @@
outline: 5px solid var(--accent-color); 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%); backdrop-filter: blur(9px) brightness(20%) saturate(600%);
} }

View File

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

View File

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