sources: Split games into per-source models
This commit is contained in:
@@ -7,7 +7,7 @@ from typing import override
|
|||||||
|
|
||||||
from gi.repository import Adw
|
from gi.repository import Adw
|
||||||
|
|
||||||
from cartridges import collections, games
|
from cartridges import collections, sources
|
||||||
|
|
||||||
from .config import APP_ID, PREFIX
|
from .config import APP_ID, PREFIX
|
||||||
from .ui.window import Window
|
from .ui.window import Window
|
||||||
@@ -25,7 +25,7 @@ class Application(Adw.Application):
|
|||||||
))
|
))
|
||||||
self.set_accels_for_action("app.quit", ("<Control>q",))
|
self.set_accels_for_action("app.quit", ("<Control>q",))
|
||||||
|
|
||||||
games.load()
|
sources.load()
|
||||||
collections.load()
|
collections.load()
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ from typing import TYPE_CHECKING, Any, cast
|
|||||||
|
|
||||||
from gi.repository import Gio, GLib, GObject
|
from gi.repository import Gio, GLib, GObject
|
||||||
|
|
||||||
from cartridges import SETTINGS, games
|
from cartridges import SETTINGS
|
||||||
from cartridges.games import Game
|
|
||||||
from cartridges.sources import imported
|
from cartridges.sources import imported
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -67,31 +66,6 @@ class Collection(Gio.SimpleActionGroup):
|
|||||||
save()
|
save()
|
||||||
|
|
||||||
|
|
||||||
def _get_collections() -> Generator[Collection]:
|
|
||||||
manually_added_game_ids = {
|
|
||||||
game.game_id
|
|
||||||
for game in cast(Iterable[Game], games.model)
|
|
||||||
if game.source.startswith(imported.ID)
|
|
||||||
}
|
|
||||||
for data in SETTINGS.get_value("collections").unpack():
|
|
||||||
if data.get("removed"):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield Collection(
|
|
||||||
name=data["name"],
|
|
||||||
icon=data["icon"],
|
|
||||||
game_ids={
|
|
||||||
ident
|
|
||||||
for ident in data["game-ids"]
|
|
||||||
if not ident.startswith(imported.ID)
|
|
||||||
or ident in manually_added_game_ids
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
|
||||||
def load():
|
def load():
|
||||||
"""Load collections from GSettings."""
|
"""Load collections from GSettings."""
|
||||||
model.splice(0, 0, tuple(_get_collections()))
|
model.splice(0, 0, tuple(_get_collections()))
|
||||||
@@ -120,4 +94,24 @@ def save():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_collections() -> Generator[Collection]:
|
||||||
|
imported_ids = {p.stem for p in imported.get_paths()}
|
||||||
|
for data in SETTINGS.get_value("collections").unpack():
|
||||||
|
if data.get("removed"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield Collection(
|
||||||
|
name=data["name"],
|
||||||
|
icon=data["icon"],
|
||||||
|
game_ids={
|
||||||
|
ident
|
||||||
|
for ident in data["game-ids"]
|
||||||
|
if not ident.startswith(imported.ID) or ident in imported_ids
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
model = Gio.ListStore.new(Collection)
|
model = Gio.ListStore.new(Collection)
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel
|
||||||
|
|
||||||
import itertools
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from collections.abc import Callable, Iterable
|
from collections.abc import Callable
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from shlex import quote
|
from shlex import quote
|
||||||
@@ -48,7 +47,6 @@ GAMES_DIR = DATA_DIR / "games"
|
|||||||
COVERS_DIR = DATA_DIR / "covers"
|
COVERS_DIR = DATA_DIR / "covers"
|
||||||
|
|
||||||
_SPEC_VERSION = 2.0
|
_SPEC_VERSION = 2.0
|
||||||
_MANUALLY_ADDED_ID = "imported"
|
|
||||||
|
|
||||||
|
|
||||||
class Game(Gio.SimpleActionGroup):
|
class Game(Gio.SimpleActionGroup):
|
||||||
@@ -122,14 +120,6 @@ class Game(Gio.SimpleActionGroup):
|
|||||||
|
|
||||||
return game
|
return game
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def for_editing(cls) -> Self:
|
|
||||||
"""Create a game for the user to manually set its properties."""
|
|
||||||
return cls(
|
|
||||||
game_id=f"{_MANUALLY_ADDED_ID}_{_increment_manually_added_id()}",
|
|
||||||
source=_MANUALLY_ADDED_ID,
|
|
||||||
)
|
|
||||||
|
|
||||||
def play(self):
|
def play(self):
|
||||||
"""Run the executable command in a shell."""
|
"""Run the executable command in a shell."""
|
||||||
if Path("/.flatpak-info").exists():
|
if Path("/.flatpak-info").exists():
|
||||||
@@ -179,27 +169,3 @@ class Game(Gio.SimpleActionGroup):
|
|||||||
app = cast("Application", Gio.Application.get_default())
|
app = cast("Application", Gio.Application.get_default())
|
||||||
window = cast("Window", app.props.active_window)
|
window = cast("Window", app.props.active_window)
|
||||||
window.send_toast(title, undo=undo)
|
window.send_toast(title, undo=undo)
|
||||||
|
|
||||||
|
|
||||||
def load():
|
|
||||||
"""Populate `games.model` with all games from all sources."""
|
|
||||||
from . import sources
|
|
||||||
|
|
||||||
model.splice(0, 0, tuple(sources.get_games()))
|
|
||||||
|
|
||||||
|
|
||||||
def _increment_manually_added_id() -> int:
|
|
||||||
numbers = {
|
|
||||||
game.game_id.split("_")[1]
|
|
||||||
for game in cast(Iterable[Game], model)
|
|
||||||
if game.game_id.startswith(_MANUALLY_ADDED_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
for count in itertools.count():
|
|
||||||
if count not in numbers:
|
|
||||||
return count
|
|
||||||
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
|
|
||||||
model = Gio.ListStore.new(Game)
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import os
|
|||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator, Iterable
|
||||||
from contextlib import suppress
|
from functools import cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Final, Protocol, cast
|
from typing import Final, Protocol, cast
|
||||||
|
|
||||||
from gi.repository import GLib
|
from gi.repository import Gio, GLib, GObject
|
||||||
|
|
||||||
from cartridges.games import Game
|
from cartridges.games import Game
|
||||||
|
|
||||||
@@ -41,9 +41,7 @@ OPEN = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Source(Protocol):
|
class _SourceModule(Protocol):
|
||||||
"""A source of games to import."""
|
|
||||||
|
|
||||||
ID: Final[str]
|
ID: Final[str]
|
||||||
NAME: Final[str]
|
NAME: Final[str]
|
||||||
|
|
||||||
@@ -53,17 +51,75 @@ class Source(Protocol):
|
|||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
def get_games() -> Generator[Game]:
|
class Source(GObject.Object, Gio.ListModel): # pyright: ignore[reportIncompatibleMethodOverride]
|
||||||
"""Installed games from all sources."""
|
"""A source of games to import."""
|
||||||
|
|
||||||
|
id = GObject.Property(type=str)
|
||||||
|
name = GObject.Property(type=str)
|
||||||
|
icon_name = GObject.Property(type=str)
|
||||||
|
|
||||||
|
_module: _SourceModule
|
||||||
|
|
||||||
|
def __init__(self, module: _SourceModule, added: int):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.id, self.name, self._module = module.ID, module.NAME, module
|
||||||
|
self.bind_property(
|
||||||
|
"name",
|
||||||
|
self,
|
||||||
|
"icon-name",
|
||||||
|
GObject.BindingFlags.SYNC_CREATE,
|
||||||
|
lambda _, name: f"{name}-symbolic",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._games = list(self._get_games(added))
|
||||||
|
except OSError:
|
||||||
|
self._games = []
|
||||||
|
|
||||||
|
def do_get_item(self, position: int) -> Game | None:
|
||||||
|
"""Get the item at `position`."""
|
||||||
|
try:
|
||||||
|
return self._games[position]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def do_get_item_type(self) -> type[Game]:
|
||||||
|
"""Get the type of the items in `self`."""
|
||||||
|
return Game
|
||||||
|
|
||||||
|
def do_get_n_items(self) -> int:
|
||||||
|
"""Get the number of items in `self`."""
|
||||||
|
return len(self._games)
|
||||||
|
|
||||||
|
def append(self, game: Game):
|
||||||
|
"""Append `game` to `self`."""
|
||||||
|
pos = len(self._games)
|
||||||
|
self._games.append(game)
|
||||||
|
self.items_changed(pos, 0, 1)
|
||||||
|
|
||||||
|
def _get_games(self, added: int) -> Generator[Game]:
|
||||||
|
for game in self._module.get_games():
|
||||||
|
game.added = game.added or added
|
||||||
|
yield game
|
||||||
|
|
||||||
|
|
||||||
|
def load():
|
||||||
|
"""Populate `sources.model` with all sources."""
|
||||||
|
model.splice(0, 0, tuple(_get_sources()))
|
||||||
|
|
||||||
|
|
||||||
|
@cache
|
||||||
|
def get(ident: str) -> Source:
|
||||||
|
"""Get the source with `ident`."""
|
||||||
|
return next(s for s in cast(Iterable[Source], model) if s.id == ident)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sources() -> Generator[Source]:
|
||||||
added = int(time.time())
|
added = int(time.time())
|
||||||
for source in all_sources():
|
for info in pkgutil.iter_modules(__path__, prefix="."):
|
||||||
with suppress(OSError):
|
module = cast(_SourceModule, importlib.import_module(info.name, __package__))
|
||||||
for game in source.get_games():
|
yield Source(module, added)
|
||||||
game.added = game.added or added
|
|
||||||
yield game
|
|
||||||
|
|
||||||
|
|
||||||
def all_sources() -> Generator[Source]:
|
model = Gio.ListStore.new(Source)
|
||||||
"""All sources of games."""
|
|
||||||
for module in pkgutil.iter_modules(__path__, prefix="."):
|
|
||||||
yield cast(Source, importlib.import_module(module.name, __package__))
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
# SPDX-FileCopyrightText: Copyright 2025 kramo
|
||||||
|
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from gettext import gettext as _
|
from gettext import gettext as _
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from gi.repository import Gdk, GLib
|
from gi.repository import Gdk, GLib
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ ID, NAME = "imported", _("Added")
|
|||||||
|
|
||||||
def get_games() -> Generator[Game]:
|
def get_games() -> Generator[Game]:
|
||||||
"""Manually added games."""
|
"""Manually added games."""
|
||||||
for path in GAMES_DIR.glob("imported_*.json"):
|
for path in get_paths():
|
||||||
try:
|
try:
|
||||||
with path.open(encoding="utf-8") as f:
|
with path.open(encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
@@ -38,3 +40,15 @@ def get_games() -> Generator[Game]:
|
|||||||
break
|
break
|
||||||
|
|
||||||
yield game
|
yield game
|
||||||
|
|
||||||
|
|
||||||
|
def new() -> Game:
|
||||||
|
"""Create a new game for the user to manually set its properties."""
|
||||||
|
numbers = {int(p.stem.rsplit("_", 1)[1]) for p in get_paths()}
|
||||||
|
number = next(i for i in itertools.count() if i not in numbers)
|
||||||
|
return Game(game_id=f"{ID}_{number}", source=ID)
|
||||||
|
|
||||||
|
|
||||||
|
def get_paths() -> Generator[Path]:
|
||||||
|
"""Get the paths of all imported games on disk."""
|
||||||
|
yield from GAMES_DIR.glob("imported_*.json")
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ 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
|
||||||
|
|
||||||
from cartridges import games
|
from cartridges import games, sources
|
||||||
from cartridges.config import PREFIX
|
from cartridges.config import PREFIX
|
||||||
from cartridges.games import Game
|
from cartridges.games import Game
|
||||||
|
from cartridges.sources import imported
|
||||||
|
|
||||||
from .collections import CollectionsBox
|
from .collections import CollectionsBox
|
||||||
from .cover import Cover # noqa: F401
|
from .cover import Cover # noqa: F401
|
||||||
@@ -124,7 +125,7 @@ class GameDetails(Adw.NavigationPage):
|
|||||||
|
|
||||||
if not self.game.added:
|
if not self.game.added:
|
||||||
self.game.added = int(time.time())
|
self.game.added = int(time.time())
|
||||||
games.model.append(self.game)
|
sources.get(imported.ID).append(self.game)
|
||||||
|
|
||||||
self.stack.props.visible_child_name = "details"
|
self.stack.props.visible_child_name = "details"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any, override
|
|||||||
|
|
||||||
from gi.repository import Gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
from cartridges import STATE_SETTINGS
|
from cartridges import STATE_SETTINGS, sources
|
||||||
from cartridges.games import Game
|
from cartridges.games import Game
|
||||||
|
|
||||||
_SORT_MODES = {
|
_SORT_MODES = {
|
||||||
@@ -19,6 +19,8 @@ _SORT_MODES = {
|
|||||||
"oldest": ("added", False),
|
"oldest": ("added", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model = Gtk.FlattenListModel.new(sources.model)
|
||||||
|
|
||||||
|
|
||||||
class GameSorter(Gtk.Sorter):
|
class GameSorter(Gtk.Sorter):
|
||||||
"""A sorter for game objects.
|
"""A sorter for game objects.
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ template $Window: Adw.ApplicationWindow {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
model: bind template.games;
|
model: bind template.model;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ from typing import Any, TypeVar, cast
|
|||||||
|
|
||||||
from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
from gi.repository import Adw, Gio, GLib, GObject, Gtk
|
||||||
|
|
||||||
from cartridges import STATE_SETTINGS, games
|
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.games import Game
|
from cartridges.sources import imported
|
||||||
from cartridges.ui import collections
|
from cartridges.ui import collections, games
|
||||||
|
|
||||||
from .collection_details import CollectionDetails
|
from .collection_details import CollectionDetails
|
||||||
from .collections import CollectionFilter, CollectionSidebarItem
|
from .collections import CollectionFilter, CollectionSidebarItem
|
||||||
@@ -61,6 +61,8 @@ class Window(Adw.ApplicationWindow):
|
|||||||
collection_filter: CollectionFilter = Gtk.Template.Child()
|
collection_filter: CollectionFilter = Gtk.Template.Child()
|
||||||
details: GameDetails = Gtk.Template.Child()
|
details: GameDetails = Gtk.Template.Child()
|
||||||
|
|
||||||
|
model = GObject.Property(type=Gio.ListModel, default=games.model)
|
||||||
|
|
||||||
search_text = GObject.Property(type=str)
|
search_text = GObject.Property(type=str)
|
||||||
show_hidden = GObject.Property(type=bool, default=False)
|
show_hidden = GObject.Property(type=bool, default=False)
|
||||||
|
|
||||||
@@ -70,11 +72,6 @@ class Window(Adw.ApplicationWindow):
|
|||||||
_collection_removed_signal: int | None = None
|
_collection_removed_signal: int | None = None
|
||||||
_selected_sidebar_item = 0
|
_selected_sidebar_item = 0
|
||||||
|
|
||||||
@GObject.Property(type=Gio.ListStore)
|
|
||||||
def games(self) -> Gio.ListStore:
|
|
||||||
"""Model of the user's games."""
|
|
||||||
return games.model
|
|
||||||
|
|
||||||
@GObject.Property(type=Collection)
|
@GObject.Property(type=Collection)
|
||||||
def collection(self) -> Collection | None:
|
def collection(self) -> Collection | None:
|
||||||
"""The currently selected collection."""
|
"""The currently selected collection."""
|
||||||
@@ -257,7 +254,7 @@ class Window(Adw.ApplicationWindow):
|
|||||||
self.details.edit()
|
self.details.edit()
|
||||||
|
|
||||||
def _add(self):
|
def _add(self):
|
||||||
self.details.game = Game.for_editing()
|
self.details.game = imported.new()
|
||||||
|
|
||||||
if self.navigation_view.props.visible_page_tag != "details":
|
if self.navigation_view.props.visible_page_tag != "details":
|
||||||
self.navigation_view.push_by_tag("details")
|
self.navigation_view.push_by_tag("details")
|
||||||
|
|||||||
Reference in New Issue
Block a user