From d252b6b97d72aa8ed8820b5f2745a8c20e0017a3 Mon Sep 17 00:00:00 2001 From: GeoffreyCoulaud Date: Sun, 2 Jul 2023 00:08:00 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Very=20unfinished=20initial=20wo?= =?UTF-8?q?rk=20on=20import=20undo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game.py | 1 + src/importer/importer.py | 26 ++++++++++++----- src/store/store.py | 62 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/game.py b/src/game.py index e4b7693..03fdba3 100644 --- a/src/game.py +++ b/src/game.py @@ -82,6 +82,7 @@ class Game(Gtk.Box): shared.schema.connect("changed", self.schema_changed) def update_values(self, data): + shared.store.delete_backup() for key, value in data.items(): # Convert executables to strings if key == "executable" and isinstance(value, list): diff --git a/src/importer/importer.py b/src/importer/importer.py index 024283c..8bce1f6 100644 --- a/src/importer/importer.py +++ b/src/importer/importer.py @@ -93,6 +93,11 @@ class Importer(ErrorProducer): self.create_dialog() + # Backup the store to enable undo + shared.store.delete_backup() + shared.store.save_backup() + shared.store.protect_backup() + # Collect all errors and reset the cancellables for the managers # - Only one importer exists at any given time # - Every import starts fresh @@ -218,6 +223,7 @@ class Importer(ErrorProducer): def import_callback(self): """Callback called when importing has finished""" logging.info("Import done") + shared.store.unprotect_backup() self.import_dialog.close() self.summary_toast = self.create_summary_toast() self.create_error_dialog() @@ -288,13 +294,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_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) + ) + toast.set_button_label(_("Undo")) + toast.connect( + "button-clicked", self.dialog_response_callback, "undo_import" + ) shared.win.toast_overlay.add_toast(toast) return toast @@ -315,5 +325,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": + shared.store.restore_backup() else: self.timeout_toast() diff --git a/src/store/store.py b/src/store/store.py index d56a746..ab4d7b6 100644 --- a/src/store/store.py +++ b/src/store/store.py @@ -18,6 +18,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later import logging +from pathlib import Path +from typing import Optional +from shutil import rmtree, copytree + +from gi.repository import GLib from src import shared from src.game import Game @@ -33,6 +38,11 @@ class Store: pipelines: dict[str, Pipeline] games: dict[str, Game] + games_backup: Optional[dict[str, Game]] = None + covers_backup_path: Optional[Path] = None + is_backup_protected: bool = False + has_backup: bool = False + def __init__(self) -> None: self.managers = {} self.pipeline_managers = set() @@ -104,3 +114,55 @@ class Store: self.pipelines[game.game_id] = pipeline pipeline.advance() return pipeline + + def save_backup(self): + """Save an internal backup of games and covers that can be restored""" + self.games_backup = self.games.copy() + self.covers_backup_path = GLib.dir_make_tmp() + copytree(str(shared.covers_dir), self.covers_backup_path) + + def protect_backup(self): + """Protect the current backup from being deleted""" + self.is_backup_protected = True + + def unprotect_backup(self): + """No longer protect the backup from being deleted""" + self.is_backup_protected = False + + def restore_backup(self): + """Restore the latest backup of games and covers""" + + if not self.has_backup: + return + + # Remove covers + rmtree(shared.covers_dir) + shared.covers_dir.mkdir() + + # Remove games + for game in self.games_backup.values(): + game.update_values({"removed": True}) + game.save() + shared.win.library.remove_all() + shared.win.hidden_library.remove_all() + + # Restore covers + copytree(self.covers_backup_path, str(shared.covers_dir)) + + # Restore games and covers + for game in self.games_backup.values(): + self.add_game(game, {}, run_pipeline=False) + game.save() + game.update() + + self.delete_backup() + + def delete_backup(self): + """Delete the latest backup of games and covers (if not protected)""" + if self.is_backup_protected: + return + self.games_backup = None + if self.covers_backup_path and Path(self.covers_backup_path).is_dir(): + self.covers_backup_path = None + rmtree(self.covers_backup_path, ignore_errors=True) + self.has_backup = False