From 9b48e26acc9e8bea79a731dc27ef3305af8e9c27 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Tue, 24 Jun 2025 10:48:14 +0500 Subject: [PATCH] refactor(context_menu): simplify ContextMenuManager implementation Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 583 ++++++++++++--------------- 1 file changed, 262 insertions(+), 321 deletions(-) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index 424ac48..0afa5b8 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -6,16 +6,16 @@ import subprocess import threading import logging import re -import json +import orjson +import vdf from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt from PySide6.QtGui import QDesktopServices -from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites from portprotonqt.localization import _ +from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam, get_steam_home, get_last_steam_user, convert_steam_id -from portprotonqt.dialogs import AddGameDialog from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable -import vdf +from portprotonqt.dialogs import AddGameDialog logger = logging.getLogger(__name__) @@ -52,7 +52,6 @@ class ContextMenuManager: os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "PortProtonQt", "legendary_cache" ) - # Initialize signals for thread-safe UI updates self.signals = ContextMenuSignals() if self.parent.statusBar() is None: logger.warning("Status bar is not initialized in MainWindow") @@ -81,6 +80,24 @@ class ContextMenuManager: logger.debug("Showing info dialog: %s - %s", title, message) QMessageBox.information(self.parent, title, message) + def _show_status_message(self, message: str, timeout: int = 3000): + """Show a status message on the status bar if available.""" + if self.parent.statusBar(): + self.parent.statusBar().showMessage(message, timeout) + logger.debug("Direct status message: %s", message) + else: + logger.warning("Status bar not available for message: %s", message) + + def _check_portproton(self): + """Check if PortProton is available.""" + if self.portproton_location is None: + self.signals.show_warning_dialog.emit( + _("Error"), + _("PortProton is not found") + ) + return False + return True + def _is_egs_game_installed(self, app_name: str) -> bool: """ Check if an EGS game is installed by reading installed.json. @@ -95,12 +112,11 @@ class ContextMenuManager: if not os.path.exists(installed_json_path): logger.debug("installed.json not found at %s", installed_json_path) return False - try: - with open(installed_json_path, encoding="utf-8") as f: - installed_games = json.load(f) + with open(installed_json_path, "rb") as f: + installed_games = orjson.loads(f.read()) return app_name in installed_games - except (OSError, json.JSONDecodeError) as e: + except (OSError, orjson.JSONDecodeError) as e: logger.error("Failed to read installed.json: %s", e) return False @@ -117,107 +133,91 @@ class ContextMenuManager: favorites = read_favorites() is_favorite = game_card.name in favorites - - if is_favorite: - favorite_action = menu.addAction(_("Remove from Favorites")) - favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, False)) - else: - favorite_action = menu.addAction(_("Add to Favorites")) - favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True)) + favorite_action = menu.addAction( + _("Remove from Favorites") if is_favorite else _("Add to Favorites") + ) + favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, not is_favorite)) if game_card.game_source == "epic": - # Always show Import to Legendary import_action = menu.addAction(_("Import to Legendary")) import_action.triggered.connect( lambda: self.import_to_legendary(game_card.name, game_card.appid) ) - # Show other actions only if the game is installed if self._is_egs_game_installed(game_card.appid): is_in_steam = is_game_in_steam(game_card.name) - if is_in_steam: - remove_steam_action = menu.addAction(_("Remove from Steam")) - remove_steam_action.triggered.connect( - lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) - ) - else: - add_steam_action = menu.addAction(_("Add to Steam")) - add_steam_action.triggered.connect( - lambda: self.add_egs_to_steam(game_card.name, game_card.appid) - ) + steam_action = menu.addAction( + _("Remove from Steam") if is_in_steam else _("Add to Steam") + ) + steam_action.triggered.connect( + lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) + if is_in_steam + else self.add_egs_to_steam(game_card.name, game_card.appid) + ) open_folder_action = menu.addAction(_("Open Game Folder")) open_folder_action.triggered.connect( lambda: self.open_egs_game_folder(game_card.appid) ) - # Add desktop shortcut actions for EGS games desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip() desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop") - if os.path.exists(desktop_path): - remove_desktop_action = menu.addAction(_("Remove from Desktop")) - remove_desktop_action.triggered.connect( - lambda: self.remove_egs_from_desktop(game_card.name) - ) - else: - add_desktop_action = menu.addAction(_("Add to Desktop")) - add_desktop_action.triggered.connect( - lambda: self.add_egs_to_desktop(game_card.name, game_card.appid) - ) - # Add menu shortcut actions for EGS games + desktop_action = menu.addAction( + _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop") + ) + desktop_action.triggered.connect( + lambda: self.remove_egs_from_desktop(game_card.name) + if os.path.exists(desktop_path) + else self.add_egs_to_desktop(game_card.name, game_card.appid) + ) applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications") menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop") - if os.path.exists(menu_path): - remove_menu_action = menu.addAction(_("Remove from Menu")) - remove_menu_action.triggered.connect( - lambda: self.remove_egs_from_menu(game_card.name) - ) - else: - add_menu_action = menu.addAction(_("Add to Menu")) - add_menu_action.triggered.connect( - lambda: self.add_egs_to_menu(game_card.name, game_card.appid) - ) + menu_action = menu.addAction( + _("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu") + ) + menu_action.triggered.connect( + lambda: self.remove_egs_from_menu(game_card.name) + if os.path.exists(menu_path) + else self.add_egs_to_menu(game_card.name, game_card.appid) + ) if game_card.game_source not in ("steam", "epic"): desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip() desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop") - if os.path.exists(desktop_path): - remove_action = menu.addAction(_("Remove from Desktop")) - remove_action.triggered.connect(lambda: self.remove_from_desktop(game_card.name)) - else: - add_action = menu.addAction(_("Add to Desktop")) - add_action.triggered.connect(lambda: self.add_to_desktop(game_card.name, game_card.exec_line)) - + desktop_action = menu.addAction( + _("Remove from Desktop") if os.path.exists(desktop_path) else _("Add to Desktop") + ) + desktop_action.triggered.connect( + lambda: self.remove_from_desktop(game_card.name) + if os.path.exists(desktop_path) + else self.add_to_desktop(game_card.name, game_card.exec_line) + ) edit_action = menu.addAction(_("Edit Shortcut")) edit_action.triggered.connect( lambda: self.edit_game_shortcut(game_card.name, game_card.exec_line, game_card.cover_path) ) - delete_action = menu.addAction(_("Delete from PortProton")) delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line)) - open_folder_action = menu.addAction(_("Open Game Folder")) open_folder_action.triggered.connect( lambda: self.open_game_folder(game_card.name, game_card.exec_line) ) - applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications") - desktop_path = os.path.join(applications_dir, f"{game_card.name}.desktop") - if os.path.exists(desktop_path): - remove_action = menu.addAction(_("Remove from Menu")) - remove_action.triggered.connect(lambda: self.remove_from_menu(game_card.name)) - else: - add_action = menu.addAction(_("Add to Menu")) - add_action.triggered.connect(lambda: self.add_to_menu(game_card.name, game_card.exec_line)) - + menu_path = os.path.join(applications_dir, f"{game_card.name}.desktop") + menu_action = menu.addAction( + _("Remove from Menu") if os.path.exists(menu_path) else _("Add to Menu") + ) + menu_action.triggered.connect( + lambda: self.remove_from_menu(game_card.name) + if os.path.exists(menu_path) + else self.add_to_menu(game_card.name, game_card.exec_line) + ) is_in_steam = is_game_in_steam(game_card.name) - if is_in_steam: - remove_steam_action = menu.addAction(_("Remove from Steam")) - remove_steam_action.triggered.connect( - lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) - ) - else: - add_steam_action = menu.addAction(_("Add to Steam")) - add_steam_action.triggered.connect( - lambda: self.add_to_steam(game_card.name, game_card.exec_line, game_card.cover_path) - ) + steam_action = menu.addAction( + _("Remove from Steam") if is_in_steam else _("Add to Steam") + ) + steam_action.triggered.connect( + lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source) + if is_in_steam + else self.add_to_steam(game_card.name, game_card.exec_line, game_card.cover_path) + ) menu.exec(game_card.mapToGlobal(pos)) @@ -231,24 +231,21 @@ class ContextMenuManager: """ if not self._check_portproton(): return - if not os.path.exists(self.legendary_path): self.signals.show_warning_dialog.emit( - _("Error"), _("Legendary executable not found at {0}").format(self.legendary_path) + _("Error"), + _("Legendary executable not found at {path}").format(path=self.legendary_path) ) return def on_add_to_steam_result(result: tuple[bool, str]): success, message = result - if success: - self.signals.show_info_dialog.emit( - _("Success"), - _("'{0}' was added to Steam. Please restart Steam for changes to take effect.").format(game_name) - ) - else: - self.signals.show_warning_dialog.emit(_("Error"), message) + self.signals.show_info_dialog.emit( + _("Success"), + _("'{game_name}' was added to Steam. Please restart Steam for changes to take effect.").format(game_name=game_name) + ) - logger.debug("Adding '%s' to Steam", game_name) + logger.debug("Adding EGS game '%s' to Steam", game_name) add_egs_to_steam(app_name, game_name, self.legendary_path, on_add_to_steam_result) def open_egs_game_folder(self, app_name: str): @@ -260,29 +257,21 @@ class ContextMenuManager: """ if not self._check_portproton(): return - exe_path = get_egs_executable(app_name, self.legendary_config_path) if not exe_path or not os.path.exists(exe_path): self.signals.show_warning_dialog.emit( _("Error"), - _("Executable file not found for game: {0}").format(app_name) + _("Executable not found for game: {game_name}").format(game_name=app_name) ) return - try: folder_path = os.path.dirname(os.path.abspath(exe_path)) QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Opened folder for EGS game '{0}'").format(app_name), 3000 - ) - logger.debug("Direct status message: Opened folder for '%s'", app_name) - else: - logger.warning("Status bar not available when opening folder for '%s'", app_name) + self._show_status_message(_("Opened folder for '{game_name}'").format(game_name=app_name)) except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to open game folder: {0}").format(str(e)) + _("Failed to open folder: {error}").format(error=str(e)) ) def import_to_legendary(self, game_name, app_name): @@ -295,43 +284,34 @@ class ContextMenuManager: """ if not self._check_portproton(): return - folder_path = QFileDialog.getExistingDirectory( - self.parent, - _("Select Game Installation Folder"), - os.path.expanduser("~") + self.parent, _("Select Game Installation Folder"), os.path.expanduser("~") ) if not folder_path: - if self.parent.statusBar(): - self.parent.statusBar().showMessage(_("No folder selected"), 3000) - logger.debug("Direct status message: No folder selected for '%s'", game_name) - else: - logger.warning("Status bar not available when no folder selected for '%s'", game_name) + self._show_status_message(_("No folder selected")) return - if not os.path.exists(self.legendary_path): self.signals.show_warning_dialog.emit( _("Error"), - _("Legendary executable not found at {0}").format(self.legendary_path) + _("Legendary executable not found at {path}").format(path=self.legendary_path) ) return - def run_import(): cmd = [self.legendary_path, "import", app_name, folder_path] - subprocess.run( - cmd, - capture_output=True, - text=True, - check=True - ) - - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Importing '{0}' to Legendary...").format(game_name), 0 - ) - logger.debug("Direct status message: Importing '%s' to Legendary", game_name) - else: - logger.warning("Status bar not available when importing '%s'", game_name) + try: + subprocess.run(cmd, capture_output=True, text=True, check=True) + self.signals.show_info_dialog.emit( + _("Success"), + _("Imported '{game_name}' to Legendary").format(game_name=game_name) + ) + except subprocess.CalledProcessError as e: + self.signals.show_warning_dialog.emit( + _("Error"), + _("Failed to import '{game_name}' to Legendary: {error}").format( + game_name=game_name, error=e.stderr + ) + ) + self._show_status_message(_("Importing '{game_name}' to Legendary...").format(game_name=game_name)) threading.Thread(target=run_import, daemon=True).start() def toggle_favorite(self, game_card, add: bool): @@ -346,35 +326,16 @@ class ContextMenuManager: if add and game_card.name not in favorites: favorites.append(game_card.name) game_card.is_favorite = True - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Added '{0}' to favorites").format(game_card.name), 3000 - ) - logger.debug("Direct status message: Added '%s' to favorites", game_card.name) - else: - logger.warning("Status bar not available when adding '%s' to favorites", game_card.name) + message = _("Added '{game_name}' to favorites").format(game_name=game_card.name) elif not add and game_card.name in favorites: favorites.remove(game_card.name) game_card.is_favorite = False - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Removed '{0}' from favorites").format(game_card.name), 3000 - ) - logger.debug("Direct status message: Removed '%s' from favorites", game_card.name) - else: - logger.warning("Status bar not available when removing '%s' from favorites", game_card.name) + message = _("Removed '{game_name}' from favorites").format(game_name=game_card.name) + else: + return save_favorites(favorites) game_card.update_favorite_icon() - - def _check_portproton(self): - """Check if PortProton is available.""" - if self.portproton_location is None: - self.signals.show_warning_dialog.emit( - _("Error"), - _("PortProton is not found.") - ) - return False - return True + self._show_status_message(message) def _get_desktop_path(self, game_name): """Construct the .desktop file path, trying both original and sanitized game names.""" @@ -386,8 +347,7 @@ class ContextMenuManager: def _get_egs_desktop_path(self, game_name): """Construct the .desktop file path for EGS games.""" - desktop_path = os.path.join(self.portproton_location, "egs_desktops", f"{game_name}.desktop") - return desktop_path + return os.path.join(self.portproton_location, "egs_desktops", f"{game_name}.desktop") def _create_egs_desktop_file(self, game_name: str, app_name: str) -> bool: """ @@ -402,15 +362,12 @@ class ContextMenuManager: """ if not self._check_portproton(): return False - if not os.path.exists(self.legendary_path): self.signals.show_warning_dialog.emit( _("Error"), - _("Legendary executable not found at {0}").format(self.legendary_path) + _("Legendary executable not found at {path}").format(path=self.legendary_path) ) return False - - # Determine wrapper wrapper = "flatpak run ru.linux_gaming.PortProton" start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh") if self.portproton_location and ".var" not in self.portproton_location: @@ -418,23 +375,17 @@ class ContextMenuManager: if not os.path.exists(start_sh_path): self.signals.show_warning_dialog.emit( _("Error"), - _("start.sh not found at {0}").format(start_sh_path) + _("start.sh not found at {path}").format(path=start_sh_path) ) return False - - # Get cover image path image_folder = os.path.join( os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "PortProtonQt", "images" ) cover_path = os.path.join(image_folder, f"{app_name}.jpg") icon_path = cover_path if os.path.exists(cover_path) else "" - - # Create egs_desktops directory egs_desktop_dir = os.path.join(self.portproton_location, "egs_desktops") os.makedirs(egs_desktop_dir, exist_ok=True) - - # Create .desktop file with direct Exec line desktop_path = self._get_egs_desktop_path(game_name) desktop_entry = f"""[Desktop Entry] Type=Application @@ -448,12 +399,12 @@ Categories=Game with open(desktop_path, "w", encoding="utf-8") as f: f.write(desktop_entry) os.chmod(desktop_path, 0o755) - logger.info(f"Created .desktop file for EGS game: {desktop_path}") + logger.info("Created .desktop file for EGS game: %s", desktop_path) return True except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to create .desktop file: {0}").format(str(e)) + _("Failed to create .desktop file: {error}").format(error=str(e)) ) return False @@ -467,31 +418,25 @@ Categories=Game """ if not self._check_portproton(): return - desktop_path = self._get_egs_desktop_path(game_name) if not os.path.exists(desktop_path): - # Create the .desktop file if it doesn't exist if not self._create_egs_desktop_file(game_name, app_name): return - desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip() os.makedirs(desktop_dir, exist_ok=True) dest_path = os.path.join(desktop_dir, f"{game_name}.desktop") - try: shutil.copyfile(desktop_path, dest_path) os.chmod(dest_path, 0o755) - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Game '{0}' added to desktop").format(game_name), 3000 - ) - logger.debug("Direct status message: Game '{0}' added to desktop", game_name) - else: - logger.warning("Status bar not available when adding '{0}' to desktop", game_name) + self._show_status_message(_("Added '{game_name}' to {location}").format( + game_name=game_name, location=_("Desktop") + )) except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game to desktop: {0}").format(game_name, str(e)) + _("Failed to add '{game_name}' to {location}: {error}").format( + game_name=game_name, location=_("Desktop"), error=str(e) + ) ) def remove_egs_from_desktop(self, game_name: str): @@ -505,9 +450,10 @@ Categories=Game desktop_path = os.path.join(desktop_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game '{0}' from Desktop: {{0}}").format(game_name), - _("Successfully removed game '{0}' from Desktop").format(game_name), - game_name + _("Failed to remove '{game_name}' from {location}: {error}"), + _("Removed '{game_name}' from {location}"), + game_name, + location=_("Desktop") ) def add_egs_to_menu(self, game_name: str, app_name: str): @@ -520,31 +466,25 @@ Categories=Game """ if not self._check_portproton(): return - desktop_path = self._get_egs_desktop_path(game_name) if not os.path.exists(desktop_path): - # Create the .desktop file if it doesn't exist if not self._create_egs_desktop_file(game_name, app_name): return - applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications") os.makedirs(applications_dir, exist_ok=True) dest_path = os.path.join(applications_dir, f"{game_name}.desktop") - try: shutil.copyfile(desktop_path, dest_path) os.chmod(dest_path, 0o755) - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Game '{0}' added to menu").format(game_name), 3000 - ) - logger.debug("Direct status message: Game '{0}' added to menu", game_name) - else: - logger.warning("Status bar not available when adding '{0}' to menu", game_name) + self._show_status_message(_("Added '{game_name}' to {location}").format( + game_name=game_name, location=_("Menu") + )) except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game '{0}' to menu: {1}").format(game_name, str(e)) + _("Failed to add '{game_name}' to {location}: {error}").format( + game_name=game_name, location=_("Menu"), error=str(e) + ) ) def remove_egs_from_menu(self, game_name: str): @@ -558,16 +498,16 @@ Categories=Game desktop_path = os.path.join(applications_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game '{0}' from menu: {{0}}").format(game_name), - _("Successfully removed game '{0}' from menu").format(game_name), - game_name + _("Failed to remove '{game_name}' from {location}: {error}"), + _("Removed '{game_name}' from {location}"), + game_name, + location=_("Menu") ) def _get_exec_line(self, game_name, exec_line): """Retrieve and validate exec_line from .desktop file if necessary.""" if exec_line and exec_line.strip() != "full": return exec_line - desktop_path = self._get_desktop_path(game_name) if os.path.exists(desktop_path): try: @@ -577,19 +517,19 @@ Categories=Game if not exec_line: self.signals.show_warning_dialog.emit( _("Error"), - _("No executable command found in .desktop file for game: {0}").format(game_name) + _("No executable command in .desktop file for '{game_name}'").format(game_name=game_name) ) return None else: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to parse .desktop file for game: {0}").format(game_name) + _("Failed to parse .desktop file for '{game_name}'").format(game_name=game_name) ) return None except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to read .desktop file: {0}").format(str(e)) + _("Failed to read .desktop file: {error}").format(error=str(e)) ) return None else: @@ -601,7 +541,7 @@ Categories=Game return exec_line self.signals.show_warning_dialog.emit( _("Error"), - _(".desktop file not found for game: {0}").format(game_name) + _("No .desktop file found for '{game_name}'").format(game_name=game_name) ) return None return exec_line @@ -613,7 +553,7 @@ Categories=Game if not entry_exec_split: self.signals.show_warning_dialog.emit( _("Error"), - _("Invalid executable command: {0}").format(exec_line) + _("Invalid executable command: {exec_line}").format(exec_line=exec_line) ) return None if entry_exec_split[0] == "env" and len(entry_exec_split) >= 3: @@ -625,31 +565,27 @@ Categories=Game if not exe_path or not os.path.exists(exe_path): self.signals.show_warning_dialog.emit( _("Error"), - _("Executable file not found: {0}").format(exe_path or "None") + _("Executable not found: {path}").format(path=exe_path or "None") ) return None return exe_path except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to parse executable command: {0}").format(str(e)) + _("Failed to parse executable: {error}").format(error=str(e)) ) return None - def _remove_file(self, file_path, error_message, success_message, game_name): + def _remove_file(self, file_path, error_message, success_message, game_name, location=""): """Remove a file and handle errors.""" try: os.remove(file_path) - if self.parent.statusBar(): - self.parent.statusBar().showMessage(success_message.format(game_name), 3000) - logger.debug("Direct status message: %s", success_message.format(game_name)) - else: - logger.warning("Status bar not available when removing file for '%s'", game_name) + self._show_status_message(_(success_message).format(game_name=game_name, location=location)) return True except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - error_message.format(str(e)) + _(error_message).format(game_name=game_name, location=location, error=str(e)) ) return False @@ -658,40 +594,33 @@ Categories=Game reply = QMessageBox.question( self.parent, _("Confirm Deletion"), - _("Are you sure you want to delete '{0}'? This will remove the .desktop file and custom data.") - .format(game_name), + _("Are you sure you want to delete '{game_name}'? This will remove the .desktop file and custom data.").format(game_name=game_name), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return - if not self._check_portproton(): return - desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): self.signals.show_warning_dialog.emit( _("Error"), - _("Could not locate .desktop file for game: {0}").format(game_name) + _("No .desktop file found for '{game_name}'").format(game_name=game_name) ) return - exec_line = self._get_exec_line(game_name, exec_line) if not exec_line: return - exe_path = self._parse_exe_path(exec_line, game_name) exe_name = os.path.splitext(os.path.basename(exe_path))[0] if exe_path else None - if not self._remove_file( desktop_path, - _("Failed to delete .desktop file: {0}"), - _("Game '{0}' deleted successfully"), + _("Failed to delete .desktop file: {error}"), + _("Deleted '{game_name}' successfully"), game_name ): return - if exe_name: xdg_data_home = os.getenv( "XDG_DATA_HOME", @@ -704,156 +633,166 @@ Categories=Game except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to delete custom data: {0}").format(str(e)) + _("Failed to delete custom data: {error}").format(error=str(e)) ) def add_to_menu(self, game_name, exec_line): """Copy the .desktop file to ~/.local/share/applications.""" if not self._check_portproton(): return - desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): self.signals.show_warning_dialog.emit( _("Error"), - _("Could not locate .desktop file for game: {0}").format(game_name) + _("No .desktop file found for '{game_name}'").format(game_name=game_name) ) return - applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications") os.makedirs(applications_dir, exist_ok=True) dest_path = os.path.join(applications_dir, f"{game_name}.desktop") - try: shutil.copyfile(desktop_path, dest_path) os.chmod(dest_path, 0o755) - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Game '{0}' added to menu").format(game_name), 3000 - ) - logger.debug("Direct status message: Game '{0}' added to menu", game_name) - else: - logger.warning("Status bar not available when adding '{0}' to menu", game_name) + self._show_status_message(_("Added '{game_name}' to {location}").format( + game_name=game_name, location=_("Menu") + )) except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game '{0}' to menu: {1}").format(game_name, str(e)) + _("Failed to add '{game_name}' to {location}: {error}").format( + game_name=game_name, location=_("Menu"), error=str(e) + ) ) def remove_from_menu(self, game_name): - """Remove the .desktop file from ~/.local/share/applications.""" + """ + Removes the game from the menu by removing its .desktop file from ~/.local/share/applications. + + Args: + game_name: The display name of the game. + """ applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications") desktop_path = os.path.join(applications_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game '{0}' from menu: {{0}}").format(game_name), - _("Successfully removed game '{0}' from menu").format(game_name), - game_name + _("Failed to remove '{game_name}' from {location}: {error}"), + _("Removed '{game_name}' from {location}"), + game_name, + location=_("Menu") ) def add_to_desktop(self, game_name, exec_line): - """Copy the .desktop file to Desktop folder.""" + """ + Copies the .desktop file to the user's Desktop folder. + + Args: + game_name: The display name of the game. + exec_line: The executable command line for the game. + """ if not self._check_portproton(): return - desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): self.signals.show_warning_dialog.emit( _("Error"), - _("Could not locate .desktop file for game: {0}").format(game_name) + _("No .desktop file found for '{game_name}'").format(game_name=game_name) ) return - desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip() os.makedirs(desktop_dir, exist_ok=True) dest_path = os.path.join(desktop_dir, f"{game_name}.desktop") - try: shutil.copyfile(desktop_path, dest_path) os.chmod(dest_path, 0o755) - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Game '{0}' added to desktop").format(game_name), 3000 - ) - logger.debug("Direct status message: Game '{0}' added to desktop", game_name) - else: - logger.warning("Status bar not available when adding '{0}' to desktop", game_name) + self._show_status_message(_("Added '{game_name}' to {location}").format( + game_name=game_name, location=_("Desktop") + )) except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game '{0}' to desktop: {1}").format(game_name, str(e)) + _("Failed to add '{game_name}' to {location}: {error}").format( + game_name=game_name, location=_("Desktop"), error=str(e) + ) ) def remove_from_desktop(self, game_name): - """Remove the .desktop file from Desktop folder.""" + """ + Removes the game from the Desktop folder by removing its .desktop file. + + Args: + game_name: The display name of the game. + """ desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip() desktop_path = os.path.join(desktop_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game '{0}' from Desktop: {{0}}").format(game_name), - _("Successfully removed game '{0}' from Desktop").format(game_name), - game_name + _("Failed to remove '{game_name}' from {location}: {error}"), + _("Removed '{game_name}' from {location}"), + game_name, + location=_("Desktop") ) def edit_game_shortcut(self, game_name, exec_line, cover_path): - """Opens the AddGameDialog in edit mode to modify an existing .desktop file.""" + """ + Opens a dialog allowing the user to edit a game shortcut in edit mode to modify an existing .desktop file. + + Args: + game_name: The display name of the game. + exec_line: The executable command line of the game. + cover_path: The path to the game's cover image. + """ if not self._check_portproton(): return - exec_line = self._get_exec_line(game_name, exec_line) if not exec_line: return - exe_path = self._parse_exe_path(exec_line, game_name) if not exe_path: return - dialog = AddGameDialog( parent=self.parent, theme=self.theme, edit_mode=True, game_name=game_name, exe_path=exe_path, - cover_path=cover_path + cover_path=cover_path, ) if dialog.exec() == QDialog.DialogCode.Accepted: new_name = dialog.nameEdit.text().strip() new_exe_path = dialog.exeEdit.text().strip() new_cover_path = dialog.coverEdit.text().strip() - if not new_name or not new_exe_path: self.signals.show_warning_dialog.emit( _("Error"), - _("Game name and executable path are required.") + _("Game name and executable path are required") ) return - desktop_entry, new_desktop_path = dialog.getDesktopEntryData() if not desktop_entry or not new_desktop_path: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to generate .desktop file data.") + _("Failed to generate .desktop file data") ) return - old_desktop_path = self._get_desktop_path(game_name) - if game_name != new_name and os.path.exists(old_desktop_path): + old_path = self._get_desktop_path(game_name) + if game_name != new_name and os.path.exists(old_path): self._remove_file( - old_desktop_path, - _("Failed to remove old .desktop file: {0}"), - _("Old .desktop file removed for '{0}'"), + old_path, + _("Failed to delete old .desktop file: {error}"), + _("Removed old .desktop file for '{game_name}'"), game_name ) try: with open(new_desktop_path, "w", encoding="utf-8") as f: f.write(desktop_entry) - os.chmod(new_desktop_path, 0o755) + os.chmod(new_desktop_path, 0o755) except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to save .desktop file: {0}").format(str(e)) + _("Failed to save .desktop file: {error}").format(error=str(e)) ) return @@ -865,7 +804,6 @@ Categories=Game ) custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name) os.makedirs(custom_folder, exist_ok=True) - ext = os.path.splitext(new_cover_path)[1].lower() if ext in [".png", ".jpg", ".jpeg", ".bmp"]: try: @@ -873,35 +811,40 @@ Categories=Game except OSError as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to copy cover image: {0}").format(str(e)) + _("Failed to copy cover image: {error}").format(error=str(e)) ) return def add_to_steam(self, game_name, exec_line, cover_path): - """Handle adding a non-Steam game to Steam via steam_api.""" + """ + Adds a non-Steam game to Steam using steam_api. + + Args: + game_name: The display name of the game. + exec_line: The executable command line. + cover_path: Path to the cover image. + """ if not self._check_portproton(): return - exec_line = self._get_exec_line(game_name, exec_line) if not exec_line: return - exe_path = self._parse_exe_path(exec_line, game_name) if not exe_path: return - - logger.debug("Adding '{0}' to Steam", game_name) - + logger.debug("Adding '%s' to Steam", game_name) try: - add_to_steam(game_name, exec_line, cover_path) + success, message = add_to_steam(game_name, exec_line, cover_path) self.signals.show_info_dialog.emit( _("Success"), - _("'{0}' was added to Steam. Please restart Steam for changes to take effect.").format(game_name) + _("'{game_name}' was added to Steam. Please restart Steam for changes to take effect.").format(game_name=game_name) ) except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game '{0}' to Steam: {1}").format(game_name, str(e)) + _("Failed to add '{game_name}' to Steam: {error}").format( + game_name=game_name, error=str(e) + ) ) def remove_from_steam(self, game_name, exec_line, game_source): @@ -914,10 +857,13 @@ Categories=Game if success: self.signals.show_info_dialog.emit( _("Success"), - _("'{0}' was removed from Steam. Please restart Steam for changes to take effect.").format(game_name) + _("'{game_name}' was removed from Steam. Please restart Steam for changes to take effect.").format(game_name=game_name) ) else: - self.signals.show_warning_dialog.emit(_("Error"), message) + self.signals.show_warning_dialog.emit( + _("Error"), + _(message).format(game_name=game_name) + ) if game_source == "epic": # For EGS games, construct the script path used in Steam shortcuts.vdf @@ -961,11 +907,11 @@ Categories=Game # Backup shortcuts.vdf try: shutil.copy2(steam_shortcuts_path, backup_path) - logger.info(f"Created backup of shortcuts.vdf at {backup_path}") + logger.info("Created backup of shortcuts.vdf at %s", backup_path) except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to create backup of shortcuts.vdf: {0}").format(str(e)) + _("Failed to create backup of shortcuts.vdf: {error}").format(error=str(e)) ) return @@ -976,7 +922,7 @@ Categories=Game except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to load shortcuts.vdf: {0}").format(str(e)) + _("Failed to load shortcuts.vdf: {error}").format(error=str(e)) ) return @@ -988,7 +934,7 @@ Categories=Game for _key, entry in shortcuts.items(): if entry.get("AppName") == game_name and entry.get("Exe") == quoted_script_path: modified = True - logger.info(f"Removing EGS game '{game_name}' from Steam shortcuts") + logger.info("Removing EGS game '%s' from Steam shortcuts", game_name) continue new_shortcuts[str(index)] = entry index += 1 @@ -996,7 +942,7 @@ Categories=Game if not modified: self.signals.show_warning_dialog.emit( _("Error"), - _("Game '{0}' not found in Steam shortcuts").format(game_name) + _("Game '{game_name}' not found in Steam shortcuts").format(game_name=game_name) ) return @@ -1004,90 +950,85 @@ Categories=Game try: with open(steam_shortcuts_path, 'wb') as f: vdf.binary_dump({"shortcuts": new_shortcuts}, f) - logger.info(f"Updated shortcuts.vdf, removed '{game_name}'") - on_remove_from_steam_result((True, f"Game '{game_name}' removed from Steam")) + logger.info("Updated shortcuts.vdf, removed '%s'", game_name) + on_remove_from_steam_result((True, "Game '{game_name}' was removed from Steam. Please restart Steam for changes to take effect.")) except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to update shortcuts.vdf: {0}").format(str(e)) + _("Failed to update shortcuts.vdf: {error}").format(error=str(e)) ) if os.path.exists(backup_path): try: shutil.copy2(backup_path, steam_shortcuts_path) logger.info("Restored shortcuts.vdf from backup") except Exception as restore_err: - logger.error(f"Failed to restore shortcuts.vdf: {restore_err}") - on_remove_from_steam_result((False, f"Failed to update shortcuts.vdf: {e}")) + logger.error("Failed to restore shortcuts.vdf: %s", restore_err) + on_remove_from_steam_result((False, "Failed to update shortcuts.vdf: {error}")) return # Optionally, remove the script file if os.path.exists(script_path): try: os.remove(script_path) - logger.info(f"Removed EGS script: {script_path}") - except Exception as e: - logger.warning(f"Failed to remove EGS script {script_path}: {e}") + logger.info("Removed EGS script: %s", script_path) + except OSError as e: + logger.warning(f"Failed to remove EGS script '{script_path}': {str(e)}") except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to remove EGS game '{0}' from Steam: {1}").format(game_name, str(e)) + _("Failed to remove EGS game '{game_name}' from Steam: {error}").format( + game_name=game_name, error=str(e) + ) ) - on_remove_from_steam_result((False, f"Failed to remove EGS game '{game_name}' from Steam: {str(e)}")) + on_remove_from_steam_result((False, "Failed to remove EGS game '{game_name}' from Steam: {error}")) return else: - # For non-EGS games, use the existing logic without callback + # For non-EGS games, use steam_api exec_line = self._get_exec_line(game_name, exec_line) if not exec_line: return - exe_path = self._parse_exe_path(exec_line, game_name) if not exe_path: return - - if self.parent.statusBar(): - logger.debug("Direct status message: Removing '{0}' from Steam", game_name) - else: - logger.warning("Status bar not available when removing '{0}' from Steam", game_name) - + logger.debug("Removing non-EGS game '%s' from Steam", game_name) try: - remove_from_steam(game_name, exec_line) + success, message = remove_from_steam(game_name, exec_line) self.signals.show_info_dialog.emit( _("Success"), - _("'{0}' was removed from Steam. Please restart Steam for changes to take effect.").format(game_name) + _("'{game_name}' was removed from Steam. Please restart Steam for changes to take effect.").format(game_name=game_name) ) except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to remove game '{0}' from Steam: {1}").format(game_name, str(e)) + _("Failed to remove game '{game_name}' from Steam: {error}").format( + game_name=game_name, error=str(e) + ) ) def open_game_folder(self, game_name, exec_line): - """Open the folder containing the game's executable.""" + """ + Opens the folder containing the game's executable. + + Args: + game_name: The display name of the game. + exec_line: The executable command line. + """ if not self._check_portproton(): return - exec_line = self._get_exec_line(game_name, exec_line) if not exec_line: return - exe_path = self._parse_exe_path(exec_line, game_name) if not exe_path: return - try: folder_path = os.path.dirname(os.path.abspath(exe_path)) QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Successfully opened folder for '{0}'").format(game_name), 3000 - ) - logger.debug("Direct status message: Opened folder for '{0}'", game_name) - else: - logger.warning("Status bar not available when opening folder for '{0}'", game_name) + self._show_status_message(_("Opened folder for '{game_name}'").format(game_name=game_name)) except Exception as e: self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to open game folder: {0}").format(str(e)) + _("Failed to open folder: {error}").format(error=str(e)) )