Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package minigalaxy for openSUSE:Factory checked in at 2026-01-23 17:32:20 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/minigalaxy (Old) and /work/SRC/openSUSE:Factory/.minigalaxy.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "minigalaxy" Fri Jan 23 17:32:20 2026 rev:19 rq:1328745 version:1.4.1 Changes: -------- --- /work/SRC/openSUSE:Factory/minigalaxy/minigalaxy.changes 2025-07-11 21:32:41.867446959 +0200 +++ /work/SRC/openSUSE:Factory/.minigalaxy.new.1928/minigalaxy.changes 2026-01-23 17:32:47.693267612 +0100 @@ -1,0 +2,11 @@ +Thu Jan 22 20:54:32 UTC 2026 - Michael Vetter <[email protected]> + +- Update to 1.4.1: + * Installations now report more intermediate steps like checksum + verifications to the UI. + * Fix bugs related to error handling of ongoing installations. + * Fix an issue where CJK characters in game library path prevents + the config file from being loaded properly. + * Automatically add Weblate contributions to README and About dialog on release. + +------------------------------------------------------------------- Old: ---- minigalaxy-1.4.0.tar.gz New: ---- minigalaxy-1.4.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ minigalaxy.spec ++++++ --- /var/tmp/diff_new_pack.9kkRBP/_old 2026-01-23 17:32:48.229289528 +0100 +++ /var/tmp/diff_new_pack.9kkRBP/_new 2026-01-23 17:32:48.233289692 +0100 @@ -1,7 +1,7 @@ # # spec file for package minigalaxy # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: minigalaxy -Version: 1.4.0 +Version: 1.4.1 Release: 0 Summary: A GOG client for Linux that lets you download and play your GOG Linux games License: GPL-3.0-only ++++++ minigalaxy-1.4.0.tar.gz -> minigalaxy-1.4.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/.github/workflows/release.yml new/minigalaxy-1.4.1/.github/workflows/release.yml --- old/minigalaxy-1.4.0/.github/workflows/release.yml 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/.github/workflows/release.yml 2026-01-22 09:35:24.000000000 +0100 @@ -17,6 +17,7 @@ - name: Prepare release files id: tag run: | + ./scripts/credit-weblate-translators.sh ./scripts/create-release.sh env: DEBFULLNAME: ${{ secrets.DEBFULLNAME }} @@ -29,6 +30,7 @@ git config --global user.name 'Wouter Wijsman' git config --global user.email '[email protected]' git add pyproject.toml data/io.github.sharkwouter.Minigalaxy.metainfo.xml debian/changelog minigalaxy/version.py + git add README.md data/ui/about.ui git commit -m "Add new release" git push - name: Release diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/CHANGELOG.md new/minigalaxy-1.4.1/CHANGELOG.md --- old/minigalaxy-1.4.0/CHANGELOG.md 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/CHANGELOG.md 2026-01-22 09:35:24.000000000 +0100 @@ -1,3 +1,9 @@ +**1.4.1** +- Installations now report more intermediate steps like checksum verifications to the UI. (thanks to GB609) +- Fix bugs related to error handling of ongoing installations. (thanks to GB609) +- Fix an issue where CJK characters in game library path prevents the config file from being loaded properly. (thanks to kyle-zhang-42) +- Automatically add Weblate contributions to README and About dialog on release. (thanks to GB609) + **1.4.0** - Various improvements to the download manager, including a pause function (thanks to GB609) - Speed up creation of wine prefixes during installations (thanks to GB609) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/README.md new/minigalaxy-1.4.1/README.md --- old/minigalaxy-1.4.0/README.md 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/README.md 2026-01-22 09:35:24.000000000 +0100 @@ -68,6 +68,7 @@ - Arch Linux - Manjaro - Fedora Linux 31 or newer +- Gentoo Linux - openSUSE Tumbleweed and Leap 15.2 or newer - MX Linux 19 or newer - Solus @@ -122,6 +123,24 @@ </pre> </details> +<details><summary>FreeBSD</summary> + +Available in the <a href="https://www.freshports.org/games/minigalaxy/">official repositories</a>. You can install it with: +<pre> +# pkg install games/minigalaxy +</pre> +</details> + +<details><summary>Gentoo</summary> + +Available in the <a href="https://wiki.gentoo.org/wiki/Project:GURU">GURU overlay</a>. You can enable the repository and install it with: +<pre> +sudo emerge --ask app-eselect/eselect-repository +sudo eselect repository enable guru +sudo emaint sync -r guru +sudo emerge --ask games-util/minigalaxy +</pre> +</details> <details><summary>openSUSE</summary> Available in the official repositories for openSUSE Tumbleweed and also Leap since 15.2. You can install it with: @@ -228,6 +247,7 @@ - slowsage for contributing code - viacheslavka for contributing code - GB609 for contributing code +- kyle-zhang-42 for contributing code - s8321414 for translating to Taiwanese Mandarin - fuzunspm for translating to Turkish - thomansb22 for translating to French diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/data/io.github.sharkwouter.Minigalaxy.metainfo.xml new/minigalaxy-1.4.1/data/io.github.sharkwouter.Minigalaxy.metainfo.xml --- old/minigalaxy-1.4.0/data/io.github.sharkwouter.Minigalaxy.metainfo.xml 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/data/io.github.sharkwouter.Minigalaxy.metainfo.xml 2026-01-22 09:35:24.000000000 +0100 @@ -33,7 +33,17 @@ <provides> <binary>minigalaxy</binary> </provides> - <releases><release version="1.4.0" date="2025-07-09"> + <releases><release version="1.4.1" date="2026-01-22"> + <description> + <p>Implements the following changes:</p> + <ul> + <li>Installations now report more intermediate steps like checksum verifications to the UI. (thanks to GB609)</li> + <li>Fix bugs related to error handling of ongoing installations. (thanks to GB609)</li> + <li>Fix an issue where CJK characters in game library path prevents the config file from being loaded properly. (thanks to kyle-zhang-42)</li> + <li>Automatically add Weblate contributions to README and About dialog on release. (thanks to GB609)</li> + </ul> + </description> + </release><release version="1.4.0" date="2025-07-09"> <description> <p>Implements the following changes:</p> <ul> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/data/ui/about.ui new/minigalaxy-1.4.1/data/ui/about.ui --- old/minigalaxy-1.4.0/data/ui/about.ui 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/data/ui/about.ui 2026-01-22 09:35:24.000000000 +0100 @@ -32,7 +32,8 @@ <a href="https://github.com/orende">orende</a> <a href="https://github.com/viacheslavka">slavka</a> <a href="https://github.com/slowsage">slowsage</a> -<a href="https://github.com/GB609">GB609</a></property> +<a href="https://github.com/GB609">GB609</a> +<a href="https://github.com/kyle-zhang-42">kyle-zhang-42;</a></property> <property name="translator-credits"><a href="https://github.com/ArturWroblewski">Artur Wróblewski</a> <a href="https://github.com/Pyrofani">Athanasios Nektarios Karachalios Stagkas</a> <a href="https://github.com/BlindJerobine">BlindJerobine</a> @@ -58,7 +59,8 @@ <a href="https://github.com/advy99">Antonio David Villegas Yeguas</a> <a href="https://github.com/manurtinez">Manu Martinez</a> <a href="https://github.com/Unrud">Unrud</a> -<a href="https://github.com/GLSWV">GLSWV</a></property> +<a href="https://github.com/GLSWV">GLSWV</a> +</property> <property name="artists"><a href="https://opengameart.org/users/epic-runes">Epic Runes</a></property> <property name="logo-icon-name">image-missing</property> <property name="license-type">gpl-3-0</property> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/debian/changelog new/minigalaxy-1.4.1/debian/changelog --- old/minigalaxy-1.4.0/debian/changelog 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/debian/changelog 2026-01-22 09:35:24.000000000 +0100 @@ -1,3 +1,16 @@ +minigalaxy (1.4.1) noble; urgency=medium + + * Installations now report more intermediate steps like checksum + verifications to the UI. (thanks to GB609) + * Fix bugs related to error handling of ongoing installations. (thanks + to GB609) + * Fix an issue where CJK characters in game library path prevents the + config file from being loaded properly. (thanks to kyle-zhang-42) + * Automatically add Weblate contributions to README and About dialog + on release. (thanks to GB609) + + -- Wouter Wijsman <[email protected]> Thu, 22 Jan 2026 08:34:19 +0000 + minigalaxy (1.4.0) noble; urgency=medium * Various improvements to the download manager, including a pause diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/minigalaxy/config.py new/minigalaxy-1.4.1/minigalaxy/config.py --- old/minigalaxy-1.4.0/minigalaxy/config.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/minigalaxy/config.py 2026-01-22 09:35:24.000000000 +0100 @@ -24,7 +24,7 @@ def __load(self) -> None: if os.path.isfile(self.__config_file): - with open(self.__config_file, "r") as file: + with open(self.__config_file, "r", encoding="utf-8") as file: try: self.__config = json.loads(file.read()) except (json.decoder.JSONDecodeError, UnicodeDecodeError): @@ -36,10 +36,22 @@ config_dir = os.path.dirname(self.__config_file) os.makedirs(config_dir, mode=0o700, exist_ok=True) temp_file = f"{self.__config_file}.tmp" - with open(temp_file, "w") as file: + with open(temp_file, "w", encoding="utf-8") as file: file.write(json.dumps(self.__config, ensure_ascii=False)) os.rename(temp_file, self.__config_file) + def save(self) -> None: + """ + Config will normally immediately save all changes applied to it to the configuration file automatically. + This mechanism relies on getters and setters and the method `config.__write` is called whenever + one of the properties is assigned a new value. + So it only works for direct assignments. But there are some properties which returned Lists or might + return other none-simple types in the future. Changes made to these sub-objects would not be detected + and thus also not be persisted automatically. + In these situations `Config.save` can be used to avoid having to re-assign the same object reference. + """ + self.__write() + @property def locale(self) -> str: return self.__config.get("locale", "") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/minigalaxy/entity/state.py new/minigalaxy-1.4.1/minigalaxy/entity/state.py --- old/minigalaxy-1.4.0/minigalaxy/entity/state.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/minigalaxy/entity/state.py 2026-01-22 09:35:24.000000000 +0100 @@ -13,3 +13,4 @@ UNINSTALLING = auto() UPDATING = auto() UPDATE_INSTALLABLE = auto() + VERIFYING = auto() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/minigalaxy/installer.py new/minigalaxy-1.4.1/minigalaxy/installer.py --- old/minigalaxy-1.4.0/minigalaxy/installer.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/minigalaxy/installer.py 2026-01-22 09:35:24.000000000 +0100 @@ -10,7 +10,7 @@ import time from collections import deque -from enum import Enum +from enum import Enum, auto from queue import Empty from threading import Thread, RLock @@ -74,7 +74,8 @@ keep_installers: bool, create_desktop_file: bool, installer_inventory=None, - raise_error=False + raise_error=False, + progress_callback=None ): error_message = "" error = None @@ -85,9 +86,9 @@ if not installer_inventory: installer_inventory = InstallerInventory.from_file_system(installer) - fail_on_error(verify_installer_integrity(game, installer_inventory), - InstallResultType.CHECKSUM_ERROR) + verify_installer_integrity(game, installer_inventory, progress_callback) + progress_callback(InstallResultType.INSTALL_START, game.name) fail_on_error(verify_disk_space(game, installer), InstallResultType.FAILURE) tmp_dir, = fail_on_error(make_tmp_dir(game)) @@ -146,10 +147,11 @@ return remaining_args -def verify_installer_integrity(game, installer_inventory): +def verify_installer_integrity(game, installer_inventory, progress_callback=None): error_message = [] invalid_files = {} + progress_callback(InstallResultType.VERIFY_START, game.name, installer_inventory) for installer in installer_inventory.as_keep_files_list(): installer_file_name = os.path.basename(installer) if not os.path.exists(installer): @@ -167,11 +169,13 @@ if installer_inventory.verify_checksum(installer_file_name, calculated_checksum): logger.info("%s integrity is preserved. MD5 is: %s", installer_file_name, calculated_checksum) + progress_callback(InstallResultType.VERIFY_PROGRESS, installer_file_name, calculated_checksum) else: error_message.append(_("{} was corrupted. Please download it again.").format(installer_file_name)) invalid_files[installer] = calculated_checksum - return '\n'.join(error_message), invalid_files + if error_message: + raise InstallException('\n'.join(error_message), InstallResultType.CHECKSUM_ERROR, invalid_files) def verify_disk_space(game, installer): @@ -676,7 +680,13 @@ return True def as_keep_files_list(self): - files = [self.inventory_file] + """Returns a list of all files contained in this inventory INCLUDING the inventory itself""" + files = self.contained_files() + files.append(self.inventory_file) + return files + + def contained_files(self): + files = [] for f in self.data.keys(): files.append(os.path.join(self.directory, f)) return files @@ -716,27 +726,49 @@ class InstallResultType(Enum): - SUCCESS = 1 - FAILURE = 2 - CHECKSUM_ERROR = 3 - POST_INSTALL_FAILURE = 4 + """checksum verification has started""" + VERIFY_START = auto() + """A file has been verified successfully""" + VERIFY_PROGRESS = auto() + """The real installation has started""" + INSTALL_START = auto() + """Installation ended with success""" + SUCCESS = auto() + """Installation ended in failure""" + FAILURE = auto() + """Checksum verification failed""" + CHECKSUM_ERROR = auto() + """An error happened during post installation actions""" + POST_INSTALL_FAILURE = auto() class InstallResult: def __init__(self, install_id, result_type: InstallResultType, reason, details=None): """Data class that will be passed to result_callback of InstallTask reason is a type-dependent string: + - INSTALL_START: game name + - VERIFY_START: game name + - VERIFY_PROGRESS: verified file - SUCCESS: install directory path - FAILURE and CHECKSUM_ERROR: string error message + - POST_INSTALL_FAILURE: error message the "details" field provides additional context information: + - VERIFY_START: InstallerInventory + - VERIFY_PROGRESS: the md5 of the file - FAILURE: depending on the failing step, usually a directory path - - CHECKSUM_ERROR: dict {abs_file: calculated_checksum} + - CHECKSUM_ERROR: dict {abs_file: calculated_checksum} of all failed files """ self.install_id = install_id self.type = result_type self.reason = reason self.details = details + self.installation_terminated = result_type in [ + InstallResultType.SUCCESS, + InstallResultType.FAILURE, + InstallResultType.CHECKSUM_ERROR, + InstallResultType.POST_INSTALL_FAILURE + ] def __str__(self): return f"InstallResult(id={self.install_id}, type={self.type}), reason={self.reason})" @@ -768,11 +800,19 @@ def execute(self): try: - install_game(*self.arg_array, **self.named_args, raise_error=True) - self.callback(InstallResult(self.installer_id, InstallResultType.SUCCESS, self.game.install_dir, None)) + # install_game will throw an exception if it doesn't succeed + install_game(*self.arg_array, **self.named_args, raise_error=True, progress_callback=self.notifyStep) + self.notifyStep(InstallResultType.SUCCESS, self.game.install_dir, None) except InstallException as e: logger.error("Error installing item %s: %s", self.installer_id, e.message, exc_info=1) - self.callback(InstallResult(self.installer_id, e.fail_type, e.message, e.data)) + self.notifyStep(e.fail_type, e.message, e.data) + + def notifyStep(self, result_type: InstallResultType, reason='', details=None): + '''Small proxy method to be passed to install_game for intermediate progress report''' + try: + self.callback(InstallResult(self.installer_id, result_type, reason, details)) + except Exception as e: + logger.error("Installation callback handler threw an error: %s", e.message, exc_info=1) def __eq__(self, other): if not isinstance(other, InstallTask): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/minigalaxy/ui/library.py new/minigalaxy-1.4.1/minigalaxy/ui/library.py --- old/minigalaxy-1.4.0/minigalaxy/ui/library.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/minigalaxy/ui/library.py 2026-01-22 09:35:24.000000000 +0100 @@ -193,6 +193,14 @@ games.append(Game(name=name, game_id=game_id, install_dir=full_path, category=category)) else: games.extend(get_installed_windows_games(full_path, game_categories_dict)) + + # try to repair a corrupted list of ongoing downloads + # if something is considered 'installed', it shouldn't be on the download list anymore + for game in games: + if game.id in self.config.current_downloads: + self.config.current_downloads.remove(game.id) + self.config.save() + return games def __add_games_from_api(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/minigalaxy/ui/library_entry.py new/minigalaxy-1.4.1/minigalaxy/ui/library_entry.py --- old/minigalaxy-1.4.0/minigalaxy/ui/library_entry.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/minigalaxy/ui/library_entry.py 2026-01-22 09:35:24.000000000 +0100 @@ -59,11 +59,13 @@ State.UNINSTALLING: self.state_uninstalling, State.UPDATABLE: self.state_updatable, State.UPDATING: self.state_updating, + State.VERIFYING: self.state_verifying } self.thumbnail_loaded = False def init_ui_elements(self): self.image.set_tooltip_text(self.game.name) + self.menu_button.set_tooltip_text(_("Show game options menu")) self.reload_state() load_thumbnail_thread = threading.Thread(target=self.load_thumbnail) load_thumbnail_thread.start() @@ -366,32 +368,67 @@ self._install(self.game.id, save_location, update=True, inventory=inventory, on_success=on_success) - def __install_finished_callback(self, result: InstallResult, on_success=None, on_failure=None, dlc_title=""): + def __install_step_callback(self, result: InstallResult, on_success=None, on_failure=None, dlc_title=""): """ Generic callback passed to enqueue_game_install. Handles some common work to do on success or failure, like: * updating the installed version info * changing state of the UI element accordingly * showing an error message on failure + * this function only handles 'final' states of an installation, dealing with progress reports + is delegated to __handle_install_state_update """ item_name = dlc_title if dlc_title else self.game.name - logger.info("Received install finished notification for %s: %s", item_name, result) + logger.info("Received install step notification for %s: %s", item_name, result) + + if result.installation_terminated: + # Regardless of whether the installation succeeds or fails, we should stop trying to restart the install + self.config.remove_ongoing_download(result.install_id) + # installations are sequenced - there can never be more than one at a time, so it's ok + # to maintain the state of the current installation when it finishes + del self.num_verified_files + del self.install_inventory + if result.type is InstallResultType.SUCCESS: self.update_to_state_if_idle(State.INSTALLED) - self.config.remove_ongoing_download(result.install_id) if dlc_title: self.game.set_dlc_info("version", self.api.get_version(self.game, dlc_name=dlc_title), dlc_title) else: self.game.set_info("version", self.api.get_version(self.game)) if on_success: on_success() - else: + return + + if result.installation_terminated: item_name = dlc_title if dlc_title else self.game.name GLib.idle_add(self.parent_window.show_error, _("Failed to install {}").format(item_name), result.reason) self.reset_to_idle_state_if_possible() if on_failure: on_failure() + return + + self.__handle_install_state_update(result) + + def __handle_install_state_update(self, result: InstallResult): + if result.type is InstallResultType.VERIFY_START: + self.num_verified_files = 0 + self.update_to_state_if_idle(State.VERIFYING) + self.install_inventory = result.details + return + + if result.type is InstallResultType.VERIFY_PROGRESS: + self.num_verified_files = self.num_verified_files + 1 + self.update_to_state_if_idle(State.VERIFYING) + if self.current_state is State.VERIFYING: + # progress bar will be occupied by download progress when not idle + # this can only happen when several large DLCs are downloaded in parallel + percentage = self.num_verified_files / len(self.install_inventory.contained_files()) * 100 + self.set_progress(percentage) + return + + if result.type is InstallResultType.INSTALL_START: + self.update_to_state_if_idle(State.INSTALLING) def _install(self, gog_item_id, save_location, update=False, dlc_title="", inventory=None, on_success=None, on_failure=None): @@ -407,7 +444,7 @@ self.update_to_state_if_idle(processing_state) def install_finished(result): - self.__install_finished_callback(result, on_success, on_failure, dlc_title) + self.__install_step_callback(result, on_success, on_failure, dlc_title) enqueue_game_install( gog_item_id, @@ -567,109 +604,118 @@ else: self.update_to_state(State.DOWNLOADABLE) + def set_main_button(self, clickable, label=None, tooltip=None): + self.button.set_sensitive(clickable) + if label is not None: + self.button.set_label(label) + if tooltip is not None: + self.button.set_tooltip_text(tooltip) + + def update_visible_widgets(self, *widgets, info_buttons=True): + """ + Helper to ensure consistent widget visibility updates on state changes. + The following widgets are automatically made invisible if not explicitely declare as visible + by mentioning them as argument: + - self.menu_button_update + - self.menu_button_uninstall + - self.button_cancel + - self.progress_bar + + Additionally, self.menu_button can be enforced to be visible by setting info_buttons=True. + It will also be shown when one of its sub-buttons shall be visible. + """ + if info_buttons: + self.menu_button.show() + else: + self.menu_button.hide() + + menu_buttons = [self.menu_button_update, self.menu_button_uninstall] + for b in [self.menu_button_update, self.menu_button_uninstall, self.button_cancel, self.progress_bar]: + if b in widgets: + b.show() + # force menu button to be visible if any child shall be visible + if b in menu_buttons and not info_buttons: + self.menu_button.show() + else: + b.hide() + def state_downloadable(self): - self.button.set_label(_("Download")) - self.button.set_tooltip_text(_("Download and install the game")) - self.button.set_sensitive(True) + self.set_main_button(True, _("Download"), _("Download and install the game")) self.image.set_sensitive(False) # The user must have the possibility to access # to the store button even if the game is not installed - self.menu_button.show() - self.menu_button.set_tooltip_text(_("Show game options menu")) - self.menu_button_update.hide() - self.menu_button_dlc.hide() - self.menu_button_uninstall.hide() - self.button_cancel.hide() - self.progress_bar.hide() + self.update_visible_widgets(info_buttons=True) self.game.install_dir = "" def state_installable(self): - self.button.set_label(_("Install")) - self.button.set_tooltip_text(_("Install the game")) - self.button.set_sensitive(True) + self.set_main_button(True, _("Install"), _("Install the game")) self.image.set_sensitive(False) # The user must have the possibility to access # to the store button even if the game is not installed - self.menu_button.show() - self.menu_button_uninstall.hide() - self.menu_button_update.hide() - self.button_cancel.hide() - self.progress_bar.hide() + self.update_visible_widgets(info_buttons=True) self.game.install_dir = "" def state_queued(self): - self.button.set_label(_("In queue…")) - self.button.set_sensitive(False) + self.set_main_button(False, _("In queue…")) self.image.set_sensitive(False) - self.menu_button_uninstall.hide() - self.menu_button_update.hide() - self.button_cancel.show() - self.progress_bar.show() + self.update_visible_widgets(self.progress_bar, self.button_cancel, info_buttons=True) def state_downloading(self): - self.button.set_label(_("Downloading…")) - self.button.set_sensitive(False) + self.set_main_button(False, _("Downloading…")) self.image.set_sensitive(False) - self.menu_button_uninstall.hide() - self.menu_button_update.hide() - self.button_cancel.show() - self.progress_bar.show() + self.update_visible_widgets(self.progress_bar, self.button_cancel, info_buttons=True) def state_installing(self): - self.button.set_label(_("Installing…")) - self.button.set_sensitive(False) + self.set_main_button(False, _("Installing…")) self.image.set_sensitive(True) - self.menu_button_uninstall.hide() - self.menu_button_update.hide() - self.button_cancel.hide() - self.progress_bar.hide() + self.update_visible_widgets(info_buttons=True) self.game.set_install_dir(self.config.install_dir) self.parent_library.filter_library() def state_installed(self): - self.button.set_label(_("Play")) - self.button.set_tooltip_text(_("Launch the game")) + self.set_main_button(True, _("Play"), _("Launch the game")) self.button.get_style_context().add_class("suggested-action") - self.button.set_sensitive(True) + self.image.set_sensitive(True) - self.menu_button.set_tooltip_text(_("Show game options menu")) - self.menu_button.show() - self.menu_button_uninstall.show() - self.button_cancel.hide() - self.progress_bar.hide() - self.menu_button_update.hide() + self.update_visible_widgets(self.menu_button_uninstall, info_buttons=True) self.update_icon.hide() self.game.set_install_dir(self.config.install_dir) def state_uninstalling(self): - self.button.set_label(_("Uninstalling…")) + self.set_main_button(False, _("Uninstalling…")) self.button.get_style_context().remove_class("suggested-action") - self.button.set_sensitive(False) + self.image.set_sensitive(False) - self.menu_button.hide() - self.button_cancel.hide() + self.update_visible_widgets(info_buttons=False) self.game.install_dir = "" self.parent_library.filter_library() def state_updatable(self): + self.set_main_button(True, _("Play")) + self.update_icon.show() self.update_icon.set_from_icon_name("emblem-synchronizing", Gtk.IconSize.LARGE_TOOLBAR) - self.button.set_label(_("Play")) - self.menu_button.show() + + self.update_visible_widgets(self.menu_button_update, self.menu_button_uninstall, info_buttons=True) + tooltip_text = "{} (update{})".format(self.game.name, ", Wine" if self.game.platform == "windows" else "") self.image.set_tooltip_text(tooltip_text) - self.menu_button_update.show() if self.game.platform == "windows": self.wine_icon.set_margin_left(22) def state_updating(self): - self.button.set_label(_("Updating…")) + self.set_main_button(False, _("Updating…")) + self.update_visible_widgets(info_buttons=True) + + def state_verifying(self): + self.set_main_button(False, _("Verifying checksums…")) + self.update_visible_widgets(self.progress_bar, self.button_cancel, info_buttons=True) def update_to_state(self, state): self.current_state = state @@ -704,6 +750,7 @@ """ Helper class to encapsulate several pieces of info used in several methods. """ + def __init__(self, item_id, name): self.id = item_id self.name = name diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/minigalaxy/version.py new/minigalaxy-1.4.1/minigalaxy/version.py --- old/minigalaxy-1.4.0/minigalaxy/version.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/minigalaxy/version.py 2026-01-22 09:35:24.000000000 +0100 @@ -1 +1 @@ -VERSION = "1.4.0" +VERSION = "1.4.1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/pyproject.toml new/minigalaxy-1.4.1/pyproject.toml --- old/minigalaxy-1.4.0/pyproject.toml 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/pyproject.toml 2026-01-22 09:35:24.000000000 +0100 @@ -1,7 +1,7 @@ [project] name = "minigalaxy" description = "A simple GOG Linux client" -version = "1.4.0" +version = "1.4.1" authors = [ { name = "Wouter Wijsman", email = "[email protected]" } ] @@ -16,5 +16,5 @@ ] [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta:__legacy__" +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/scripts/credit-weblate-translators.sh new/minigalaxy-1.4.1/scripts/credit-weblate-translators.sh --- old/minigalaxy-1.4.0/scripts/credit-weblate-translators.sh 1970-01-01 01:00:00.000000000 +0100 +++ new/minigalaxy-1.4.1/scripts/credit-weblate-translators.sh 2026-01-22 09:35:24.000000000 +0100 @@ -0,0 +1,121 @@ +#!/bin/bash + +_REPO_ROOT=$(dirname "$(readlink -f "$0")") +_REPO_ROOT=$(realpath "$_REPO_ROOT"/..) + +cd "$_REPO_ROOT" + +function createTranslationCredits { + declare -A translators + local author + local language + local thanksLine + + while read; do + author="${REPLY#Author: }" + author="${author%% <*>}" + + read language + language="${language#Translated using Weblate (}" + language="${language%)}" + + #text matches what is in readme + thanksLine="- $author for translating to $language" + # assign author as value for about.ui patching + translators+=(["$thanksLine"]="$author") + done + + declare -p translators +} + +function patchAbout { + local _STATE="" + + # this loop reads about.ui line-by-line + while read; do + + # check the current line if it starts the translator-credits or ends that tag again + case "$REPLY" in + # just mark the start... + *\<property\ name=\"translator-credits\"*) + _STATE="BEGIN" + ;; + + # ... to be able to recognize the next closing tag as belonging to the start + # so we can patch in the weblate listing + *\</property\>*) + if [ "$_STATE" = "BEGIN" ]; then + _STATE="DONE" + # the terminating tag might be one the same line as something else + _beforeEnd="${REPLY%%\</property\>*}" + # whatever follows (if any) after the closing tag might also start a new sibling tag + _afterEnd="${REPLY#*\</property\>}" + + # print whats before the closing tag if it is not from Weblate + if [ -n "$_beforeEnd" ] && ! [[ "$_beforeEnd" =~ .*"(Weblate)".* ]]; then + echo "$_beforeEnd" + fi + + for weblate_author in "${translators[@]}"; do + echo "$weblate_author (Weblate)" + done + + echo "</property>" + # change REPLY to whatever came after the closing tag + REPLY="$_afterEnd" + fi + ;; + esac + + # ignore previously generated weblate lines to re-create all of them + # this is easier than merging + if [[ "$REPLY" =~ .*"(Weblate)".* ]] || [ -z "$REPLY" ]; then + continue + fi + + echo "$REPLY" + + done <"$_REPO_ROOT/data/ui/about.ui" +} + +function patchReadme { + local _STATE + declare -A alreadyAdded + + # this loop reads README.md line-by-line + while read; do + if [[ "$REPLY" =~ .*Special\ thanks.* ]]; then + _STATE="PARSE" + fi + + if [ "$_STATE" = "PARSE" ] && [ -n "$REPLY" ]; then + alreadyAdded["$REPLY"]=true || echo "NOT WORKING: [$REPLY]" >2 + fi + + done <"$_REPO_ROOT/README.md" + + cat "$_REPO_ROOT/README.md" + for thanksLine in "${!translators[@]}"; do + if [ -z "${alreadyAdded["$thanksLine"]}" ]; then + echo "$thanksLine" + fi + done +} + +# 1. Pull the data from git log and place into an assoc +# +# There is no direct way to return an array from a function. +# It is also not possible to declare a global one and pass it to the function for manipulation, +# because local manipulations will not propagate back. +# So the script re-declares it from the 'declare -p' function output +source <(git log --no-merges --sparse --committer=weblate --author='^(?!Wouter Wijsman).*$' --perl-regexp \ + | grep -E "^Author: .*|^\s*Translated using" \ + | createTranslationCredits) + +# 2. patch into about +patchAbout > "$_REPO_ROOT/data/ui/about.ui.tmp" +mv -f "$_REPO_ROOT/data/ui/about.ui.tmp" "$_REPO_ROOT/data/ui/about.ui" + +# 3. patch into readme +patchReadme > "$_REPO_ROOT/README.md.tmp" +mv -f "$_REPO_ROOT/README.md.tmp" "$_REPO_ROOT/README.md" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/setup.py new/minigalaxy-1.4.1/setup.py --- old/minigalaxy-1.4.0/setup.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/setup.py 2026-01-22 09:35:24.000000000 +0100 @@ -2,7 +2,10 @@ from glob import glob import subprocess import os -from minigalaxy.version import VERSION +import sys + +sys.path.insert(0, os.getcwd()) +from minigalaxy.version import VERSION # noqa: E402 # Generate the translations subprocess.run(['bash', 'scripts/compile-translations.sh']) @@ -15,7 +18,7 @@ setup( name="minigalaxy", version=VERSION, - packages=find_packages(exclude=['tests']), + packages=find_packages(exclude=['tests', 'tests.*']), scripts=['bin/minigalaxy'], data_files=[ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/tests/test_installer.py new/minigalaxy-1.4.1/tests/test_installer.py --- old/minigalaxy-1.4.0/tests/test_installer.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/tests/test_installer.py 2026-01-22 09:35:24.000000000 +0100 @@ -3,7 +3,7 @@ import os from unittest import TestCase, mock -from unittest.mock import patch, mock_open, MagicMock +from unittest.mock import patch, mock_open, MagicMock, call from minigalaxy.file_info import FileInfo from minigalaxy.game import Game @@ -27,7 +27,7 @@ def test_install_game_with_checksum_exception(self, mock_checksum): '''[scenario: install_game with raise_error=True uses raise instead of return - checksum failure variant]''' failed_file_list = {"/cache/adrift_setup-1.bin": "md5abc"} - mock_checksum.return_value = ("Checksum Error", failed_file_list) + mock_checksum.side_effect = installer.InstallException("Checksum Error", installer.InstallResultType.CHECKSUM_ERROR, failed_file_list) game = Game("Absolute Drift", install_dir="/home/makson/GOG Games/Absolute Drift", platform="windows") inventory = self.prepare_inventory("/cache/adrift_setup.exe", "", 0) @@ -37,25 +37,28 @@ keep_installers=False, create_desktop_file=True, installer_inventory=inventory, raise_error=True) - self.assertEqual(installer.InstallResultType.CHECKSUM_ERROR, result.exception.fail_type) + self.assertEqual(installer.InstallResultType.CHECKSUM_ERROR, result.exception.fail_type, result.exception.message) self.assertIs(failed_file_list, result.exception.data) @mock.patch('minigalaxy.installer.verify_disk_space') @mock.patch('minigalaxy.installer.verify_installer_integrity') def test_install_game_with_failure_exception(self, mock_checksum, mock_disk_check): '''[scenario: install_game with raise_error=True uses raise instead of return - regular failure variant]''' - mock_checksum.return_value = ("", {}) mock_disk_check.return_value = "disk_full" game = Game("Absolute Drift", install_dir="/home/makson/GOG Games/Absolute Drift", platform="windows") + progress_callback = MagicMock() + inventory = self.prepare_inventory("/cache/adrift_setup.exe", "", 0) inventory.add_file("/cache/adrift_setup-1.bin", FileInfo("", 0)) with self.assertRaises(installer.InstallException) as result: installer.install_game(game, installer="", language="", install_dir="", keep_installers=False, create_desktop_file=True, - installer_inventory=inventory, raise_error=True) + installer_inventory=inventory, raise_error=True, + progress_callback=progress_callback) + progress_callback.assert_called_once_with(installer.InstallResultType.INSTALL_START, game.name) self.assertEqual(installer.InstallResultType.FAILURE, result.exception.fail_type) self.assertIs("disk_full", result.exception.message) @@ -132,7 +135,7 @@ for f in failed_file_list: inventory.verify_checksum(os.path.basename(f), "calculated_stuff") - mock_checksum.return_value = ("Checksum Error", failed_file_list) + mock_checksum.side_effect = installer.InstallException("Checksum Error", installer.InstallResultType.CHECKSUM_ERROR, failed_file_list) game = Game("Absolute Drift", install_dir=install_dir, platform="windows") with self.assertRaises(installer.InstallException): @@ -155,10 +158,15 @@ installer_path = "/home/user/.cache/minigalaxy/download/" \ "Beneath a Steel Sky/{}".format(installer_name) inventory = self.prepare_inventory(installer_path, md5_sum, 0) - exp = "" + + progress_callback = MagicMock() + with patch("builtins.open", mock_open(read_data=b"")): - obs, failures = installer.verify_installer_integrity(game, inventory) - self.assertEqual(exp, obs) + installer.verify_installer_integrity(game, inventory, progress_callback) + progress_callback.assert_has_calls([ + call(installer.InstallResultType.VERIFY_START, game.name, inventory), + call(installer.InstallResultType.VERIFY_PROGRESS, installer_name, md5_sum) + ]) @mock.patch('os.path.exists') @mock.patch('hashlib.md5') @@ -176,9 +184,13 @@ "Beneath a Steel Sky/{}".format(installer_name) inventory = self.prepare_inventory(installer_path, md5_sum, 0) exp = _("{} was corrupted. Please download it again.").format(installer_name) + + progress_callback = MagicMock() with patch("builtins.open", mock_open(read_data=b"aaaa")): - obs, failures = installer.verify_installer_integrity(game, inventory) - self.assertEqual(exp, obs) + with self.assertRaises(installer.InstallException) as cm: + installer.verify_installer_integrity(game, inventory, progress_callback) + self.assertEqual(exp, cm.exception.message) + progress_callback.assert_called_once_with(installer.InstallResultType.VERIFY_START, game.name, inventory) @mock.patch('os.path.exists') @mock.patch('os.listdir') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/minigalaxy-1.4.0/tests/test_ui_library.py new/minigalaxy-1.4.1/tests/test_ui_library.py --- old/minigalaxy-1.4.0/tests/test_ui_library.py 2025-07-09 13:43:02.000000000 +0200 +++ new/minigalaxy-1.4.1/tests/test_ui_library.py 2026-01-22 09:35:24.000000000 +0100 @@ -211,6 +211,27 @@ obs = games[0].name self.assertEqual(exp, obs) + def test_installed_games_removed_from_current_downloads(self): + """Make sure that library detects when already installed games are still marked as to be downloaded""" + + # none-empty list of playTasks needed so that library recognizes it as installed game + game_json_data = '{ "gameId": "1207665883", "name": "Aliens vs Predator Classic 2000", "playTasks":[{}]}' + gog_info_file = "goggame-1207665883.info" + self.mock_config.current_downloads = [1207665883] + + api_mock = MagicMock() + test_library = Library(MagicMock(), self.mock_config, api_mock, MagicMock()) + + with tempfile.TemporaryDirectory() as tmpdir: + self.mock_config.install_dir = tmpdir + os.makedirs(f'{tmpdir}/Alien', mode=0o755) + with open(f'{tmpdir}/Alien/{gog_info_file}', "w", encoding="utf-8") as file: + file.write(game_json_data) + test_library._Library__get_installed_games() + + self.assertEqual([], self.mock_config.current_downloads) + self.mock_config.save.assert_called_once() + def test_read_game_categories_file_should_return_populated_dict(self): with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as tmpfile: tmpfile.write('{"Test Game":"Adventure"}')
