diff --git a/cartridges/sources/__init__.py b/cartridges/sources/__init__.py
index bad9b62..ce09a22 100644
--- a/cartridges/sources/__init__.py
+++ b/cartridges/sources/__init__.py
@@ -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)
diff --git a/cartridges/ui/meson.build b/cartridges/ui/meson.build
index e92caf3..b9eb6de 100644
--- a/cartridges/ui/meson.build
+++ b/cartridges/ui/meson.build
@@ -7,6 +7,7 @@ python.install_sources(
'game_details.py',
'game_item.py',
'games.py',
+ 'sources.py',
'window.py',
),
subdir: 'cartridges' / 'ui',
diff --git a/cartridges/ui/sources.py b/cartridges/ui/sources.py
new file mode 100644
index 0000000..d8e58c7
--- /dev/null
+++ b/cartridges/ui/sources.py
@@ -0,0 +1,44 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
+
+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."""
+
+ model = GObject.Property(type=Gio.ListModel)
+
+ @GObject.Property(type=Source)
+ def source(self) -> Source:
+ """The source that `self` represents."""
+ return self._source
+
+ @source.setter
+ def source(self, source: Source):
+ self._source = source
+ flags = GObject.BindingFlags.SYNC_CREATE
+ source.bind_property("name", self, "title", flags)
+ source.bind_property("icon-name", self, "icon-name", flags)
+
+ self.model = Gtk.FilterListModel(
+ model=source,
+ filter=games.filter_,
+ watch_items=True, # pyright: ignore[reportCallIssue]
+ )
+ # https://gitlab.gnome.org/GNOME/gtk/-/issues/7959
+ self.model.connect(
+ "items-changed",
+ lambda *_: self.set_property("visible", self.model.props.n_items),
+ )
+ self.props.visible = self.model.props.n_items
+
+
+model = Gtk.SortListModel.new(
+ sources.model,
+ Gtk.StringSorter.new(Gtk.PropertyExpression.new(Source, None, "name")),
+)
diff --git a/cartridges/ui/window.blp b/cartridges/ui/window.blp
index 037aa32..a872c7b 100644
--- a/cartridges/ui/window.blp
+++ b/cartridges/ui/window.blp
@@ -94,6 +94,10 @@ template $Window: Adw.ApplicationWindow {
}
}
+ Adw.SidebarSection sources {
+ title: _("Sources");
+ }
+
Adw.SidebarSection collections {
title: _("Collections");
diff --git a/cartridges/ui/window.py b/cartridges/ui/window.py
index 08c2515..3df39ed 100644
--- a/cartridges/ui/window.py
+++ b/cartridges/ui/window.py
@@ -13,14 +13,15 @@ from gi.repository import Adw, Gio, GLib, GObject, Gtk
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.sources import Source, imported
+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
@@ -45,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()
@@ -108,6 +110,7 @@ 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, self._create_source_item)
self.collections.bind_model(
collections.model,
lambda collection: CollectionSidebarItem(collection=collection),
@@ -162,6 +165,18 @@ class Window(Adw.ApplicationWindow):
self.toast_overlay.add_toast(toast)
+ def _create_source_item(self, source: Source) -> SourceSidebarItem:
+ item = SourceSidebarItem(source=source)
+ item.connect(
+ "notify::visible",
+ lambda item, _: self._source_empty() if not item.props.visible else None,
+ )
+ return item
+
+ def _source_empty(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(":")
@@ -175,10 +190,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
diff --git a/data/icons/heroic-symbolic.svg b/data/icons/heroic-symbolic.svg
new file mode 100644
index 0000000..a50b119
--- /dev/null
+++ b/data/icons/heroic-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/data/icons/icons.gresource.xml.in b/data/icons/icons.gresource.xml.in
index ac3aa52..fa0ef95 100644
--- a/data/icons/icons.gresource.xml.in
+++ b/data/icons/icons.gresource.xml.in
@@ -4,6 +4,10 @@
apply-symbolic.svg
cancel-symbolic.svg
filter-symbolic.svg
+
+ heroic-symbolic.svg
+ imported-symbolic.svg
+ steam-symbolic.svg
collection-symbolic.svg
ball-symbolic.svg
diff --git a/data/icons/imported-symbolic.svg b/data/icons/imported-symbolic.svg
new file mode 100644
index 0000000..3162223
--- /dev/null
+++ b/data/icons/imported-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/data/icons/steam-symbolic.svg b/data/icons/steam-symbolic.svg
new file mode 100644
index 0000000..9826a3b
--- /dev/null
+++ b/data/icons/steam-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/po/POTFILES.in b/po/POTFILES.in
index c877ff6..661a261 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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