Compare commits

...

8 Commits

Author SHA1 Message Date
Jamie Gravendeel
dab108ce8b ui: Use GObject.BindingGroup 2026-01-10 01:46:20 +01:00
Jamie Gravendeel
515bafa428 ui: Use GObject.SignalGroup 2026-01-10 01:22:55 +01:00
Jamie Gravendeel
c5cfa476ff sources: Add initial UI 2026-01-05 20:34:32 +01:00
Jamie Gravendeel
7bc9d6aee9 window: Move game filters to ui.games 2026-01-05 20:34:32 +01:00
Jamie Gravendeel
00795b83fd sources: Fix icon name 2026-01-05 20:34:32 +01:00
Jamie Gravendeel
f9cb794394 cover: Add width and height properties 2026-01-05 19:43:38 +01:00
Jamie Gravendeel
1aee234cbf cartridges: Use generic methods in favor of TypeVar 2026-01-05 19:34:02 +01:00
Jamie Gravendeel
21588fe92b application: Only use GTK after startup 2026-01-05 19:31:11 +01:00
20 changed files with 211 additions and 105 deletions

View File

@@ -19,6 +19,11 @@ class Application(Adw.Application):
def __init__(self):
super().__init__(application_id=APP_ID)
@override
def do_startup(self):
Adw.Application.do_startup(self)
self.props.style_manager.props.color_scheme = Adw.ColorScheme.PREFER_DARK
self.add_action_entries((
("quit", lambda *_: self.quit()),
("about", lambda *_: self._present_about_dialog()),
@@ -28,11 +33,6 @@ class Application(Adw.Application):
sources.load()
collections.load()
@override
def do_startup(self):
Adw.Application.do_startup(self)
Adw.StyleManager.get_default().props.color_scheme = Adw.ColorScheme.PREFER_DARK
@override
def do_activate(self):
window = self.props.active_window or Window(application=self)

View File

@@ -54,6 +54,8 @@ class _SourceModule(Protocol):
class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatibleMethodOverride]
"""A source of games to import."""
__gtype_name__ = __qualname__
id = GObject.Property(type=str)
name = GObject.Property(type=str)
icon_name = GObject.Property(type=str)
@@ -65,11 +67,11 @@ class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatib
self.id, self.name, self._module = module.ID, module.NAME, module
self.bind_property(
"name",
"id",
self,
"icon-name",
GObject.BindingFlags.SYNC_CREATE,
lambda _, name: f"{name}-symbolic",
lambda _, ident: f"{ident}-symbolic",
)
try:

View File

@@ -1,5 +1,11 @@
using Gtk 4.0;
using Adw 1;
using GObject 2.0;
GObject.SignalGroup collection_signals {
target: bind template.collection;
target-type: typeof<$Collection>;
}
template $CollectionDetails: Adw.Dialog {
title: bind $_or(template.collection as <$Collection>.name, _("New Collection")) as <string>;

View File

@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from itertools import product
from typing import Any, NamedTuple, TypeVar, cast
from typing import Any, NamedTuple
from gi.repository import Adw, Gio, GObject, Gtk
@@ -40,8 +40,6 @@ _ICONS = (
_Icon("fist", ""),
)
_T = TypeVar("_T")
@Gtk.Template.from_resource(f"{PREFIX}/collection-details.ui")
class CollectionDetails(Adw.Dialog):
@@ -52,6 +50,7 @@ class CollectionDetails(Adw.Dialog):
name_entry: Adw.EntryRow = Gtk.Template.Child()
icons_grid: Gtk.Grid = Gtk.Template.Child()
collection_signals: GObject.SignalGroup = Gtk.Template.Child()
sort_changed = GObject.Signal()
_selected_icon: str
@@ -65,10 +64,8 @@ class CollectionDetails(Adw.Dialog):
def collection(self, collection: Collection):
self._collection = collection
self.insert_action_group("collection", collection)
remove_action = cast(Gio.SimpleAction, collection.lookup_action("remove"))
remove_action.connect("activate", lambda *_: self.force_close())
def __init__(self, **kwargs: Any):
def __init__(self, collection: Collection, **kwargs: Any):
super().__init__(**kwargs)
self.insert_action_group("details", group := Gio.SimpleActionGroup())
@@ -83,6 +80,13 @@ class CollectionDetails(Adw.Dialog):
transform_to=lambda _, text: bool(text),
)
self.collection_signals.connect_closure(
"notify::removed",
lambda *_: self.force_close(),
after=True,
)
self.collection = collection
group_button = None
for index, (row, col) in enumerate(product(range(3), range(7))):
icon = _ICONS[index].name
@@ -132,9 +136,9 @@ class CollectionDetails(Adw.Dialog):
self.close()
@Gtk.Template.Callback()
def _or(self, _obj, first: _T, second: _T) -> _T:
def _or[T](self, _obj, first: T, second: T) -> T:
return first or second
@Gtk.Template.Callback()
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T:
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return first if condition else second

View File

@@ -37,19 +37,9 @@ class CollectionFilter(Gtk.Filter):
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 = GObject.Property(type=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):
def __init__(self, collection: Collection, **kwargs: Any):
super().__init__(**kwargs)
self.bind_property(
@@ -60,6 +50,13 @@ class CollectionSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttribute
lambda _, name: GLib.markup_escape_text(name),
)
self._collection_bindings = GObject.BindingGroup()
flags = GObject.BindingFlags.DEFAULT
self._collection_bindings.bind("name", self, "title", flags)
self._collection_bindings.bind("icon-name", self, "icon-name", flags)
self.bind_property("collection", self._collection_bindings, "source")
self.collection = collection
class CollectionButton(Gtk.ToggleButton):
"""A toggle button representing a collection."""

View File

@@ -5,27 +5,27 @@ template $Cover: Adw.Bin {
child: Adw.Clamp {
orientation: vertical;
unit: px;
maximum-size: bind _picture.height-request;
tightening-threshold: bind _picture.height-request;
maximum-size: bind template.height;
tightening-threshold: bind template.height;
child: Adw.Clamp {
unit: px;
maximum-size: bind _picture.width-request;
tightening-threshold: bind _picture.width-request;
maximum-size: bind template.width;
tightening-threshold: bind template.width;
child: Adw.ViewStack {
name: "cover";
visible-child-name: bind $_get_stack_child(template.paintable) as <string>;
visible-child-name: bind $_if_else(template.paintable, "cover", "icon") as <string>;
overflow: hidden;
enable-transitions: true;
Adw.ViewStackPage {
name: "cover";
child: Picture _picture {
child: Picture {
paintable: bind template.paintable;
width-request: 200;
height-request: 300;
width-request: bind template.width;
height-request: bind template.height;
content-fit: cover;
};
}
@@ -34,7 +34,10 @@ template $Cover: Adw.Bin {
name: "icon";
child: Image {
icon-name: bind template.app-icon-name;
icon-name: bind $_concat(
template.root as <Window>.application as <Application>.application-id,
"-symbolic"
) as <string>;
pixel-size: 80;
styles [

View File

@@ -3,7 +3,7 @@
from gi.repository import Adw, Gdk, GObject, Gtk
from cartridges.config import APP_ID, PREFIX
from cartridges.config import PREFIX
@Gtk.Template.from_resource(f"{PREFIX}/cover.ui")
@@ -12,12 +12,14 @@ class Cover(Adw.Bin):
__gtype_name__ = __qualname__
picture = GObject.Property(lambda self: self._picture, type=Gtk.Picture)
paintable = GObject.Property(type=Gdk.Paintable)
app_icon_name = GObject.Property(type=str, default=f"{APP_ID}-symbolic")
_picture = Gtk.Template.Child()
width = GObject.Property(type=int, default=200)
height = GObject.Property(type=int, default=300)
@Gtk.Template.Callback()
def _get_stack_child(self, _obj, paintable: Gdk.Paintable | None) -> str:
return "cover" if paintable else "icon"
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return first if condition else second
@Gtk.Template.Callback()
def _concat(self, _obj, *strings: str) -> str:
return "".join(strings)

View File

@@ -1,6 +1,12 @@
using Gtk 4.0;
using Gdk 4.0;
using Adw 1;
using GObject 2.0;
GObject.SignalGroup game_signals {
target: bind template.game;
target-type: typeof<$Game>;
}
template $GameDetails: Adw.NavigationPage {
name: "details";

View File

@@ -12,8 +12,8 @@ template $GameItem: Box {
Adw.Clamp {
unit: px;
maximum-size: bind cover.picture as <Picture>.width-request;
tightening-threshold: bind cover.picture as <Picture>.width-request;
maximum-size: bind cover.width;
tightening-threshold: bind cover.width;
child: Overlay {
child: $Cover cover {

View File

@@ -7,7 +7,7 @@ import sys
import time
from datetime import UTC, datetime
from gettext import gettext as _
from typing import Any, TypeVar, cast
from typing import Any, cast
from urllib.parse import quote
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
@@ -20,14 +20,12 @@ from cartridges.sources import imported
from .collections import CollectionsBox
from .cover import Cover # noqa: F401
_POP_ON_ACTION = "hide", "unhide", "remove"
_POP_ON_PROPERTY_NOTIFY = "hidden", "removed"
_EDITABLE_PROPERTIES = {prop.name for prop in games.PROPERTIES if prop.editable}
_REQUIRED_PROPERTIES = {
prop.name for prop in games.PROPERTIES if prop.editable and prop.required
}
_T = TypeVar("_T")
@Gtk.Template.from_resource(f"{PREFIX}/game-details.ui")
class GameDetails(Adw.NavigationPage):
@@ -42,6 +40,7 @@ class GameDetails(Adw.NavigationPage):
developer_entry: Adw.EntryRow = Gtk.Template.Child()
executable_entry: Adw.EntryRow = Gtk.Template.Child()
game_signals: GObject.SignalGroup = Gtk.Template.Child()
sort_changed = GObject.Signal()
@GObject.Property(type=Game)
@@ -54,21 +53,9 @@ class GameDetails(Adw.NavigationPage):
self._game = game
self.insert_action_group("game", game)
for action, ident in self._signal_ids.copy().items():
action.disconnect(ident)
del self._signal_ids[action]
for name in _POP_ON_ACTION:
action = cast(Gio.SimpleAction, game.lookup_action(name))
self._signal_ids[action] = action.connect(
"activate", lambda *_: self.activate_action("navigation.pop")
)
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
self._signal_ids = dict[Gio.SimpleAction, int]()
self.insert_action_group("details", group := Gio.SimpleActionGroup())
group.add_action_entries((
("edit", lambda *_: self.edit()),
@@ -102,6 +89,13 @@ class GameDetails(Adw.NavigationPage):
valid = Gtk.ClosureExpression.new(bool, lambda _, *values: all(values), entries)
valid.bind(apply, "enabled")
for name in _POP_ON_PROPERTY_NOTIFY:
self.game_signals.connect_closure(
f"notify::{name}",
lambda *_: self.activate_action("navigation.pop"),
after=True,
)
def edit(self):
"""Enter edit mode."""
for prop in _EDITABLE_PROPERTIES:
@@ -148,11 +142,11 @@ class GameDetails(Adw.NavigationPage):
self.collections_box.finish()
@Gtk.Template.Callback()
def _or(self, _obj, first: _T, second: _T) -> _T:
def _or[T](self, _obj, first: T, second: T) -> T:
return first or second
@Gtk.Template.Callback()
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T:
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return first if condition else second
@Gtk.Template.Callback()

View File

@@ -19,7 +19,24 @@ _SORT_MODES = {
"oldest": ("added", False),
}
model = Gtk.FlattenListModel.new(sources.model)
filter_ = Gtk.EveryFilter()
filter_.append(
Gtk.BoolFilter(
expression=Gtk.PropertyExpression.new(Game, None, "removed"),
invert=True,
)
)
filter_.append(
Gtk.BoolFilter(
expression=Gtk.PropertyExpression.new(Game, None, "blacklisted"),
invert=True,
)
)
model = Gtk.FilterListModel(
model=Gtk.FlattenListModel.new(sources.model),
filter=filter_,
watch_items=True, # pyright: ignore[reportCallIssue]
)
class GameSorter(Gtk.Sorter):

View File

@@ -7,6 +7,7 @@ python.install_sources(
'game_details.py',
'game_item.py',
'games.py',
'sources.py',
'window.py',
),
subdir: 'cartridges' / 'ui',

50
cartridges/ui/sources.py Normal file
View File

@@ -0,0 +1,50 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
from typing import Any
from gi.repository import Adw, Gio, GObject, Gtk
from cartridges import sources
from cartridges.sources import Source
from cartridges.ui import games
class SourceSidebarItem(Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
"""A sidebar item representing a source."""
source = GObject.Property(type=Source)
model = GObject.Property(type=Gio.ListModel)
def __init__(self, source: Source, **kwargs: Any):
super().__init__(**kwargs)
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7959
self._model_signals = GObject.SignalGroup.new(Gio.ListModel)
self._model_signals.connect_closure(
"items-changed",
lambda model, *_: model.notify("n-items"),
after=True,
)
self._source_bindings = GObject.BindingGroup()
flags = GObject.BindingFlags.DEFAULT
self._source_bindings.bind("name", self, "title", flags)
self._source_bindings.bind("icon-name", self, "icon-name", flags)
self.bind_property("source", self._source_bindings, "source")
self.model = Gtk.FilterListModel(filter=games.filter_, watch_items=True) # pyright: ignore[reportCallIssue]
self.model.bind_property(
"n-items",
self,
"visible",
GObject.BindingFlags.SYNC_CREATE,
)
self.bind_property("source", self.model, "model")
self.source = source
model = Gtk.SortListModel.new(
sources.model,
Gtk.StringSorter.new(Gtk.PropertyExpression.new(Source, None, "name")),
)

View File

@@ -1,5 +1,17 @@
using Gtk 4.0;
using Adw 1;
using GObject 2.0;
using Gio 2.0;
GObject.SignalGroup collection_signals {
target: bind template.collection;
target-type: typeof<$Collection>;
}
GObject.SignalGroup model_signals {
target: bind template.model;
target-type: typeof<Gio.ListModel>;
}
template $Window: Adw.ApplicationWindow {
realize => $_setup_gamepad_monitor();
@@ -94,6 +106,10 @@ template $Window: Adw.ApplicationWindow {
}
}
Adw.SidebarSection sources {
title: _("Sources");
}
Adw.SidebarSection collections {
title: _("Collections");
@@ -262,16 +278,6 @@ template $Window: Adw.ApplicationWindow {
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.model;

View File

@@ -6,7 +6,7 @@
import sys
from collections.abc import Callable
from gettext import gettext as _
from typing import Any, TypeVar, cast
from typing import Any, cast
from gi.repository import Adw, Gio, GLib, GObject, Gtk
@@ -14,13 +14,14 @@ from cartridges import STATE_SETTINGS
from cartridges.collections import Collection
from cartridges.config import PREFIX, PROFILE
from cartridges.sources import imported
from cartridges.ui import collections, games
from cartridges.ui import collections, games, sources
from .collection_details import CollectionDetails
from .collections import CollectionFilter, CollectionSidebarItem
from .game_details import GameDetails
from .game_item import GameItem # noqa: F401
from .games import GameSorter
from .sources import SourceSidebarItem
if sys.platform.startswith("linux"):
from cartridges import gamepads
@@ -34,7 +35,6 @@ SORT_MODES = {
"oldest": ("added", True),
}
_T = TypeVar("_T")
type _UndoFunc = Callable[[], Any]
@@ -46,6 +46,7 @@ class Window(Adw.ApplicationWindow):
split_view: Adw.OverlaySplitView = Gtk.Template.Child()
sidebar: Adw.Sidebar = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
sources: Adw.SidebarSection = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
collections: Adw.SidebarSection = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
new_collection_item: Adw.SidebarItem = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
collection_menu: Gio.Menu = Gtk.Template.Child()
@@ -61,38 +62,18 @@ class Window(Adw.ApplicationWindow):
collection_filter: CollectionFilter = Gtk.Template.Child()
details: GameDetails = Gtk.Template.Child()
model = GObject.Property(type=Gio.ListModel, default=games.model)
collection = GObject.Property(type=Collection)
collection_signals: GObject.SignalGroup = Gtk.Template.Child()
model = GObject.Property(type=Gio.ListModel)
model_signals: GObject.SignalGroup = Gtk.Template.Child()
search_text = GObject.Property(type=str)
show_hidden = GObject.Property(type=bool, default=False)
settings = GObject.Property(type=Gtk.Settings)
_collection: Collection | None = None
_collection_removed_signal: int | None = None
_selected_sidebar_item = 0
@GObject.Property(type=Collection)
def collection(self) -> Collection | None:
"""The currently selected collection."""
return self._collection
@collection.setter
def collection(self, collection: Collection | None):
if self._collection and self._collection_removed_signal:
self._collection.disconnect(self._collection_removed_signal)
self._collection = collection
self._collection_removed_signal = (
collection.connect("notify::removed", lambda *_: self._collection_removed())
if collection
else None
)
def _collection_removed(self):
self.collection = None
self.sidebar.props.selected = 0
def __init__(self, **kwargs: Any):
super().__init__(**kwargs)
@@ -109,9 +90,13 @@ class Window(Adw.ApplicationWindow):
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
self.search_entry.set_key_capture_widget(self)
self.sources.bind_model(
sources.model,
lambda source: SourceSidebarItem(source),
)
self.collections.bind_model(
collections.model,
lambda collection: CollectionSidebarItem(collection=collection),
lambda collection: CollectionSidebarItem(collection),
)
self.add_action(STATE_SETTINGS.create_action("show-sidebar"))
@@ -147,6 +132,18 @@ class Window(Adw.ApplicationWindow):
("undo", lambda *_: self._undo()),
))
self.collection_signals.connect_closure(
"notify::removed",
lambda *_: self._collection_removed(),
after=True,
)
self.model_signals.connect_closure(
"items-changed",
lambda model, *_: None if model else self._model_emptied(),
after=True,
)
self.model = games.model
self._history: dict[Adw.Toast, _UndoFunc] = {}
def send_toast(self, title: str, *, undo: _UndoFunc | None = None):
@@ -163,6 +160,14 @@ class Window(Adw.ApplicationWindow):
self.toast_overlay.add_toast(toast)
def _collection_removed(self):
self.collection = None
self.sidebar.props.selected = 0
def _model_emptied(self):
self.model = games.model
self.sidebar.props.selected = 0
@Gtk.Template.Callback()
def _show_sidebar_title(self, _obj, layout: str) -> bool:
right_window_controls = layout.replace("appmenu", "").startswith(":")
@@ -176,10 +181,15 @@ class Window(Adw.ApplicationWindow):
case self.new_collection_item:
self._add_collection()
sidebar.props.selected = self._selected_sidebar_item
case SourceSidebarItem():
self.collection = None
self.model = item.model
case CollectionSidebarItem():
self.collection = item.collection
self.model = games.model
case _:
self.collection = None
self.model = games.model
if item is not self.new_collection_item:
self._selected_sidebar_item = index
@@ -214,7 +224,7 @@ class Window(Adw.ApplicationWindow):
gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable]
@Gtk.Template.Callback()
def _if_else(self, _obj, condition: object, first: _T, second: _T) -> _T:
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
return first if condition else second
@Gtk.Template.Callback()
@@ -266,12 +276,12 @@ class Window(Adw.ApplicationWindow):
if game_id:
collection.game_ids.add(game_id)
details = CollectionDetails(collection=collection)
details = CollectionDetails(collection)
details.present(self)
def _edit_collection(self, pos: int):
collection = self.collections.get_item(pos).collection
details = CollectionDetails(collection=collection)
details = CollectionDetails(collection)
details.connect(
"sort-changed",
lambda *_: collections.sorter.changed(Gtk.SorterChange.DIFFERENT),

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="#000" fill-rule="evenodd" d="m7.872 16-3.817-3.083L2 2.79 7.872 0l5.871 2.789-2.055 10.128zm0-4.257-.294-.293-.88-7.927 1.1-1.908 1.174 1.908-.807 7.927zm-.294.367-.147.367-1.761.294-.294-.66.294-.662 1.761.294zm-.073.734-.22 1.541.587.294.587-.294-.22-1.541-.367-.22zm.807-.367-.147-.367.147-.367 1.761-.293.294.66-.294.66z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@@ -4,6 +4,10 @@
<file>apply-symbolic.svg</file>
<file>cancel-symbolic.svg</file>
<file>filter-symbolic.svg</file>
<!-- Sources -->
<file>heroic-symbolic.svg</file>
<file>imported-symbolic.svg</file>
<file>steam-symbolic.svg</file>
<!-- Categories -->
<file>collection-symbolic.svg</file>
<file>ball-symbolic.svg</file>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#222" d="M7 1v6H1v2h6v6h2V9h6V7H9V1zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 144 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><g clip-path="url(#a)"><path fill="#000" fill-rule="evenodd" d="M9.352 5.1a1.509 1.509 0 1 0 2.51 1.675A1.509 1.509 0 0 0 9.352 5.1m2.923-.277a2.009 2.009 0 1 1-3.34 2.231 2.009 2.009 0 0 1 3.34-2.23ZM5.01 12.131l-.983-.407a1.7 1.7 0 0 0 3.108-.103 1.696 1.696 0 0 0-1.213-2.29 1.7 1.7 0 0 0-.966.07l1.015.421a1.249 1.249 0 0 1-.96 2.307zM2.546 2.121A8 8 0 0 1 7.966 0l.003.013a7.99 7.99 0 0 1 7.159 4.432 7.996 7.996 0 0 1-4.277 11.018 7.99 7.99 0 0 1-8.274-1.558A8 8 0 0 1 .279 10.18l3.064 1.267A2.264 2.264 0 0 0 7.823 11v-.107l2.718-1.938h.063A3.016 3.016 0 1 0 7.589 5.94v.031l-1.906 2.76h-.126c-.454 0-.898.138-1.273.395L0 7.354A8 8 0 0 1 2.546 2.12Z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 842 B

View File

@@ -17,6 +17,7 @@ cartridges/ui/game_details.py
cartridges/ui/game_item.py
cartridges/ui/games.py
cartridges/ui/shortcuts-dialog.blp
cartridges/ui/sources.py
cartridges/ui/window.blp
cartridges/ui/window.py
data/page.kramo.Cartridges.desktop.in