diff --git a/cartridges/application.py b/cartridges/application.py index 9877ec8..a0c8e7f 100644 --- a/cartridges/application.py +++ b/cartridges/application.py @@ -2,15 +2,12 @@ # SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed # SPDX-FileCopyrightText: Copyright 2025 kramo -from collections.abc import Generator, Iterable from gettext import gettext as _ from typing import override from gi.repository import Adw from cartridges import collections, games -from cartridges.games import Game -from cartridges.sources import Source, heroic, steam from .config import APP_ID, PREFIX from .ui.window import Window @@ -28,20 +25,9 @@ class Application(Adw.Application): )) self.set_accels_for_action("app.quit", ("q",)) - saved = tuple(games.load()) - new = self.import_games(steam, heroic, skip_ids={g.game_id for g in saved}) - games.model.splice(0, 0, (*saved, *new)) + games.load() collections.load() - @staticmethod - def import_games(*sources: Source, skip_ids: Iterable[str]) -> Generator[Game]: - """Import games from `sources`, skipping ones in `skip_ids`.""" - for source in sources: - try: - yield from source.get_games(skip_ids=skip_ids) - except OSError: - continue - @override def do_startup(self): Adw.Application.do_startup(self) diff --git a/cartridges/games.py b/cartridges/games.py index c7beb87..027b548 100644 --- a/cartridges/games.py +++ b/cartridges/games.py @@ -7,17 +7,16 @@ import itertools import json import os import subprocess -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Iterable from gettext import gettext as _ -from json import JSONDecodeError from pathlib import Path from shlex import quote from types import UnionType from typing import TYPE_CHECKING, Any, NamedTuple, Self, cast -from gi.repository import Gdk, Gio, GLib, GObject +from gi.repository import Gdk, Gio, GObject -from cartridges import DATA_DIR +from . import DATA_DIR if TYPE_CHECKING: from .application import Application @@ -45,8 +44,8 @@ PROPERTIES: tuple[_GameProp, ...] = ( _GameProp("version", float), ) -_GAMES_DIR = DATA_DIR / "games" -_COVERS_DIR = DATA_DIR / "covers" +GAMES_DIR = DATA_DIR / "games" +COVERS_DIR = DATA_DIR / "covers" _SPEC_VERSION = 2.0 _MANUALLY_ADDED_ID = "imported" @@ -150,8 +149,8 @@ class Game(Gio.SimpleActionGroup): """Save the game's properties to disk.""" properties = {prop.name: getattr(self, prop.name) for prop in PROPERTIES} - _GAMES_DIR.mkdir(parents=True, exist_ok=True) - path = (_GAMES_DIR / self.game_id).with_suffix(".json") + GAMES_DIR.mkdir(parents=True, exist_ok=True) + path = (GAMES_DIR / self.game_id).with_suffix(".json") with path.open(encoding="utf-8") as f: json.dump(properties, f, indent=4) @@ -182,6 +181,13 @@ class Game(Gio.SimpleActionGroup): 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] @@ -196,31 +202,4 @@ def _increment_manually_added_id() -> int: raise ValueError -def load() -> Generator[Game]: - """Load previously saved games from disk.""" - for path in _GAMES_DIR.glob("*.json"): - try: - with path.open(encoding="utf-8") as f: - data = json.load(f) - except (JSONDecodeError, UnicodeDecodeError): - continue - - try: - game = Game.from_data(data) - except TypeError: - continue - - cover_path = _COVERS_DIR / game.game_id - for ext in ".gif", ".tiff": - filename = str(cover_path.with_suffix(ext)) - try: - game.cover = Gdk.Texture.new_from_filename(filename) - except GLib.Error: - continue - else: - break - - yield game - - model = Gio.ListStore.new(Game) diff --git a/cartridges/sources/__init__.py b/cartridges/sources/__init__.py index 3f272a9..b6ccb12 100644 --- a/cartridges/sources/__init__.py +++ b/cartridges/sources/__init__.py @@ -3,7 +3,7 @@ import os import sys -from collections.abc import Generator, Iterable +from collections.abc import Generator from pathlib import Path from typing import Final, Protocol @@ -44,6 +44,17 @@ class Source(Protocol): NAME: Final[str] @staticmethod - def get_games(*, skip_ids: Iterable[str]) -> Generator[Game]: - """Installed games, except those in `skip_ids`.""" + def get_games() -> Generator[Game]: + """Installed games.""" ... + + +def get_games() -> Generator[Game]: + """Installed games from all sources.""" + from . import heroic, imported, steam + + for source in heroic, imported, steam: + try: + yield from source.get_games() + except OSError: + continue diff --git a/cartridges/sources/heroic.py b/cartridges/sources/heroic.py index fcc6dfa..5af9d85 100644 --- a/cartridges/sources/heroic.py +++ b/cartridges/sources/heroic.py @@ -111,11 +111,11 @@ class _NileSource(_StoreSource): yield entry["id"] -def get_games(*, skip_ids: Iterable[str]) -> Generator[Game]: +def get_games() -> Generator[Game]: """Installed Heroic games.""" added = int(time.time()) for source in _LegendarySource, _GOGSource, _NileSource, _SideloadSource: - yield from _games_from(source, added, skip_ids) + yield from _games_from(source, added) def _config_dir() -> Path: @@ -139,9 +139,7 @@ def _hidden_app_names() -> Generator[str]: yield game["appName"] -def _games_from( - source: type[_Source], added: int, skip_ids: Iterable[str] -) -> Generator[Game]: +def _games_from(source: type[_Source], added: int) -> Generator[Game]: try: with (_config_dir() / source.library_path()).open() as fp: library = json.load(fp) @@ -164,10 +162,6 @@ def _games_from( if (installed is not None) and (app_name not in installed): continue - game_id = f"{source_id}_{app_name}" - if game_id in skip_ids: - continue - cover_uri = f"{entry.get('art_square', '')}{source.COVER_URI_PARAMS}" cover_path = images_cache / sha256(cover_uri.encode()).hexdigest() @@ -179,7 +173,7 @@ def _games_from( yield Game( added=added, executable=f"{OPEN} heroic://launch/{entry['runner']}/{app_name}", - game_id=game_id, + game_id=f"{source_id}_{app_name}", source=source_id, hidden=app_name in hidden, name=entry["title"], diff --git a/cartridges/sources/imported.py b/cartridges/sources/imported.py new file mode 100644 index 0000000..b1093a6 --- /dev/null +++ b/cartridges/sources/imported.py @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: Copyright 2025 kramo + +import json +from collections.abc import Generator +from gettext import gettext as _ +from json import JSONDecodeError + +from gi.repository import Gdk, GLib + +from cartridges.games import COVERS_DIR, GAMES_DIR, Game + +ID, NAME = "imported", _("Added") + + +def get_games() -> Generator[Game]: + """Manually added games.""" + for path in GAMES_DIR.glob("imported_*.json"): + try: + with path.open(encoding="utf-8") as f: + data = json.load(f) + except (JSONDecodeError, UnicodeDecodeError): + continue + + try: + game = Game.from_data(data) + except TypeError: + continue + + cover_path = COVERS_DIR / game.game_id + for ext in ".gif", ".tiff": + filename = str(cover_path.with_suffix(ext)) + try: + game.cover = Gdk.Texture.new_from_filename(filename) + except GLib.Error: + continue + else: + break + + yield game diff --git a/cartridges/sources/steam.py b/cartridges/sources/steam.py index 0e8f923..65e2d78 100644 --- a/cartridges/sources/steam.py +++ b/cartridges/sources/steam.py @@ -8,7 +8,7 @@ import re import struct import time from collections import defaultdict -from collections.abc import Generator, Iterable, Sequence +from collections.abc import Generator, Sequence from contextlib import suppress from gettext import gettext as _ from os import SEEK_CUR @@ -107,7 +107,7 @@ class _AppInfo(NamedTuple): return cls(common.get("type"), developer, capsule) -def get_games(*, skip_ids: Iterable[str]) -> Generator[Game]: +def get_games() -> Generator[Game]: """Installed Steam games.""" added = int(time.time()) @@ -115,7 +115,7 @@ def get_games(*, skip_ids: Iterable[str]) -> Generator[Game]: with (_data_dir() / "appcache" / "appinfo.vdf").open("rb") as fp: appinfo = defaultdict(_AppInfo, _parse_appinfo_vdf(fp)) - appids = {i.rsplit("_", 1)[-1] for i in skip_ids if i.startswith(f"{ID}_")} + appids = set() for manifest in _manifests(): try: name, appid, stateflags, lastplayed = _App.from_manifest(manifest) diff --git a/po/POTFILES.in b/po/POTFILES.in index b609298..6cca4ad 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -4,6 +4,7 @@ cartridges/gamepads.py cartridges/games.py cartridges/sources/__init__.py cartridges/sources/heroic.py +cartridges/sources/imported.py cartridges/sources/steam.py cartridges/ui/collections.py cartridges/ui/cover.blp