Compare commits
6 Commits
rewrite-si
...
rewrite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5cfa476ff | ||
|
|
7bc9d6aee9 | ||
|
|
00795b83fd | ||
|
|
f9cb794394 | ||
|
|
1aee234cbf | ||
|
|
21588fe92b |
@@ -19,6 +19,11 @@ class Application(Adw.Application):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(application_id=APP_ID)
|
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((
|
self.add_action_entries((
|
||||||
("quit", lambda *_: self.quit()),
|
("quit", lambda *_: self.quit()),
|
||||||
("about", lambda *_: self._present_about_dialog()),
|
("about", lambda *_: self._present_about_dialog()),
|
||||||
@@ -28,11 +33,6 @@ class Application(Adw.Application):
|
|||||||
sources.load()
|
sources.load()
|
||||||
collections.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
|
@override
|
||||||
def do_activate(self):
|
def do_activate(self):
|
||||||
window = self.props.active_window or Window(application=self)
|
window = self.props.active_window or Window(application=self)
|
||||||
|
|||||||
@@ -112,11 +112,6 @@ class Gamepad(GObject.Object):
|
|||||||
focus_widget.activate()
|
focus_widget.activate()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.window.sidebar.get_focus_child():
|
|
||||||
selected_item = self.window.sidebar.get_selected_item()
|
|
||||||
self.window.navigate_sidebar(self.window.sidebar, item=selected_item)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.window.grid.activate_action(
|
self.window.grid.activate_action(
|
||||||
"list.activate-item",
|
"list.activate-item",
|
||||||
GLib.Variant.new_uint32(self._get_current_position()),
|
GLib.Variant.new_uint32(self._get_current_position()),
|
||||||
@@ -141,20 +136,7 @@ class Gamepad(GObject.Object):
|
|||||||
open_menu.grab_focus()
|
open_menu.grab_focus()
|
||||||
return
|
return
|
||||||
|
|
||||||
grid_visible = self.window.view_stack.props.visible_child_name == "grid"
|
self.window.grid.grab_focus()
|
||||||
if self._is_focused_on_top_bar():
|
|
||||||
focus_widget = self.window.grid if grid_visible else self.window.sidebar
|
|
||||||
|
|
||||||
# If the grid is not visible (i.e no search results or imports)
|
|
||||||
# the searchbar is focused as a fallback.
|
|
||||||
fallback_child = (
|
|
||||||
self.window.grid
|
|
||||||
if self.window.sidebar.get_focus_child()
|
|
||||||
else self.window.sidebar
|
|
||||||
)
|
|
||||||
focus_widget = fallback_child if grid_visible else self.window.search_entry
|
|
||||||
|
|
||||||
focus_widget.grab_focus()
|
|
||||||
self.window.props.focus_visible = True
|
self.window.props.focus_visible = True
|
||||||
|
|
||||||
def _navigate_to_game_position(self, new_pos: int):
|
def _navigate_to_game_position(self, new_pos: int):
|
||||||
@@ -165,67 +147,24 @@ class Gamepad(GObject.Object):
|
|||||||
|
|
||||||
def _move_horizontally(self, direction: Gtk.DirectionType):
|
def _move_horizontally(self, direction: Gtk.DirectionType):
|
||||||
if self._is_focused_on_top_bar():
|
if self._is_focused_on_top_bar():
|
||||||
if self.window.header_bar.child_focus(direction):
|
if not self.window.header_bar.child_focus(direction):
|
||||||
self.window.props.focus_visible = True
|
|
||||||
return
|
|
||||||
|
|
||||||
# The usual behaviour of child_focus() on the header bar on the
|
|
||||||
# left will result in the above child focus to fail, so
|
|
||||||
# we need to manually check the user is going left to then focus the
|
|
||||||
# sidebar.
|
|
||||||
|
|
||||||
if direction is not self._get_rtl_direction(
|
|
||||||
Gtk.DirectionType.RIGHT, Gtk.DirectionType.LEFT
|
|
||||||
):
|
|
||||||
self.window.header_bar.keynav_failed(direction)
|
self.window.header_bar.keynav_failed(direction)
|
||||||
return
|
|
||||||
|
|
||||||
self.window.sidebar.grab_focus()
|
|
||||||
self.window.props.focus_visible = True
|
self.window.props.focus_visible = True
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.window.sidebar.get_focus_child():
|
|
||||||
# The usual behaviour of child_focus() on the sidebar
|
|
||||||
# would result in the + button being focused, instead of the grid
|
|
||||||
# so we need to grab the focus of the grid if the user inputs the
|
|
||||||
# corresponding direction to the grid.
|
|
||||||
|
|
||||||
grid_direction = self._get_rtl_direction(
|
|
||||||
Gtk.DirectionType.LEFT, Gtk.DirectionType.RIGHT
|
|
||||||
)
|
|
||||||
|
|
||||||
# Focus the first game when re-entering from sidebar
|
|
||||||
if direction is grid_direction:
|
|
||||||
self.window.grid.scroll_to(0, Gtk.ListScrollFlags.FOCUS, None)
|
|
||||||
self.window.grid.grab_focus()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.window.sidebar.keynav_failed(direction)
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._can_navigate_games_page():
|
if self._can_navigate_games_page():
|
||||||
if not self._get_focused_game():
|
self._navigate_to_game_position(
|
||||||
return
|
self._get_current_position()
|
||||||
|
+ (
|
||||||
new_pos = self._get_current_position() + (
|
-1
|
||||||
-1
|
if direction
|
||||||
if direction
|
== self._get_rtl_direction(
|
||||||
== self._get_rtl_direction(
|
Gtk.DirectionType.RIGHT, Gtk.DirectionType.LEFT
|
||||||
Gtk.DirectionType.RIGHT, Gtk.DirectionType.LEFT
|
)
|
||||||
|
else 1
|
||||||
)
|
)
|
||||||
else 1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the user is focused on the first game and tries to go
|
|
||||||
# back another game, instead of failing, the focus should
|
|
||||||
# change to the sidebar.
|
|
||||||
|
|
||||||
if new_pos < 0:
|
|
||||||
self.window.sidebar.grab_focus()
|
|
||||||
self.window.props.focus_visible = True
|
|
||||||
return
|
|
||||||
|
|
||||||
self._navigate_to_game_position(new_pos)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.window.navigation_view.props.visible_page_tag == "details":
|
if self.window.navigation_view.props.visible_page_tag == "details":
|
||||||
@@ -245,14 +184,6 @@ class Gamepad(GObject.Object):
|
|||||||
self.window.grid.grab_focus()
|
self.window.grid.grab_focus()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.window.sidebar.get_focus_child():
|
|
||||||
if self.window.sidebar.child_focus(direction):
|
|
||||||
self.window.props.focus_visible = True
|
|
||||||
return
|
|
||||||
|
|
||||||
self.window.sidebar.keynav_failed(direction)
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._can_navigate_games_page():
|
if self._can_navigate_games_page():
|
||||||
if not (game := self._get_focused_game()):
|
if not (game := self._get_focused_game()):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ class _SourceModule(Protocol):
|
|||||||
class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatibleMethodOverride]
|
class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
"""A source of games to import."""
|
"""A source of games to import."""
|
||||||
|
|
||||||
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
id = GObject.Property(type=str)
|
id = GObject.Property(type=str)
|
||||||
name = GObject.Property(type=str)
|
name = GObject.Property(type=str)
|
||||||
icon_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.id, self.name, self._module = module.ID, module.NAME, module
|
||||||
self.bind_property(
|
self.bind_property(
|
||||||
"name",
|
"id",
|
||||||
self,
|
self,
|
||||||
"icon-name",
|
"icon-name",
|
||||||
GObject.BindingFlags.SYNC_CREATE,
|
GObject.BindingFlags.SYNC_CREATE,
|
||||||
lambda _, name: f"{name}-symbolic",
|
lambda _, ident: f"{ident}-symbolic",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
||||||
|
|
||||||
from itertools import product
|
from itertools import product
|
||||||
from typing import Any, NamedTuple, TypeVar, cast
|
from typing import Any, NamedTuple, cast
|
||||||
|
|
||||||
from gi.repository import Adw, Gio, GObject, Gtk
|
from gi.repository import Adw, Gio, GObject, Gtk
|
||||||
|
|
||||||
@@ -40,8 +40,6 @@ _ICONS = (
|
|||||||
_Icon("fist", "✊"),
|
_Icon("fist", "✊"),
|
||||||
)
|
)
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
|
||||||
|
|
||||||
|
|
||||||
@Gtk.Template.from_resource(f"{PREFIX}/collection-details.ui")
|
@Gtk.Template.from_resource(f"{PREFIX}/collection-details.ui")
|
||||||
class CollectionDetails(Adw.Dialog):
|
class CollectionDetails(Adw.Dialog):
|
||||||
@@ -132,9 +130,9 @@ class CollectionDetails(Adw.Dialog):
|
|||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@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
|
return first or second
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@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
|
return first if condition else second
|
||||||
|
|||||||
@@ -5,27 +5,27 @@ template $Cover: Adw.Bin {
|
|||||||
child: Adw.Clamp {
|
child: Adw.Clamp {
|
||||||
orientation: vertical;
|
orientation: vertical;
|
||||||
unit: px;
|
unit: px;
|
||||||
maximum-size: bind _picture.height-request;
|
maximum-size: bind template.height;
|
||||||
tightening-threshold: bind _picture.height-request;
|
tightening-threshold: bind template.height;
|
||||||
|
|
||||||
child: Adw.Clamp {
|
child: Adw.Clamp {
|
||||||
unit: px;
|
unit: px;
|
||||||
maximum-size: bind _picture.width-request;
|
maximum-size: bind template.width;
|
||||||
tightening-threshold: bind _picture.width-request;
|
tightening-threshold: bind template.width;
|
||||||
|
|
||||||
child: Adw.ViewStack {
|
child: Adw.ViewStack {
|
||||||
name: "cover";
|
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;
|
overflow: hidden;
|
||||||
enable-transitions: true;
|
enable-transitions: true;
|
||||||
|
|
||||||
Adw.ViewStackPage {
|
Adw.ViewStackPage {
|
||||||
name: "cover";
|
name: "cover";
|
||||||
|
|
||||||
child: Picture _picture {
|
child: Picture {
|
||||||
paintable: bind template.paintable;
|
paintable: bind template.paintable;
|
||||||
width-request: 200;
|
width-request: bind template.width;
|
||||||
height-request: 300;
|
height-request: bind template.height;
|
||||||
content-fit: cover;
|
content-fit: cover;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,10 @@ template $Cover: Adw.Bin {
|
|||||||
name: "icon";
|
name: "icon";
|
||||||
|
|
||||||
child: Image {
|
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;
|
pixel-size: 80;
|
||||||
|
|
||||||
styles [
|
styles [
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from gi.repository import Adw, Gdk, GObject, Gtk
|
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")
|
@Gtk.Template.from_resource(f"{PREFIX}/cover.ui")
|
||||||
@@ -12,12 +12,14 @@ class Cover(Adw.Bin):
|
|||||||
|
|
||||||
__gtype_name__ = __qualname__
|
__gtype_name__ = __qualname__
|
||||||
|
|
||||||
picture = GObject.Property(lambda self: self._picture, type=Gtk.Picture)
|
|
||||||
paintable = GObject.Property(type=Gdk.Paintable)
|
paintable = GObject.Property(type=Gdk.Paintable)
|
||||||
app_icon_name = GObject.Property(type=str, default=f"{APP_ID}-symbolic")
|
width = GObject.Property(type=int, default=200)
|
||||||
|
height = GObject.Property(type=int, default=300)
|
||||||
_picture = Gtk.Template.Child()
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _get_stack_child(self, _obj, paintable: Gdk.Paintable | None) -> str:
|
def _if_else[T](self, _obj, condition: object, first: T, second: T) -> T:
|
||||||
return "cover" if paintable else "icon"
|
return first if condition else second
|
||||||
|
|
||||||
|
@Gtk.Template.Callback()
|
||||||
|
def _concat(self, _obj, *strings: str) -> str:
|
||||||
|
return "".join(strings)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ template $GameItem: Box {
|
|||||||
|
|
||||||
Adw.Clamp {
|
Adw.Clamp {
|
||||||
unit: px;
|
unit: px;
|
||||||
maximum-size: bind cover.picture as <Picture>.width-request;
|
maximum-size: bind cover.width;
|
||||||
tightening-threshold: bind cover.picture as <Picture>.width-request;
|
tightening-threshold: bind cover.width;
|
||||||
|
|
||||||
child: Overlay {
|
child: Overlay {
|
||||||
child: $Cover cover {
|
child: $Cover cover {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from typing import Any, TypeVar, cast
|
from typing import Any, cast
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
|
from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
|
||||||
@@ -26,8 +26,6 @@ _REQUIRED_PROPERTIES = {
|
|||||||
prop.name for prop in games.PROPERTIES if prop.editable and prop.required
|
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")
|
@Gtk.Template.from_resource(f"{PREFIX}/game-details.ui")
|
||||||
class GameDetails(Adw.NavigationPage):
|
class GameDetails(Adw.NavigationPage):
|
||||||
@@ -148,11 +146,11 @@ class GameDetails(Adw.NavigationPage):
|
|||||||
self.collections_box.finish()
|
self.collections_box.finish()
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@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
|
return first or second
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@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
|
return first if condition else second
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
|
|||||||
@@ -19,7 +19,24 @@ _SORT_MODES = {
|
|||||||
"oldest": ("added", False),
|
"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):
|
class GameSorter(Gtk.Sorter):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ python.install_sources(
|
|||||||
'game_details.py',
|
'game_details.py',
|
||||||
'game_item.py',
|
'game_item.py',
|
||||||
'games.py',
|
'games.py',
|
||||||
|
'sources.py',
|
||||||
'window.py',
|
'window.py',
|
||||||
),
|
),
|
||||||
subdir: 'cartridges' / 'ui',
|
subdir: 'cartridges' / 'ui',
|
||||||
|
|||||||
44
cartridges/ui/sources.py
Normal file
44
cartridges/ui/sources.py
Normal file
@@ -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")),
|
||||||
|
)
|
||||||
@@ -94,6 +94,10 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Adw.SidebarSection sources {
|
||||||
|
title: _("Sources");
|
||||||
|
}
|
||||||
|
|
||||||
Adw.SidebarSection collections {
|
Adw.SidebarSection collections {
|
||||||
title: _("Collections");
|
title: _("Collections");
|
||||||
|
|
||||||
@@ -208,7 +212,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
content: Adw.ToastOverlay toast_overlay {
|
content: Adw.ToastOverlay toast_overlay {
|
||||||
child: Adw.ViewStack view_stack {
|
child: Adw.ViewStack {
|
||||||
visible-child-name: bind $_if_else(
|
visible-child-name: bind $_if_else(
|
||||||
grid.model as <NoSelection>.n-items,
|
grid.model as <NoSelection>.n-items,
|
||||||
"grid",
|
"grid",
|
||||||
@@ -262,16 +266,6 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
expression: expr item as <$Game>.hidden;
|
expression: expr item as <$Game>.hidden;
|
||||||
invert: bind template.show-hidden inverted;
|
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;
|
model: bind template.model;
|
||||||
|
|||||||
@@ -6,21 +6,22 @@
|
|||||||
import sys
|
import sys
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from gettext import gettext as _
|
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
|
from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import STATE_SETTINGS
|
from cartridges import STATE_SETTINGS
|
||||||
from cartridges.collections import Collection
|
from cartridges.collections import Collection
|
||||||
from cartridges.config import PREFIX, PROFILE
|
from cartridges.config import PREFIX, PROFILE
|
||||||
from cartridges.sources import imported
|
from cartridges.sources import Source, imported
|
||||||
from cartridges.ui import collections, games
|
from cartridges.ui import collections, games, sources
|
||||||
|
|
||||||
from .collection_details import CollectionDetails
|
from .collection_details import CollectionDetails
|
||||||
from .collections import CollectionFilter, CollectionSidebarItem
|
from .collections import CollectionFilter, 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
|
||||||
|
from .sources import SourceSidebarItem
|
||||||
|
|
||||||
if sys.platform.startswith("linux"):
|
if sys.platform.startswith("linux"):
|
||||||
from cartridges import gamepads
|
from cartridges import gamepads
|
||||||
@@ -34,7 +35,6 @@ SORT_MODES = {
|
|||||||
"oldest": ("added", True),
|
"oldest": ("added", True),
|
||||||
}
|
}
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
|
||||||
type _UndoFunc = Callable[[], Any]
|
type _UndoFunc = Callable[[], Any]
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +46,7 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
split_view: Adw.OverlaySplitView = Gtk.Template.Child()
|
split_view: Adw.OverlaySplitView = Gtk.Template.Child()
|
||||||
sidebar: Adw.Sidebar = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
|
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]
|
collections: Adw.SidebarSection = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
|
||||||
new_collection_item: Adw.SidebarItem = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
|
new_collection_item: Adw.SidebarItem = Gtk.Template.Child() # pyright: ignore[reportAttributeAccessIssue]
|
||||||
collection_menu: Gio.Menu = Gtk.Template.Child()
|
collection_menu: Gio.Menu = Gtk.Template.Child()
|
||||||
@@ -56,7 +57,6 @@ class Window(Adw.ApplicationWindow):
|
|||||||
sort_button: Gtk.MenuButton = Gtk.Template.Child()
|
sort_button: Gtk.MenuButton = Gtk.Template.Child()
|
||||||
main_menu_button: Gtk.MenuButton = Gtk.Template.Child()
|
main_menu_button: Gtk.MenuButton = Gtk.Template.Child()
|
||||||
toast_overlay: Adw.ToastOverlay = Gtk.Template.Child()
|
toast_overlay: Adw.ToastOverlay = Gtk.Template.Child()
|
||||||
view_stack: Adw.ViewStack = 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()
|
collection_filter: CollectionFilter = Gtk.Template.Child()
|
||||||
@@ -110,6 +110,7 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
|
# https://gitlab.gnome.org/GNOME/gtk/-/issues/7901
|
||||||
self.search_entry.set_key_capture_widget(self)
|
self.search_entry.set_key_capture_widget(self)
|
||||||
|
self.sources.bind_model(sources.model, self._create_source_item)
|
||||||
self.collections.bind_model(
|
self.collections.bind_model(
|
||||||
collections.model,
|
collections.model,
|
||||||
lambda collection: CollectionSidebarItem(collection=collection),
|
lambda collection: CollectionSidebarItem(collection=collection),
|
||||||
@@ -164,26 +165,17 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
self.toast_overlay.add_toast(toast)
|
self.toast_overlay.add_toast(toast)
|
||||||
|
|
||||||
def navigate_sidebar(self, sidebar: Adw.Sidebar, item: Adw.SidebarItem): # pyright: ignore[reportAttributeAccessIssue]
|
def _create_source_item(self, source: Source) -> SourceSidebarItem:
|
||||||
"""Select given item in the sidebar.
|
item = SourceSidebarItem(source=source)
|
||||||
|
item.connect(
|
||||||
|
"notify::visible",
|
||||||
|
lambda item, _: self._source_empty() if not item.props.visible else None,
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
Item should correspond to a collection, or the all game category/
|
def _source_empty(self):
|
||||||
new collection buttons.
|
self.model = games.model
|
||||||
"""
|
self.sidebar.props.selected = 0
|
||||||
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 = sidebar.props.selected
|
|
||||||
|
|
||||||
if self.split_view.props.collapsed:
|
|
||||||
self.split_view.props.show_sidebar = False
|
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _show_sidebar_title(self, _obj, layout: str) -> bool:
|
def _show_sidebar_title(self, _obj, layout: str) -> bool:
|
||||||
@@ -192,7 +184,27 @@ class Window(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _navigate(self, sidebar: Adw.Sidebar, index: int): # pyright: ignore[reportAttributeAccessIssue]
|
def _navigate(self, sidebar: Adw.Sidebar, index: int): # pyright: ignore[reportAttributeAccessIssue]
|
||||||
self.navigate_sidebar(sidebar, sidebar.get_item(index))
|
item = sidebar.get_item(index)
|
||||||
|
|
||||||
|
match item:
|
||||||
|
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
|
||||||
|
|
||||||
|
if self.split_view.props.collapsed:
|
||||||
|
self.split_view.props.show_sidebar = False
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
def _update_selection(self, sidebar: Adw.Sidebar, *_args): # pyright: ignore[reportAttributeAccessIssue]
|
def _update_selection(self, sidebar: Adw.Sidebar, *_args): # pyright: ignore[reportAttributeAccessIssue]
|
||||||
@@ -221,7 +233,7 @@ class Window(Adw.ApplicationWindow):
|
|||||||
gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable]
|
gamepads.setup_monitor() # pyright: ignore[reportPossiblyUnboundVariable]
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@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
|
return first if condition else second
|
||||||
|
|
||||||
@Gtk.Template.Callback()
|
@Gtk.Template.Callback()
|
||||||
|
|||||||
1
data/icons/heroic-symbolic.svg
Normal file
1
data/icons/heroic-symbolic.svg
Normal 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 |
@@ -4,6 +4,10 @@
|
|||||||
<file>apply-symbolic.svg</file>
|
<file>apply-symbolic.svg</file>
|
||||||
<file>cancel-symbolic.svg</file>
|
<file>cancel-symbolic.svg</file>
|
||||||
<file>filter-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 -->
|
<!-- Categories -->
|
||||||
<file>collection-symbolic.svg</file>
|
<file>collection-symbolic.svg</file>
|
||||||
<file>ball-symbolic.svg</file>
|
<file>ball-symbolic.svg</file>
|
||||||
|
|||||||
1
data/icons/imported-symbolic.svg
Normal file
1
data/icons/imported-symbolic.svg
Normal 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 |
1
data/icons/steam-symbolic.svg
Normal file
1
data/icons/steam-symbolic.svg
Normal 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 |
@@ -17,6 +17,7 @@ cartridges/ui/game_details.py
|
|||||||
cartridges/ui/game_item.py
|
cartridges/ui/game_item.py
|
||||||
cartridges/ui/games.py
|
cartridges/ui/games.py
|
||||||
cartridges/ui/shortcuts-dialog.blp
|
cartridges/ui/shortcuts-dialog.blp
|
||||||
|
cartridges/ui/sources.py
|
||||||
cartridges/ui/window.blp
|
cartridges/ui/window.blp
|
||||||
cartridges/ui/window.py
|
cartridges/ui/window.py
|
||||||
data/page.kramo.Cartridges.desktop.in
|
data/page.kramo.Cartridges.desktop.in
|
||||||
|
|||||||
Reference in New Issue
Block a user