diff --git a/src/importer/importer.py b/src/importer/importer.py index 024283c..efa8c03 100644 --- a/src/importer/importer.py +++ b/src/importer/importer.py @@ -18,7 +18,9 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import json import logging +from shutil import copytree, rmtree from gi.repository import Adw, GLib, Gtk @@ -26,8 +28,15 @@ from src import shared from src.errors.error_producer import ErrorProducer from src.errors.friendly_error import FriendlyError from src.game import Game +from src.importer.sources.bottles_source import BottlesSource +from src.importer.sources.flatpak_source import FlatpakSource +from src.importer.sources.heroic_source import HeroicSource +from src.importer.sources.itch_source import ItchSource +from src.importer.sources.legendary_source import LegendarySource from src.importer.sources.location import UnresolvableLocationError +from src.importer.sources.lutris_source import LutrisSource from src.importer.sources.source import Source +from src.importer.sources.steam_source import SteamSource from src.store.managers.async_manager import AsyncManager from src.store.pipeline import Pipeline from src.utils.task import Task @@ -53,6 +62,20 @@ class Importer(ErrorProducer): super().__init__() self.game_pipelines = set() self.sources = set() + if shared.schema.get_boolean("lutris"): + self.sources.add(LutrisSource()) + if shared.schema.get_boolean("steam"): + self.sources.add(SteamSource()) + if shared.schema.get_boolean("heroic"): + self.sources.add(HeroicSource()) + if shared.schema.get_boolean("bottles"): + self.sources.add(BottlesSource()) + if shared.schema.get_boolean("flatpak"): + self.sources.add(FlatpakSource()) + if shared.schema.get_boolean("itch"): + self.sources.add(ItchSource()) + if shared.schema.get_boolean("legendary"): + self.sources.add(LegendarySource()) @property def n_games_added(self): @@ -85,12 +108,47 @@ class Importer(ErrorProducer): and len(self.game_pipelines) == self.n_pipelines_done ) - def add_source(self, source): - self.sources.add(source) + def load_games_from_disk(self): + """Load the games from disk""" + if shared.games_dir.is_dir(): + for game_file in shared.games_dir.iterdir(): + data = json.load(game_file.open()) + game = Game(data) + shared.store.add_game(game, {"skip_save": True}) + game.update() + + def delete_backup(self): + """Delete a a previously made backup""" + rmtree(shared.backup_games_dir, ignore_errors=True) + rmtree(shared.backup_covers_dir, ignore_errors=True) + + def create_backup(self): + """Make a games and covers backup""" + self.delete_backup() + copytree(shared.games_dir, shared.backup_games_dir) + copytree(shared.covers_dir, shared.backup_covers_dir) + + def restore_backup(self): + """Restore a previously made backup""" + + # Remove games from the store and UI + for game in shared.store.games.values(): + game.update_values({"removed": True}) + game.update() + + # Restore the disk backup + rmtree(shared.games_dir, ignore_errors=True) + rmtree(shared.covers_dir, ignore_errors=True) + shared.backup_games_dir.rename(shared.games_dir) + shared.backup_covers_dir.rename(shared.covers_dir) + + # Reload games from disk + self.load_games_from_disk() def run(self): """Use several Gio.Task to import games from added sources""" + self.create_backup() self.create_dialog() # Collect all errors and reset the cancellables for the managers @@ -288,13 +346,17 @@ class Importer(ErrorProducer): "open_preferences", "import", ) - - elif self.n_games_added == 1: - toast.set_title(_("1 game imported")) - - elif self.n_games_added > 1: - # The variable is the number of games - toast.set_title(_("{} games imported").format(self.n_games_added)) + else: + toast.set_button_label(_("Undo")) + toast.connect( + "button-clicked", self.dialog_response_callback, "undo_import" + ) + toast.set_title( + _("1 game imported") + if self.n_games_added == 1 + # The variable is the number of games + else _("{} games imported").format(self.n_games_added) + ) shared.win.toast_overlay.add_toast(toast) return toast @@ -315,5 +377,7 @@ class Importer(ErrorProducer): self.open_preferences(*args) elif response == "open_preferences_import": self.open_preferences(*args).connect("close-request", self.timeout_toast) + elif response == "undo_import": + self.restore_backup() else: self.timeout_toast() diff --git a/src/main.py b/src/main.py index d0afd3e..9831992 100644 --- a/src/main.py +++ b/src/main.py @@ -17,7 +17,6 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -import json import lzma import sys @@ -31,15 +30,7 @@ from gi.repository import Adw, Gio, GLib, Gtk from src import shared from src.details_window import DetailsWindow -from src.game import Game from src.importer.importer import Importer -from src.importer.sources.bottles_source import BottlesSource -from src.importer.sources.flatpak_source import FlatpakSource -from src.importer.sources.heroic_source import HeroicSource -from src.importer.sources.itch_source import ItchSource -from src.importer.sources.legendary_source import LegendarySource -from src.importer.sources.lutris_source import LutrisSource -from src.importer.sources.steam_source import SteamSource from src.logging.setup import log_system_info, setup_logging from src.preferences import PreferencesWindow from src.store.managers.display_manager import DisplayManager @@ -83,17 +74,16 @@ class CartridgesApplication(Adw.Application): "is-maximized", self.win, "maximized", Gio.SettingsBindFlags.DEFAULT ) - # Load games from disk - shared.store.add_manager(FileManager(), False) + # Add managers to the store for game imports shared.store.add_manager(DisplayManager()) - self.load_games_from_disk() - - # Add rest of the managers for game imports + shared.store.add_manager(FileManager()) shared.store.add_manager(LocalCoverManager()) shared.store.add_manager(SteamAPIManager()) shared.store.add_manager(OnlineCoverManager()) shared.store.add_manager(SGDBManager()) - shared.store.enable_manager_in_pipelines(FileManager) + + # Load games from disk + Importer().load_games_from_disk() # Create actions self.create_actions( @@ -134,13 +124,6 @@ class CartridgesApplication(Adw.Application): self.win.present() - def load_games_from_disk(self): - if shared.games_dir.is_dir(): - for game_file in shared.games_dir.iterdir(): - data = json.load(game_file.open()) - game = Game(data) - shared.store.add_game(game, {"skip_save": True}) - def on_about_action(self, *_args): # Get the debug info from the log files debug_str = "" @@ -207,30 +190,7 @@ class CartridgesApplication(Adw.Application): DetailsWindow() def on_import_action(self, *_args): - importer = Importer() - - if shared.schema.get_boolean("lutris"): - importer.add_source(LutrisSource()) - - if shared.schema.get_boolean("steam"): - importer.add_source(SteamSource()) - - if shared.schema.get_boolean("heroic"): - importer.add_source(HeroicSource()) - - if shared.schema.get_boolean("bottles"): - importer.add_source(BottlesSource()) - - if shared.schema.get_boolean("flatpak"): - importer.add_source(FlatpakSource()) - - if shared.schema.get_boolean("itch"): - importer.add_source(ItchSource()) - - if shared.schema.get_boolean("legendary"): - importer.add_source(LegendarySource()) - - importer.run() + Importer().run() def on_remove_game_action(self, *_args): self.win.active_game.remove_game() diff --git a/src/shared.py.in b/src/shared.py.in index bfa882c..06d4204 100644 --- a/src/shared.py.in +++ b/src/shared.py.in @@ -34,12 +34,16 @@ 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" covers_dir = data_dir / "cartridges" / "covers" +backup_dir = cache_dir / "cartridges" / "backup" +backup_games_dir = backup_dir / games_dir.name +backup_covers_dir = backup_dir / covers_dir.name + appdata_dir = Path(os.getenv("appdata") or "C:\\Users\\Default\\AppData\\Roaming") programfiles32_dir = Path(os.getenv("programfiles(x86)") or "C:\\Program Files (x86)") diff --git a/src/store/managers/display_manager.py b/src/store/managers/display_manager.py index 7d3a8f0..b5eec7f 100644 --- a/src/store/managers/display_manager.py +++ b/src/store/managers/display_manager.py @@ -17,6 +17,9 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import logging + +from src import shared from src.game import Game from src.game_cover import GameCover from src.store.managers.manager import Manager @@ -46,27 +49,28 @@ class DisplayManager(Manager): "notify::visible", game.toggle_play, None ) game.menu_button.get_popover().connect( - "notify::visible", game.win.set_active_game, game + "notify::visible", shared.win.set_active_game, game ) - if game.game_id in game.win.game_covers: - game.game_cover = game.win.game_covers[game.game_id] + if game.game_id in shared.win.game_covers: + game.game_cover = shared.win.game_covers[game.game_id] game.game_cover.add_picture(game.cover) else: game.game_cover = GameCover({game.cover}, game.get_cover_path()) - game.win.game_covers[game.game_id] = game.game_cover + shared.win.game_covers[game.game_id] = game.game_cover if ( - game.win.stack.get_visible_child() == game.win.details_view - and game.win.active_game == game + shared.win.stack.get_visible_child() == shared.win.details_view + and shared.win.active_game == game ): - game.win.show_details_view(game) + shared.win.show_details_view(game) if not game.removed and not game.blacklisted: + logging.debug("Adding %s (%s) to the UI", game.name, game.game_id) if game.hidden: - game.win.hidden_library.append(game) + shared.win.hidden_library.append(game) else: - game.win.library.append(game) + shared.win.library.append(game) game.get_parent().set_focusable(False) - game.win.set_library_child() + shared.win.set_library_child() diff --git a/src/store/managers/manager.py b/src/store/managers/manager.py index b1aadf6..411d190 100644 --- a/src/store/managers/manager.py +++ b/src/store/managers/manager.py @@ -110,6 +110,7 @@ class Manager(ErrorProducer): except Exception as error: # pylint: disable=broad-exception-caught handle_error(error) + logging.debug("Running %s for %s (%s)", self.name, game.name, game.game_id) try_manager_logic() def process_game(