Merge pull request #139 from kra-mo/store-improvements

Store improvements
This commit is contained in:
kramo
2023-07-07 18:27:42 +02:00
committed by GitHub
43 changed files with 2311 additions and 1744 deletions

View File

@@ -81,6 +81,7 @@ class DetailsWindow(Adw.Window):
image_filter = Gtk.FileFilter(name=_("Images"))
for extension in Image.registered_extensions():
image_filter.add_suffix(extension[1:])
image_filter.add_suffix("svg") # Gdk.Texture supports .svg but PIL doesn't
file_filters = Gio.ListStore.new(Gtk.FileFilter)
file_filters.append(image_filter)
@@ -154,9 +155,10 @@ class DetailsWindow(Adw.Window):
return
# Increment the number after the game id (eg. imported_1, imported_2)
source_id = "imported"
numbers = [0]
game_id: str
for game_id in shared.store.games:
for game_id in shared.source_games[source_id]:
prefix = "imported_"
if not game_id.startswith(prefix):
continue
@@ -167,7 +169,7 @@ class DetailsWindow(Adw.Window):
{
"game_id": f"imported_{game_number}",
"hidden": False,
"source": "imported",
"source": source_id,
"added": int(time()),
}
)

View File

@@ -61,8 +61,8 @@ class GameCover:
if path:
if path.suffix == ".gif":
task = Gio.Task.new()
task.run_in_thread(self.create_func(self.path))
self.task = Gio.Task.new()
self.task.run_in_thread(self.create_func(self.path))
else:
self.texture = Gdk.Texture.new_from_filename(str(path))
@@ -108,6 +108,8 @@ class GameCover:
self.pictures.add(picture)
if not self.animation:
self.set_texture(self.texture)
else:
self.update_animation((self.task, self.animation))
def set_texture(self, texture):
self.pictures.discard(
@@ -131,5 +133,3 @@ class GameCover:
self.update_animation,
data,
)
else:
data[0].return_value(False)

View File

@@ -91,6 +91,8 @@ class Importer(ErrorProducer):
def run(self):
"""Use several Gio.Task to import games from added sources"""
shared.win.get_application().lookup_action("import").set_enabled(False)
self.create_dialog()
# Collect all errors and reset the cancellables for the managers
@@ -221,6 +223,7 @@ class Importer(ErrorProducer):
self.import_dialog.close()
self.summary_toast = self.create_summary_toast()
self.create_error_dialog()
shared.win.get_application().lookup_action("import").set_enabled(True)
def create_error_dialog(self):
"""Dialog containing all errors raised by importers"""

View File

@@ -84,7 +84,7 @@ class HeroicSourceIterator(SourceIterator):
"source": self.source.id,
"added": added_time,
"name": entry["title"],
"developer": entry["developer"],
"developer": entry.get("developer", None),
"game_id": self.source.game_id_format.format(
service=service, game_id=app_name
),
@@ -106,14 +106,16 @@ class HeroicSourceIterator(SourceIterator):
def generator_builder(self) -> SourceIterationResult:
"""Generator method producing games from all the Heroic sub-sources"""
for sub_source in self.sub_sources.values():
for sub_source_name, sub_source in self.sub_sources.items():
# Skip disabled sub-sources
if not shared.schema.get_boolean("heroic-import-" + sub_source["service"]):
continue
# Load games from JSON
file = self.source.config_location.root.joinpath(*sub_source["path"])
try:
library = json.load(file.open())["library"]
contents = json.load(file.open())
key = "library" if sub_source_name == "legendary" else "games"
library = contents[key]
except (JSONDecodeError, OSError, KeyError):
# Invalid library.json file, skip it
logging.warning("Couldn't open Heroic file: %s", str(file))
@@ -124,9 +126,11 @@ class HeroicSourceIterator(SourceIterator):
for entry in library:
try:
result = self.game_from_library_entry(entry, added_time)
except KeyError:
except KeyError as error:
# Skip invalid games
logging.warning("Invalid Heroic game skipped in %s", str(file))
logging.warning(
"Invalid Heroic game skipped in %s", str(file), exc_info=error
)
continue
yield result

View File

@@ -51,7 +51,7 @@ class LegendarySourceIterator(SourceIterator):
data = {}
# Get additional metadata from file (optional)
metadata_file = self.source.data_location["metadata"] / f"{app_name}.json"
metadata_file = self.source.config_location["metadata"] / f"{app_name}.json"
try:
metadata = json.load(metadata_file.open())
values["developer"] = metadata["metadata"]["developer"]
@@ -67,7 +67,7 @@ class LegendarySourceIterator(SourceIterator):
def generator_builder(self) -> Generator[SourceIterationResult, None, None]:
# Open library
file = self.source.data_location["installed.json"]
file = self.source.config_location["installed.json"]
try:
library: dict = json.load(file.open())
except (JSONDecodeError, OSError):

View File

@@ -70,9 +70,9 @@ class LutrisSourceIterator(SourceIterator):
"name": row[1],
"source": f"{self.source.id}_{row[3]}",
"game_id": self.source.game_id_format.format(
game_id=row[2], game_internal_id=row[0]
runner=row[3], game_id=row[0]
),
"executable": self.source.executable_format.format(game_id=row[2]),
"executable": self.source.executable_format.format(game_id=row[0]),
}
game = Game(values)
@@ -123,4 +123,4 @@ class LutrisSource(URLExecutableSource):
@property
def game_id_format(self):
return super().game_id_format + "_{game_internal_id}"
return self.id + "_{runner}_{game_id}"

View File

@@ -118,9 +118,9 @@ class SteamSource(URLExecutableSource):
data_location = Location(
schema_key="steam-location",
candidates=(
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
shared.home / ".steam" / "steam",
shared.data_dir / "Steam",
shared.home / ".steam",
shared.flatpak_dir / "com.valvesoftware.Steam" / "data" / "Steam",
shared.programfiles32_dir / "Steam",
),
paths={

View File

@@ -93,7 +93,7 @@ class CartridgesApplication(Adw.Application):
shared.store.add_manager(SteamAPIManager())
shared.store.add_manager(OnlineCoverManager())
shared.store.add_manager(SGDBManager())
shared.store.enable_manager_in_pipelines(FileManager)
shared.store.toggle_manager_in_pipelines(FileManager, True)
# Create actions
self.create_actions(
@@ -179,6 +179,7 @@ class CartridgesApplication(Adw.Application):
# Translators: Replace this with your name for it to show up in the about window
translator_credits=_("translator_credits"),
debug_info=debug_str,
debug_info_filename="cartridges.log",
)
about.present()

View File

@@ -212,10 +212,9 @@ class PreferencesWindow(Adw.PreferencesWindow):
self.toast.dismiss()
def remove_all_games(self, *_args):
for game in shared.store.games.values():
for game in shared.store:
if not game.removed:
self.removed_games.add(game)
game.removed = True
game.save()
game.update()
@@ -343,14 +342,17 @@ class PreferencesWindow(Adw.PreferencesWindow):
# Bad picked location, inform user
else:
if location_name == "cache":
title = _("Invalid Directory")
# The variable is the name of the source
subtitle_format = _("Select the {} cache directory.")
else:
title = _("Invalid Directory")
# The variable is the name of the source
subtitle_format = _("Select the {} installation directory.")
title = _("Invalid Directory")
match location_name:
case "cache":
# The variable is the name of the source
subtitle_format = _("Select the {} cache directory.")
case "config":
# The variable is the name of the source
subtitle_format = _("Select the {} configuration directory.")
case "data":
# The variable is the name of the source
subtitle_format = _("Select the {} data directory.")
dialog = create_dialog(
self,
title,

View File

@@ -34,7 +34,7 @@ state_schema = Gio.Settings.new(APP_ID + ".State")
home = Path.home()
data_dir = Path(GLib.get_user_data_dir())
config_dir = Path(GLib.get_user_config_dir())
cache_dir = Path(GLib.get_user_config_dir())
cache_dir = Path(GLib.get_user_cache_dir())
flatpak_dir = home / ".var" / "app"
games_dir = data_dir / "cartridges" / "games"

View File

@@ -18,6 +18,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
from typing import MutableMapping, Generator, Any
from src import shared
from src.game import Game
@@ -31,24 +32,59 @@ class Store:
managers: dict[type[Manager], Manager]
pipeline_managers: set[Manager]
pipelines: dict[str, Pipeline]
games: dict[str, Game]
source_games: MutableMapping[str, MutableMapping[str, Game]]
def __init__(self) -> None:
self.managers = {}
self.pipeline_managers = set()
self.pipelines = {}
self.games = {}
self.source_games = {}
def __contains__(self, obj: object) -> bool:
"""Check if the game is present in the store with the `in` keyword"""
if not isinstance(obj, Game):
return False
if not (source_mapping := self.source_games.get(obj.source)):
return False
return obj.game_id in source_mapping
def __iter__(self) -> Generator[Game, None, None]:
"""Iterate through the games in the store with `for ... in`"""
for _source_id, games_mapping in self.source_games.items():
for _game_id, game in games_mapping.items():
yield game
def __len__(self) -> int:
"""Get the number of games in the store with the `len` builtin"""
return sum(len(source_mapping) for source_mapping in self.source_games)
def __getitem__(self, game_id: str) -> Game:
"""Get a game by its id with `store["game_id_goes_here"]`"""
for game in iter(self):
if game.game_id == game_id:
return game
raise KeyError("Game not found in store")
def get(self, game_id: str, default: Any = None) -> Game | Any:
"""Get a game by its ID, with a fallback if not found"""
try:
game = self[game_id]
return game
except KeyError:
return default
def add_manager(self, manager: Manager, in_pipeline=True):
"""Add a manager to the store"""
manager_type = type(manager)
self.managers[manager_type] = manager
if in_pipeline:
self.enable_manager_in_pipelines(manager_type)
self.toggle_manager_in_pipelines(manager_type, in_pipeline)
def enable_manager_in_pipelines(self, manager_type: type[Manager]):
"""Make a manager run in new pipelines"""
self.pipeline_managers.add(self.managers[manager_type])
def toggle_manager_in_pipelines(self, manager_type: type[Manager], enable: bool):
"""Change if a manager should run in new pipelines"""
if enable:
self.pipeline_managers.add(self.managers[manager_type])
else:
self.pipeline_managers.discard(self.managers[manager_type])
def cleanup_game(self, game: Game) -> None:
"""Remove a game's files"""
@@ -74,7 +110,7 @@ class Store:
return None
# Handle game duplicates
stored_game = self.games.get(game.game_id)
stored_game = self.get(game.game_id)
if not stored_game:
# New game, do as normal
logging.debug("New store game %s (%s)", game.name, game.game_id)
@@ -96,11 +132,15 @@ class Store:
for signal in manager.signals:
game.connect(signal, manager.execute_resilient_manager_logic)
# Add the game to the store
if not game.source in self.source_games:
self.source_games[game.source] = {}
self.source_games[game.source][game.game_id] = game
# Run the pipeline for the game
if not run_pipeline:
return None
pipeline = Pipeline(game, additional_data, self.pipeline_managers)
self.games[game.game_id] = game
self.pipelines[game.game_id] = pipeline
pipeline.advance()
return pipeline

View File

@@ -120,7 +120,7 @@ class CartridgesWindow(Adw.ApplicationWindow):
def set_library_child(self):
child, hidden_child = self.notice_empty, self.hidden_notice_empty
for game in shared.store.games.values():
for game in shared.store:
if game.removed or game.blacklisted:
continue
if game.hidden: