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 @@
 &lt;a href="https://github.com/orende"&gt;orende&lt;/a&gt;
 &lt;a href="https://github.com/viacheslavka"&gt;slavka&lt;/a&gt;
 &lt;a href="https://github.com/slowsage"&gt;slowsage&lt;/a&gt;
-&lt;a href="https://github.com/GB609"&gt;GB609&lt;/a&gt;</property>
+&lt;a href="https://github.com/GB609"&gt;GB609&lt;/a&gt;
+&lt;a 
href="https://github.com/kyle-zhang-42"&gt;kyle-zhang-42;&lt;/a&gt;</property>
     <property name="translator-credits">&lt;a 
href="https://github.com/ArturWroblewski"&gt;Artur Wróblewski&lt;/a&gt;
 &lt;a href="https://github.com/Pyrofani"&gt;Athanasios Nektarios Karachalios 
Stagkas&lt;/a&gt;
 &lt;a href="https://github.com/BlindJerobine"&gt;BlindJerobine&lt;/a&gt;
@@ -58,7 +59,8 @@
 &lt;a href="https://github.com/advy99"&gt;Antonio David Villegas 
Yeguas&lt;/a&gt;
 &lt;a href="https://github.com/manurtinez"&gt;Manu Martinez&lt;/a&gt;
 &lt;a href="https://github.com/Unrud"&gt;Unrud&lt;/a&gt;
-&lt;a href="https://github.com/GLSWV"&gt;GLSWV&lt;/a&gt;</property>
+&lt;a href="https://github.com/GLSWV"&gt;GLSWV&lt;/a&gt;
+</property>
     <property name="artists">&lt;a 
href="https://opengameart.org/users/epic-runes"&gt;Epic 
Runes&lt;/a&gt;</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"}')

Reply via email to