From f8de5ec58912bbf7ae093700e93a49cfc641af4e Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Thu, 12 Jun 2025 10:24:52 +0500 Subject: [PATCH 01/13] Revert "feat: hide the games from EGS until after the workout" This reverts commit a21705da154b4cf896cb67a08cd6b8262164daac. Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 88 ++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 7 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 5e39059..7729442 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -325,19 +325,25 @@ class MainWindow(QMainWindow): self.update_status_message.emit ) elif display_filter == "favorites": - def on_all_games(portproton_games, steam_games): - games = [game for game in portproton_games + steam_games if game[0] in favorites] + def on_all_games(portproton_games, steam_games, epic_games): + games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites] self.games_loaded.emit(games) self._load_portproton_games_async( lambda pg: self._load_steam_games_async( - lambda sg: on_all_games(pg, sg) + lambda sg: load_egs_games_async( + self.legendary_path, + lambda eg: on_all_games(pg, sg, eg), + self.downloader, + self.update_progress.emit, + self.update_status_message.emit + ) ) ) else: - def on_all_games(portproton_games, steam_games): + def on_all_games(portproton_games, steam_games, epic_games): seen = set() games = [] - for game in portproton_games + steam_games: + for game in portproton_games + steam_games + epic_games: # Уникальный ключ: имя + exec_line key = (game[0], game[4]) if key not in seen: @@ -346,7 +352,13 @@ class MainWindow(QMainWindow): self.games_loaded.emit(games) self._load_portproton_games_async( lambda pg: self._load_steam_games_async( - lambda sg: on_all_games(pg, sg) + lambda sg: load_egs_games_async( + self.legendary_path, + lambda eg: on_all_games(pg, sg, eg), + self.downloader, + self.update_progress.emit, + self.update_status_message.emit + ) ) ) return [] @@ -998,7 +1010,7 @@ class MainWindow(QMainWindow): # 3. Games display_filter self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"] - self.filter_labels = [_("all"), "steam", "portproton", _("favorites")] + self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"] self.gamesDisplayCombo = QComboBox() self.gamesDisplayCombo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.gamesDisplayCombo.addItems(self.filter_labels) @@ -1081,6 +1093,37 @@ class MainWindow(QMainWindow): self.gamepadRumbleCheckBox.setChecked(current_rumble_state) formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox) + # 8. Legendary Authentication + self.legendaryAuthButton = AutoSizeButton( + _("Open Legendary Login"), + icon=self.theme_manager.get_icon("login") + ) + self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.legendaryAuthButton.clicked.connect(self.openLegendaryLogin) + self.legendaryAuthTitle = QLabel(_("Legendary Authentication:")) + self.legendaryAuthTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE) + self.legendaryAuthTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) + formLayout.addRow(self.legendaryAuthTitle, self.legendaryAuthButton) + + self.legendaryCodeEdit = QLineEdit() + self.legendaryCodeEdit.setPlaceholderText(_("Enter Legendary Authorization Code")) + self.legendaryCodeEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE) + self.legendaryCodeEdit.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.legendaryCodeTitle = QLabel(_("Authorization Code:")) + self.legendaryCodeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE) + self.legendaryCodeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) + formLayout.addRow(self.legendaryCodeTitle, self.legendaryCodeEdit) + + self.submitCodeButton = AutoSizeButton( + _("Submit Code"), + icon=self.theme_manager.get_icon("save") + ) + self.submitCodeButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + self.submitCodeButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.submitCodeButton.clicked.connect(self.submitLegendaryCode) + formLayout.addRow(QLabel(""), self.submitCodeButton) + layout.addLayout(formLayout) # Кнопки @@ -1131,6 +1174,37 @@ class MainWindow(QMainWindow): logger.error(f"Failed to open Legendary login page: {e}") self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000) + def submitLegendaryCode(self): + """Submits the Legendary authorization code using the legendary CLI.""" + auth_code = self.legendaryCodeEdit.text().strip() + if not auth_code: + QMessageBox.warning(self, _("Error"), _("Please enter an authorization code")) + return + + try: + # Execute legendary auth command + result = subprocess.run( + [self.legendary_path, "auth", "--code", auth_code], + capture_output=True, + text=True, + check=True + ) + logger.info("Legendary authentication successful: %s", result.stdout) + self.statusBar().showMessage(_("Successfully authenticated with Legendary"), 3000) + self.legendaryCodeEdit.clear() + # Reload Epic Games Store games after successful authentication + self.games = self.loadGames() + self.updateGameGrid() + except subprocess.CalledProcessError as e: + logger.error("Legendary authentication failed: %s", e.stderr) + self.statusBar().showMessage(_("Legendary authentication failed: {0}").format(e.stderr), 5000) + except FileNotFoundError: + logger.error("Legendary executable not found at %s", self.legendary_path) + self.statusBar().showMessage(_("Legendary executable not found"), 5000) + except Exception as e: + logger.error("Unexpected error during Legendary authentication: %s", str(e)) + self.statusBar().showMessage(_("Unexpected error during authentication"), 5000) + def resetSettings(self): """Сбрасывает настройки и перезапускает приложение.""" reply = QMessageBox.question( -- 2.49.0 From ce097e489b3ead893f90ef9207151b6082909ca4 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Thu, 12 Jun 2025 22:17:02 +0500 Subject: [PATCH 02/13] feat: added handle egs games to toggleGame Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 74 ++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 7729442..497fbb5 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -1897,11 +1897,67 @@ class MainWindow(QMainWindow): self.target_exe = None def toggleGame(self, exec_line, button=None): + # Обработка Steam-игр if exec_line.startswith("steam://"): url = QUrl(exec_line) QDesktopServices.openUrl(url) return + # Обработка EGS-игр + if exec_line.startswith("legendary:launch:"): + # Извлекаем app_name из exec_line + app_name = exec_line.split("legendary:launch:")[1] + legendary_path = self.legendary_path # Путь к legendary + + # Формируем переменные окружения + env_vars = os.environ.copy() + env_vars['START_FROM_STEAM'] = '1' + env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path + + wrapper = "flatpak run ru.linux_gaming.PortProton" + if self.portproton_location is not None and ".var" not in self.portproton_location: + start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh") + wrapper = start_sh + + # Формируем команду + cmd = [ + legendary_path, "launch", app_name, "--no-wine", "--wrapper", wrapper + ] + + current_exe = os.path.basename(legendary_path) + if self.game_processes and self.target_exe is not None and self.target_exe != current_exe: + QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running")) + return + + # Обновляем кнопку + update_button = button if button is not None else self.current_play_button + self.current_running_button = update_button + self.target_exe = current_exe + exe_name = app_name # Используем app_name для EGS-игр + + # Запускаем процесс + try: + process = subprocess.Popen(cmd, env=env_vars, shell=False, preexec_fn=os.setsid) + self.game_processes.append(process) + save_last_launch(exe_name, datetime.now()) + if update_button: + update_button.setText(_("Launching")) + icon = self.theme_manager.get_icon("stop") + if isinstance(icon, str): + icon = QIcon(icon) + elif icon is None: + icon = QIcon() + update_button.setIcon(icon) + + self.checkProcessTimer = QTimer(self) + self.checkProcessTimer.timeout.connect(self.checkTargetExe) + self.checkProcessTimer.start(500) + except Exception as e: + logger.error(f"Failed to launch EGS game {app_name}: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e))) + return + + # Обработка PortProton-игр entry_exec_split = shlex.split(exec_line) if entry_exec_split[0] == "env": if len(entry_exec_split) < 3: @@ -1915,18 +1971,20 @@ class MainWindow(QMainWindow): file_to_check = entry_exec_split[3] else: file_to_check = entry_exec_split[0] + if not os.path.exists(file_to_check): QMessageBox.warning(self, _("Error"), _("File not found: {0}").format(file_to_check)) return - current_exe = os.path.basename(file_to_check) + current_exe = os.path.basename(file_to_check) if self.game_processes and self.target_exe is not None and self.target_exe != current_exe: QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running")) return + # Обновляем кнопку update_button = button if button is not None else self.current_play_button - # Если игра уже запущена для этого exe – останавливаем её по нажатию кнопки + # Если игра уже запущена для этого exe – останавливаем её if self.game_processes and self.target_exe == current_exe: if hasattr(self, 'input_manager'): self.input_manager.enable_gamepad_handling() @@ -1979,6 +2037,15 @@ class MainWindow(QMainWindow): env_vars['START_FROM_STEAM'] = '1' elif entry_exec_split[0] == "flatpak": env_vars['START_FROM_STEAM'] = '1' + return + + # Запускаем игру + self.current_running_button = update_button + self.target_exe = current_exe + exe_name = os.path.splitext(current_exe)[0] + env_vars = os.environ.copy() + env_vars['START_FROM_STEAM'] = '1' + try: process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid) self.game_processes.append(process) save_last_launch(exe_name, datetime.now()) @@ -1994,6 +2061,9 @@ class MainWindow(QMainWindow): self.checkProcessTimer = QTimer(self) self.checkProcessTimer.timeout.connect(self.checkTargetExe) self.checkProcessTimer.start(500) + except Exception as e: + logger.error(f"Failed to launch game {exe_name}: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e))) def closeEvent(self, event): """Завершает все дочерние процессы и сохраняет настройки при закрытии окна.""" -- 2.49.0 From 2875efb0501ef68732da0c707c6569d187123583 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Fri, 13 Jun 2025 18:24:56 +0500 Subject: [PATCH 03/13] feat: replace steam placeholder icon to real egs icon Signed-off-by: Boris Yumankulov --- portprotonqt/game_card.py | 2 +- portprotonqt/main_window.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/portprotonqt/game_card.py b/portprotonqt/game_card.py index 7b43f25..d480ab5 100644 --- a/portprotonqt/game_card.py +++ b/portprotonqt/game_card.py @@ -171,7 +171,7 @@ class GameCard(QFrame): self.steamLabel.setVisible(self.steam_visible) # Epic Games Store бейдж - egs_icon = self.theme_manager.get_icon("steam") + egs_icon = self.theme_manager.get_icon("epic_games") self.egsLabel = ClickableLabel( "Epic Games", icon=egs_icon, diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 497fbb5..bf9c8bb 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -1600,7 +1600,7 @@ class MainWindow(QMainWindow): steamLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://steamcommunity.com/app/{appid}"))) # Epic Games Store бейдж - egs_icon = self.theme_manager.get_icon("steam") + egs_icon = self.theme_manager.get_icon("epic_games") egsLabel = ClickableLabel( "Epic Games", icon=egs_icon, -- 2.49.0 From 70dca2b70476b1f27e532fa6c16933f62881070c Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 15 Jun 2025 20:47:09 +0500 Subject: [PATCH 04/13] feat: added import to context menu Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 77 +++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index 4e5772b..a9f8c05 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -3,7 +3,7 @@ import shlex import glob import shutil import subprocess -from PySide6.QtWidgets import QMessageBox, QDialog, QMenu +from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog from PySide6.QtCore import QUrl, QPoint from PySide6.QtGui import QDesktopServices from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites @@ -53,6 +53,12 @@ class ContextMenuManager: favorite_action = menu.addAction(_("Add to Favorites")) favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True)) + if game_card.game_source == "epic": + import_action = menu.addAction(_("Import to Legendary")) + import_action.triggered.connect( + lambda: self.import_to_legendary(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") @@ -92,6 +98,75 @@ class ContextMenuManager: menu.exec(game_card.mapToGlobal(pos)) + def import_to_legendary(self, game_name, app_name): + """ + Imports an installed Epic Games Store game to Legendary using the provided app_name. + + Args: + game_name: The display name of the game. + app_name: The Legendary app_name (unique identifier for the game). + """ + if not self._check_portproton(): + return + + # Открываем диалог для выбора папки с установленной игрой + folder_path = QFileDialog.getExistingDirectory( + self.parent, + _("Select Game Installation Folder"), + os.path.expanduser("~") + ) + if not folder_path: + self.parent.statusBar().showMessage(_("No folder selected"), 3000) + return + + # Путь к legendary + legendary_path = os.path.join( + os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), + "PortProtonQt", "legendary_cache", "legendary" + ) + if not os.path.exists(legendary_path): + QMessageBox.warning( + self.parent, + _("Error"), + _("Legendary executable not found at {0}").format(legendary_path) + ) + return + + # Формируем команду для импорта + cmd = [legendary_path, "import", app_name, folder_path] + + try: + # Выполняем команду legendary import + subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + self.parent.statusBar().showMessage( + _("Successfully imported '{0}' to Legendary").format(game_name), 3000 + ) + + + except subprocess.CalledProcessError as e: + QMessageBox.warning( + self.parent, + _("Error"), + _("Failed to import '{0}' to Legendary: {1}").format(game_name, e.stderr) + ) + except FileNotFoundError: + QMessageBox.warning( + self.parent, + _("Error"), + _("Legendary executable not found") + ) + except Exception as e: + QMessageBox.warning( + self.parent, + _("Error"), + _("Unexpected error during import: {0}").format(str(e)) + ) + def toggle_favorite(self, game_card, add: bool): """ Toggle the favorite status of a game and update its icon. -- 2.49.0 From 43e7d5b65b1752b905a6a6f17745eb16ad445c24 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 16 Jun 2025 22:51:55 +0500 Subject: [PATCH 05/13] fix: prevent premature game termination detection for EGS games Signed-off-by: Boris Yumankulov --- portprotonqt/egs_api.py | 23 ++++++++ portprotonqt/main_window.py | 107 +++++++++++++++++++++++++----------- 2 files changed, 98 insertions(+), 32 deletions(-) diff --git a/portprotonqt/egs_api.py b/portprotonqt/egs_api.py index 8f12c3f..7d15487 100644 --- a/portprotonqt/egs_api.py +++ b/portprotonqt/egs_api.py @@ -16,6 +16,29 @@ from PySide6.QtGui import QPixmap logger = get_logger(__name__) +def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None: + """Получает путь к исполняемому файлу EGS-игры из installed.json с использованием orjson.""" + installed_json_path = os.path.join(legendary_config_path, "installed.json") + try: + with open(installed_json_path, "rb") as f: + installed_data = orjson.loads(f.read()) + if app_name in installed_data: + install_path = installed_data[app_name].get("install_path", "").decode('utf-8') if isinstance(installed_data[app_name].get("install_path"), bytes) else installed_data[app_name].get("install_path", "") + executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "") + if install_path and executable: + return os.path.join(install_path, executable) + logger.warning(f"No executable found for EGS app_name: {app_name}") + return None + except FileNotFoundError: + logger.error(f"installed.json not found at {installed_json_path}") + return None + except orjson.JSONDecodeError: + logger.error(f"Invalid JSON in {installed_json_path}") + return None + except Exception as e: + logger.error(f"Error reading installed.json: {e}") + return None + def get_cache_dir() -> Path: """Returns the path to the cache directory, creating it if necessary.""" xdg_cache_home = os.getenv( diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index bf9c8bb..ce8a7a8 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -17,7 +17,7 @@ from portprotonqt.system_overlay import SystemOverlay from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games -from portprotonqt.egs_api import load_egs_games_async +from portprotonqt.egs_api import load_egs_games_async, get_egs_executable from portprotonqt.theme_manager import ThemeManager, load_theme_screenshots, load_logo from portprotonqt.time_utils import save_last_launch, get_last_launch, parse_playtime_file, format_playtime, get_last_launch_timestamp, format_last_launch from portprotonqt.config_utils import ( @@ -1905,26 +1905,15 @@ class MainWindow(QMainWindow): # Обработка EGS-игр if exec_line.startswith("legendary:launch:"): - # Извлекаем app_name из exec_line app_name = exec_line.split("legendary:launch:")[1] - legendary_path = self.legendary_path # Путь к legendary - # Формируем переменные окружения - env_vars = os.environ.copy() - env_vars['START_FROM_STEAM'] = '1' - env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path + # Получаем путь к .exe из installed.json + game_exe = get_egs_executable(app_name, self.legendary_config_path) + if not game_exe or not os.path.exists(game_exe): + QMessageBox.warning(self, _("Error"), _("Executable not found for EGS game: {0}").format(app_name)) + return - wrapper = "flatpak run ru.linux_gaming.PortProton" - if self.portproton_location is not None and ".var" not in self.portproton_location: - start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh") - wrapper = start_sh - - # Формируем команду - cmd = [ - legendary_path, "launch", app_name, "--no-wine", "--wrapper", wrapper - ] - - current_exe = os.path.basename(legendary_path) + current_exe = os.path.basename(game_exe) if self.game_processes and self.target_exe is not None and self.target_exe != current_exe: QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running")) return @@ -1933,28 +1922,82 @@ class MainWindow(QMainWindow): update_button = button if button is not None else self.current_play_button self.current_running_button = update_button self.target_exe = current_exe - exe_name = app_name # Используем app_name для EGS-игр + exe_name = os.path.splitext(current_exe)[0] - # Запускаем процесс - try: - process = subprocess.Popen(cmd, env=env_vars, shell=False, preexec_fn=os.setsid) - self.game_processes.append(process) - save_last_launch(exe_name, datetime.now()) + # Проверяем, запущена ли игра + if self.game_processes and self.target_exe == current_exe: + # Останавливаем игру + if hasattr(self, 'input_manager'): + self.input_manager.enable_gamepad_handling() + + for proc in self.game_processes: + try: + parent = psutil.Process(proc.pid) + children = parent.children(recursive=True) + for child in children: + try: + child.terminate() + except psutil.NoSuchProcess: + pass + psutil.wait_procs(children, timeout=5) + for child in children: + if child.is_running(): + child.kill() + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + except psutil.NoSuchProcess: + pass + self.game_processes = [] if update_button: - update_button.setText(_("Launching")) - icon = self.theme_manager.get_icon("stop") + update_button.setText(_("Play")) + icon = self.theme_manager.get_icon("play") if isinstance(icon, str): icon = QIcon(icon) elif icon is None: icon = QIcon() update_button.setIcon(icon) + if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None: + self.checkProcessTimer.stop() + self.checkProcessTimer.deleteLater() + self.checkProcessTimer = None + self.current_running_button = None + self.target_exe = None + self._gameLaunched = False + else: + # Запускаем игру через PortProton + env_vars = os.environ.copy() + env_vars['START_FROM_STEAM'] = '1' + env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path - self.checkProcessTimer = QTimer(self) - self.checkProcessTimer.timeout.connect(self.checkTargetExe) - self.checkProcessTimer.start(500) - except Exception as e: - logger.error(f"Failed to launch EGS game {app_name}: {e}") - QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e))) + wrapper = "flatpak run ru.linux_gaming.PortProton" + if self.portproton_location is not None and ".var" not in self.portproton_location: + start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh") + wrapper = start_sh + + cmd = [wrapper, game_exe] + + try: + process = subprocess.Popen(cmd, env=env_vars, shell=False, preexec_fn=os.setsid) + self.game_processes.append(process) + save_last_launch(exe_name, datetime.now()) + if update_button: + update_button.setText(_("Launching")) + icon = self.theme_manager.get_icon("stop") + if isinstance(icon, str): + icon = QIcon(icon) + elif icon is None: + icon = QIcon() + update_button.setIcon(icon) + + # Delay disabling gamepad handling + if hasattr(self, 'input_manager'): + QTimer.singleShot(200, self.input_manager.disable_gamepad_handling) + + self.checkProcessTimer = QTimer(self) + self.checkProcessTimer.timeout.connect(self.checkTargetExe) + self.checkProcessTimer.start(500) + except Exception as e: + logger.error(f"Failed to launch EGS game {app_name}: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e))) return # Обработка PortProton-игр -- 2.49.0 From d9729ebbea766d02a88a9713502662ce9ce95d23 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 16 Jun 2025 23:11:43 +0500 Subject: [PATCH 06/13] feat: added playtime and last launch to EGS Signed-off-by: Boris Yumankulov --- portprotonqt/egs_api.py | 57 ++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/portprotonqt/egs_api.py b/portprotonqt/egs_api.py index 7d15487..c49b8dc 100644 --- a/portprotonqt/egs_api.py +++ b/portprotonqt/egs_api.py @@ -12,6 +12,10 @@ from collections.abc import Callable from portprotonqt.localization import get_egs_language, _ from portprotonqt.logger import get_logger from portprotonqt.image_utils import load_pixmap_async +from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp +from portprotonqt.config_utils import get_portproton_location +from portprotonqt.steam_api import get_weanticheatyet_status_async + from PySide6.QtGui import QPixmap logger = get_logger(__name__) @@ -27,7 +31,6 @@ def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None: executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "") if install_path and executable: return os.path.join(install_path, executable) - logger.warning(f"No executable found for EGS app_name: {app_name}") return None except FileNotFoundError: logger.error(f"installed.json not found at {installed_json_path}") @@ -304,6 +307,7 @@ def get_egs_game_description_async( thread = threading.Thread(target=fetch_description, daemon=True) thread.start() + def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]): """ Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback. @@ -349,6 +353,7 @@ def run_legendary_list_async(legendary_path: str, callback: Callable[[list | Non def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]): """ Асинхронно загружает Epic Games Store игры с использованием legendary CLI. + Читает статистику времени игры и последнего запуска из файла statistics. """ logger.debug("Starting to load Epic Games Store games") games: list[tuple] = [] @@ -357,6 +362,14 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], cache_file = cache_dir / "legendary_games.json" cache_ttl = 3600 # Cache TTL in seconds (1 hour) + # Путь к файлу statistics + portproton_location = get_portproton_location() + if portproton_location is None: + logger.error("PortProton location is not set, cannot locate statistics file") + statistics_file = "" + else: + statistics_file = os.path.join(portproton_location, "data", "tmp", "statistics") + if not os.path.exists(legendary_path): logger.info("Legendary binary not found, downloading...") def on_legendary_downloaded(result): @@ -368,7 +381,7 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], logger.error(f"Failed to make legendary binary executable: {e}") callback(games) # Return empty games list on failure return - _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message) + _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file) else: logger.error("Failed to download legendary binary") callback(games) # Return empty games list on failure @@ -379,9 +392,9 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], callback(games) return else: - _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message) + _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file) -def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]): +def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None], statistics_file: str): """ Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI. """ @@ -433,6 +446,33 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu callback(final_games) return + # Получаем путь к .exe для извлечения имени + game_exe = get_egs_executable(app_name, os.path.dirname(legendary_path)) + exe_name = "" + if game_exe: + exe_name = os.path.splitext(os.path.basename(game_exe))[0] + + # Читаем статистику из файла statistics + playtime_seconds = 0 + formatted_playtime = "" + last_launch = _("Never") + last_launch_timestamp = 0 + if exe_name and os.path.exists(statistics_file): + try: + playtime_data = parse_playtime_file(statistics_file) + matching_key = next( + (key for key in playtime_data if os.path.basename(key).split('.')[0] == exe_name), + None + ) + if matching_key: + playtime_seconds = playtime_data[matching_key] + formatted_playtime = format_playtime(playtime_seconds) + except Exception as e: + logger.error(f"Failed to parse playtime data for {app_name}: {e}") + if exe_name: + last_launch = get_last_launch(exe_name) or _("Never") + last_launch_timestamp = get_last_launch_timestamp(exe_name) + metadata_file = metadata_dir / f"{app_name}.json" cover_url = "" try: @@ -453,7 +493,6 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu final_description = api_description or _("No description available") def on_cover_loaded(pixmap: QPixmap): - from portprotonqt.steam_api import get_weanticheatyet_status_async def on_anticheat_status(status: str): nonlocal pending_images with results_lock: @@ -464,12 +503,12 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu app_name, f"legendary:launch:{app_name}", "", - _("Never"), - "", + last_launch, # Время последнего запуска + formatted_playtime, # Форматированное время игры "", status or "", - 0, - 0, + last_launch_timestamp, # Временная метка последнего запуска + playtime_seconds, # Время игры в секундах "epic" ) pending_images -= 1 -- 2.49.0 From 2d72fdb4c7a0254c032797b5cfd05e07660e9a4c Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 22 Jun 2025 10:37:42 +0500 Subject: [PATCH 07/13] feat(egs-api): add Steam ID Signed-off-by: Boris Yumankulov --- portprotonqt/egs_api.py | 76 +++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/portprotonqt/egs_api.py b/portprotonqt/egs_api.py index c49b8dc..488e695 100644 --- a/portprotonqt/egs_api.py +++ b/portprotonqt/egs_api.py @@ -14,7 +14,7 @@ from portprotonqt.logger import get_logger from portprotonqt.image_utils import load_pixmap_async from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp from portprotonqt.config_utils import get_portproton_location -from portprotonqt.steam_api import get_weanticheatyet_status_async +from portprotonqt.steam_api import get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async, search_app from PySide6.QtGui import QPixmap @@ -354,6 +354,7 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], """ Асинхронно загружает Epic Games Store игры с использованием legendary CLI. Читает статистику времени игры и последнего запуска из файла statistics. + Проверяет наличие игры в Steam для получения ProtonDB статуса. """ logger.debug("Starting to load Epic Games Store games") games: list[tuple] = [] @@ -489,39 +490,54 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu image_folder = os.path.join(os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "PortProtonQt", "images") local_path = os.path.join(image_folder, f"{app_name}.jpg") if cover_url else "" - def on_description_fetched(api_description: str): - final_description = api_description or _("No description available") + def on_steam_apps(steam_data: tuple[list, dict]): + steam_apps, steam_apps_index = steam_data + matching_app = search_app(title, steam_apps_index) + steam_appid = matching_app.get("appid") if matching_app else None - def on_cover_loaded(pixmap: QPixmap): - def on_anticheat_status(status: str): - nonlocal pending_images - with results_lock: - game_results[index] = ( - title, - final_description, - local_path if os.path.exists(local_path) else "", - app_name, - f"legendary:launch:{app_name}", - "", - last_launch, # Время последнего запуска - formatted_playtime, # Форматированное время игры - "", - status or "", - last_launch_timestamp, # Временная метка последнего запуска - playtime_seconds, # Время игры в секундах - "epic" - ) - pending_images -= 1 - update_progress(total_games - pending_images) - if pending_images == 0: - final_games = [game_results[i] for i in sorted(game_results.keys())] - callback(final_games) + def on_protondb_tier(protondb_tier: str): + def on_description_fetched(api_description: str): + final_description = api_description or _("No description available") - get_weanticheatyet_status_async(title, on_anticheat_status) + def on_cover_loaded(pixmap: QPixmap): + def on_anticheat_status(status: str): + nonlocal pending_images + with results_lock: + game_results[index] = ( + title, + final_description, + local_path if os.path.exists(local_path) else "", + app_name, + f"legendary:launch:{app_name}", + "", + last_launch, # Время последнего запуска + formatted_playtime, # Форматированное время игры + protondb_tier, # ProtonDB tier + status or "", + last_launch_timestamp, # Временная метка последнего запуска + playtime_seconds, # Время игры в секундах + "epic" + ) + pending_images -= 1 + update_progress(total_games - pending_images) + if pending_images == 0: + final_games = [game_results[i] for i in sorted(game_results.keys())] + callback(final_games) - load_pixmap_async(cover_url, 600, 900, on_cover_loaded, app_name=app_name) + get_weanticheatyet_status_async(title, on_anticheat_status) - get_egs_game_description_async(title, on_description_fetched) + load_pixmap_async(cover_url, 600, 900, on_cover_loaded, app_name=app_name) + + get_egs_game_description_async(title, on_description_fetched) + + if steam_appid: + logger.info(f"Found Steam appid {steam_appid} for EGS game {title}") + get_protondb_tier_async(steam_appid, on_protondb_tier) + else: + logger.debug(f"No Steam app found for EGS game {title}") + on_protondb_tier("") # Proceed with empty ProtonDB tier + + get_steam_apps_and_index_async(on_steam_apps) max_workers = min(4, len(valid_games)) with ThreadPoolExecutor(max_workers=max_workers) as executor: -- 2.49.0 From 1f4f4093bdf194820d56b9c2c4ae59b0f505f606 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 22 Jun 2025 10:51:06 +0500 Subject: [PATCH 08/13] feat(egs-api): Implement add_egs_to_steam to add EGS games to Steam via shortcuts.vdf Signed-off-by: Boris Yumankulov --- portprotonqt/egs_api.py | 242 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 240 insertions(+), 2 deletions(-) diff --git a/portprotonqt/egs_api.py b/portprotonqt/egs_api.py index 488e695..c9960b9 100644 --- a/portprotonqt/egs_api.py +++ b/portprotonqt/egs_api.py @@ -14,11 +14,18 @@ from portprotonqt.logger import get_logger from portprotonqt.image_utils import load_pixmap_async from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp from portprotonqt.config_utils import get_portproton_location -from portprotonqt.steam_api import get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async, search_app - +from portprotonqt.steam_api import ( + get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async, + search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail +) +import vdf +import shutil +import zlib +from portprotonqt.downloader import Downloader from PySide6.QtGui import QPixmap logger = get_logger(__name__) +downloader = Downloader() def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None: """Получает путь к исполняемому файлу EGS-игры из installed.json с использованием orjson.""" @@ -52,6 +59,237 @@ def get_cache_dir() -> Path: cache_dir.mkdir(parents=True, exist_ok=True) return cache_dir +def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callback: Callable[[tuple[bool, str]], None]) -> None: + """ + Asynchronously adds an EGS game to Steam via shortcuts.vdf with PortProton tag. + Creates a launch script using legendary CLI with --no-wine and PortProton wrapper. + Wrapper is flatpak run if portproton_location is None or contains .var, otherwise start.sh. + Downloads Steam Grid covers if the game exists in Steam, and generates a thumbnail. + Calls the callback with (success, message). + """ + if not app_name or not app_name.strip() or not game_title or not game_title.strip(): + logger.error("Invalid app_name or game_title: empty or whitespace") + callback((False, "Game name or app name is empty or invalid")) + return + + if not os.path.exists(legendary_path): + logger.error(f"Legendary executable not found: {legendary_path}") + callback((False, f"Legendary executable not found: {legendary_path}")) + return + + portproton_dir = get_portproton_location() + if not portproton_dir: + logger.error("PortProton directory not found") + callback((False, "PortProton directory not found")) + return + + # Determine wrapper + wrapper = "flatpak run ru.linux_gaming.PortProton" + start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh") + if portproton_dir is not None and ".var" not in portproton_dir: + wrapper = start_sh_path + if not os.path.exists(start_sh_path): + logger.error(f"start.sh not found at {start_sh_path}") + callback((False, f"start.sh not found at {start_sh_path}")) + return + + # Create launch script + steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts") + os.makedirs(steam_scripts_dir, exist_ok=True) + safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_title.strip()) + script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}_egs.sh") + legendary_config_path = os.path.dirname(legendary_path) + + script_content = f"""#!/usr/bin/env bash +export LD_PRELOAD= +export LEGENDARY_CONFIG_PATH="{legendary_config_path}" +"{legendary_path}" launch {app_name} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}" "$@" +""" + try: + with open(script_path, "w", encoding="utf-8") as f: + f.write(script_content) + os.chmod(script_path, 0o755) + logger.info(f"Created launch script for EGS game: {script_path}") + except Exception as e: + logger.error(f"Failed to create launch script {script_path}: {e}") + callback((False, f"Failed to create launch script: {e}")) + return + + # Generate thumbnail + generated_icon_path = os.path.join(portproton_dir, "data", "img", f"{safe_game_name}_egs.png") + try: + img_dir = os.path.join(portproton_dir, "data", "img") + os.makedirs(img_dir, exist_ok=True) + game_exe = get_egs_executable(app_name, legendary_config_path) + if not game_exe or not os.path.exists(game_exe): + logger.warning(f"Executable not found for {app_name}, skipping thumbnail generation") + icon_path = "" + elif os.path.exists(generated_icon_path): + logger.info(f"Reusing existing thumbnail: {generated_icon_path}") + icon_path = generated_icon_path + else: + success = generate_thumbnail(game_exe, generated_icon_path, size=128, force_resize=True) + if not success or not os.path.exists(generated_icon_path): + logger.warning(f"generate_thumbnail failed for {game_exe}") + icon_path = "" + else: + logger.info(f"Generated thumbnail: {generated_icon_path}") + icon_path = generated_icon_path + except Exception as e: + logger.error(f"Error generating thumbnail for {app_name}: {e}") + icon_path = "" + + # Get Steam directories + steam_home = get_steam_home() + if not steam_home: + logger.error("Steam home directory not found") + callback((False, "Steam directory not found")) + return + + last_user = get_last_steam_user(steam_home) + if not last_user or 'SteamID' not in last_user: + logger.error("Failed to retrieve Steam user ID") + callback((False, "Failed to get Steam user ID")) + return + + userdata_dir = steam_home / "userdata" + user_id = last_user['SteamID'] + unsigned_id = convert_steam_id(user_id) + user_dir = userdata_dir / str(unsigned_id) + steam_shortcuts_path = user_dir / "config" / "shortcuts.vdf" + grid_dir = user_dir / "config" / "grid" + os.makedirs(grid_dir, exist_ok=True) + + # Backup shortcuts.vdf + backup_path = f"{steam_shortcuts_path}.backup" + if os.path.exists(steam_shortcuts_path): + try: + shutil.copy2(steam_shortcuts_path, backup_path) + logger.info(f"Created backup of shortcuts.vdf at {backup_path}") + except Exception as e: + logger.error(f"Failed to create backup of shortcuts.vdf: {e}") + callback((False, f"Failed to create backup of shortcuts.vdf: {e}")) + return + + # Generate unique appid + unique_string = f"{script_path}{game_title}" + baseid = zlib.crc32(unique_string.encode('utf-8')) & 0xffffffff + appid = baseid | 0x80000000 + if appid > 0x7FFFFFFF: + aidvdf = appid - 0x100000000 + else: + aidvdf = appid + + steam_appid = None + downloaded_count = 0 + total_covers = 4 + download_lock = threading.Lock() + + def on_cover_download(cover_file: str, cover_type: str): + nonlocal downloaded_count + try: + if cover_file and os.path.exists(cover_file): + logger.info(f"Downloaded cover {cover_type} to {cover_file}") + else: + logger.warning(f"Failed to download cover {cover_type} for appid {steam_appid}") + except Exception as e: + logger.error(f"Error processing cover {cover_type} for appid {steam_appid}: {e}") + with download_lock: + downloaded_count += 1 + if downloaded_count == total_covers: + finalize_shortcut() + + def finalize_shortcut(): + tags_dict = {'0': 'PortProton'} + shortcut = { + "appid": aidvdf, + "AppName": game_title, + "Exe": f'"{script_path}"', + "StartDir": f'"{os.path.dirname(script_path)}"', + "icon": icon_path, + "LaunchOptions": "", + "IsHidden": 0, + "AllowDesktopConfig": 1, + "AllowOverlay": 1, + "openvr": 0, + "Devkit": 0, + "DevkitGameID": "", + "LastPlayTime": 0, + "tags": tags_dict + } + logger.info(f"Shortcut entry for EGS game: {shortcut}") + + try: + if not os.path.exists(steam_shortcuts_path): + os.makedirs(os.path.dirname(steam_shortcuts_path), exist_ok=True) + open(steam_shortcuts_path, 'wb').close() + + try: + if os.path.getsize(steam_shortcuts_path) > 0: + with open(steam_shortcuts_path, 'rb') as f: + shortcuts_data = vdf.binary_load(f) + else: + shortcuts_data = {"shortcuts": {}} + except Exception as load_err: + logger.warning(f"Failed to load shortcuts.vdf, starting fresh: {load_err}") + shortcuts_data = {"shortcuts": {}} + + shortcuts = shortcuts_data.get("shortcuts", {}) + for _key, entry in shortcuts.items(): + if entry.get("AppName") == game_title and entry.get("Exe") == f'"{script_path}"': + logger.info(f"EGS game '{game_title}' already exists in Steam shortcuts") + callback((False, f"Game '{game_title}' already exists in Steam")) + return + + new_index = str(len(shortcuts)) + shortcuts[new_index] = shortcut + + with open(steam_shortcuts_path, 'wb') as f: + vdf.binary_dump({"shortcuts": shortcuts}, f) + except Exception as e: + logger.error(f"Failed to update shortcuts.vdf: {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}") + callback((False, f"Failed to update shortcuts.vdf: {e}")) + return + + logger.info(f"EGS game '{game_title}' added to Steam") + callback((True, f"Game '{game_title}' added to Steam with covers")) + + def on_steam_apps(steam_data: tuple[list, dict]): + nonlocal steam_appid + steam_apps, steam_apps_index = steam_data + matching_app = search_app(game_title, steam_apps_index) + steam_appid = matching_app.get("appid") if matching_app else None + + if not steam_appid: + logger.info(f"No Steam appid found for EGS game {game_title}, skipping cover download") + finalize_shortcut() + return + + cover_types = [ + (".jpg", "header.jpg"), + ("p.jpg", "library_600x900_2x.jpg"), + ("_hero.jpg", "library_hero.jpg"), + ("_logo.png", "logo.png") + ] + + for suffix, cover_type in cover_types: + cover_file = os.path.join(grid_dir, f"{appid}{suffix}") + cover_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{steam_appid}/{cover_type}" + downloader.download_async( + cover_url, + cover_file, + timeout=5, + callback=lambda result, cfile=cover_file, ctype=cover_type: on_cover_download(cfile, ctype) + ) + + get_steam_apps_and_index_async(on_steam_apps) + def get_egs_game_description_async( app_name: str, callback: Callable[[str], None], -- 2.49.0 From 7185019a3f012fa2b06a30098109ecbdf4cd5434 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 22 Jun 2025 11:55:21 +0500 Subject: [PATCH 09/13] feat: update context menu for egs games Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 640 +++++++++++++++++++++------ 1 file changed, 510 insertions(+), 130 deletions(-) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index a9f8c05..fc80f14 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -3,13 +3,26 @@ import shlex import glob import shutil import subprocess +import threading +import logging +import re +import json from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog -from PySide6.QtCore import QUrl, QPoint +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.steam_api import is_game_in_steam, add_to_steam, remove_from_steam from portprotonqt.dialogs import AddGameDialog +from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable +import vdf + +logger = logging.getLogger(__name__) + +class ContextMenuSignals(QObject): + """Signals for thread-safe UI updates from worker threads.""" + show_status_message = Signal(str, int) + show_warning_dialog = Signal(str, str) class ContextMenuManager: """Manages context menu actions for game management in PortProtonQt.""" @@ -30,6 +43,56 @@ class ContextMenuManager: self.theme = theme self.load_games = load_games_callback self.update_game_grid = update_game_grid_callback + self.legendary_path = os.path.join( + os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), + "PortProtonQt", "legendary_cache", "legendary" + ) + self.legendary_config_path = os.path.join( + 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") + else: + self.signals.show_status_message.connect( + self.parent.statusBar().showMessage, + Qt.ConnectionType.QueuedConnection + ) + logger.debug("Connected show_status_message signal to statusBar") + self.signals.show_warning_dialog.connect( + self._show_warning_dialog, + Qt.ConnectionType.QueuedConnection + ) + + def _show_warning_dialog(self, title: str, message: str): + """Show a warning dialog in the main thread.""" + logger.debug("Showing warning dialog: %s - %s", title, message) + QMessageBox.warning(self.parent, title, message) + + def _is_egs_game_installed(self, app_name: str) -> bool: + """ + Check if an EGS game is installed by reading installed.json. + + Args: + app_name: The Legendary app_name (unique identifier for the game). + + Returns: + bool: True if the game is installed, False otherwise. + """ + installed_json_path = os.path.join(self.legendary_config_path, "installed.json") + 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) + return app_name in installed_games + except (OSError, json.JSONDecodeError) as e: + logger.error("Failed to read installed.json: %s", e) + return False def show_context_menu(self, game_card, pos: QPoint): """ @@ -39,7 +102,6 @@ class ContextMenuManager: game_card: The GameCard instance requesting the context menu. pos: The position (in widget coordinates) where the menu should appear. """ - menu = QMenu(self.parent) menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE) @@ -54,9 +116,31 @@ class ContextMenuManager: favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True)) if game_card.game_source == "epic": - import_action = menu.addAction(_("Import to Legendary")) - import_action.triggered.connect( - lambda: self.import_to_legendary(game_card.name, game_card.appid) + # 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): + uninstall_action = menu.addAction(_("Uninstall Game")) + uninstall_action.triggered.connect( + lambda: self.uninstall_egs_game(game_card.name, 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) + ) + open_folder_action = menu.addAction(_("Open Game Folder")) + open_folder_action.triggered.connect( + lambda: self.open_egs_game_folder(game_card.appid) ) if game_card.game_source not in ("steam", "epic"): @@ -70,13 +154,17 @@ class ContextMenuManager: add_action.triggered.connect(lambda: 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)) + 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)) + 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") @@ -87,20 +175,88 @@ class ContextMenuManager: add_action = menu.addAction(_("Add to Menu")) add_action.triggered.connect(lambda: self.add_to_menu(game_card.name, game_card.exec_line)) - # Add Steam-related actions 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)) + 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)) + add_steam_action.triggered.connect( + lambda: self.add_to_steam(game_card.name, game_card.exec_line, game_card.cover_path) + ) menu.exec(game_card.mapToGlobal(pos)) - def import_to_legendary(self, game_name, app_name): + def add_egs_to_steam(self, game_name: str, app_name: str): """ - Imports an installed Epic Games Store game to Legendary using the provided app_name. + Adds an EGS game to Steam using the egs_api. + + Args: + game_name: The display name of the game. + app_name: The Legendary app_name (unique identifier for the game). + """ + if not self._check_portproton(): + return + + if not os.path.exists(self.legendary_path): + self._show_warning_dialog(_("Error"), _("Legendary executable not found at {0}").format(self.legendary_path)) + return + + def on_add_to_steam_result(result: tuple[bool, str]): + success, message = result + if success: + self.signals.show_status_message.emit( + _("The game was added successfully. Please restart Steam for changes to take effect."), 5000 + ) + else: + self.signals.show_warning_dialog.emit(_("Error"), message) + + if self.parent.statusBar(): + self.parent.statusBar().showMessage(_("Adding '{0}' to Steam...").format(game_name), 0) + logger.debug("Direct status message: Adding '%s' to Steam", game_name) + else: + logger.warning("Status bar not available when adding '%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): + """ + Opens the folder containing the EGS game's executable. + + Args: + app_name: The Legendary app_name (unique identifier for the game). + """ + 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._show_warning_dialog( + _("Error"), + _("Executable file not found for game: {0}").format(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) + except Exception as e: + self._show_warning_dialog( + _("Error"), + _("Failed to open game folder: {0}").format(str(e)) + ) + + def import_to_legendary(self, game_name, app_name): + """ + Imports an installed Epic Games Store game to Legendary asynchronously. Args: game_name: The display name of the game. @@ -109,63 +265,118 @@ class ContextMenuManager: if not self._check_portproton(): return - # Открываем диалог для выбора папки с установленной игрой folder_path = QFileDialog.getExistingDirectory( self.parent, _("Select Game Installation Folder"), os.path.expanduser("~") ) if not folder_path: - self.parent.statusBar().showMessage(_("No folder selected"), 3000) + 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) return - # Путь к legendary - legendary_path = os.path.join( - os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), - "PortProtonQt", "legendary_cache", "legendary" - ) - if not os.path.exists(legendary_path): - QMessageBox.warning( - self.parent, + if not os.path.exists(self.legendary_path): + self._show_warning_dialog( _("Error"), - _("Legendary executable not found at {0}").format(legendary_path) + _("Legendary executable not found at {0}").format(self.legendary_path) ) return - # Формируем команду для импорта - cmd = [legendary_path, "import", app_name, folder_path] - - try: - # Выполняем команду legendary import + 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( - _("Successfully imported '{0}' to Legendary").format(game_name), 3000 + _("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) + threading.Thread(target=run_import, daemon=True).start() + def uninstall_egs_game(self, game_name: str, app_name: str): + """ + Uninstalls an Epic Games Store game using Legendary asynchronously. - except subprocess.CalledProcessError as e: - QMessageBox.warning( - self.parent, + Args: + game_name: The display name of the game. + app_name: The Legendary app_name (unique identifier for the game). + """ + if not self._check_portproton(): + return + + if not os.path.exists(self.legendary_path): + self._show_warning_dialog( _("Error"), - _("Failed to import '{0}' to Legendary: {1}").format(game_name, e.stderr) + _("Legendary executable not found at {0}").format(self.legendary_path) ) - except FileNotFoundError: - QMessageBox.warning( - self.parent, - _("Error"), - _("Legendary executable not found") - ) - except Exception as e: - QMessageBox.warning( - self.parent, - _("Error"), - _("Unexpected error during import: {0}").format(str(e)) + return + + reply = QMessageBox.question( + self.parent, + _("Confirm Uninstallation"), + _("Are you sure you want to uninstall '{0}'? This will remove the game files.").format(game_name), + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + if reply != QMessageBox.StandardButton.Yes: + return + + def run_uninstall(): + cmd = [self.legendary_path, "uninstall", app_name, "--skip-uninstaller"] + try: + subprocess.run( + cmd, + capture_output=True, + text=True, + check=True, + env={"LEGENDARY_CONFIG_PATH": self.legendary_config_path} + ) + self.signals.show_status_message.emit( + _("Successfully uninstalled '{0}'").format(game_name), 3000 + ) + except subprocess.CalledProcessError as e: + self.signals.show_status_message.emit( + _("Failed to uninstall '{0}'").format(game_name), 3000 + ) + self.signals.show_warning_dialog.emit( + _("Error"), + _("Failed to uninstall '{0}': {1}").format(game_name, e.stderr) + ) + except FileNotFoundError: + self.signals.show_status_message.emit( + _("Legendary executable not found"), 3000 + ) + self.signals.show_warning_dialog.emit( + _("Error"), + _("Legendary executable not found") + ) + except Exception as e: + self.signals.show_status_message.emit( + _("Unexpected error during uninstall"), 3000 + ) + self.signals.show_warning_dialog.emit( + _("Error"), + _("Unexpected error during uninstall: {0}").format(str(e)) + ) + + if self.parent.statusBar(): + self.parent.statusBar().showMessage( + _("Uninstalling '{0}'...").format(game_name), 0 ) + logger.debug("Direct status message: Uninstalling '%s'", game_name) + else: + logger.warning("Status bar not available when uninstalling '%s'", game_name) + threading.Thread(target=run_uninstall, daemon=True).start() def toggle_favorite(self, game_card, add: bool): """ @@ -179,18 +390,33 @@ class ContextMenuManager: if add and game_card.name not in favorites: favorites.append(game_card.name) game_card.is_favorite = True - self.parent.statusBar().showMessage(_("Added '{0}' to favorites").format(game_card.name), 3000) + 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) elif not add and game_card.name in favorites: favorites.remove(game_card.name) game_card.is_favorite = False - self.parent.statusBar().showMessage(_("Removed '{0}' from favorites").format(game_card.name), 3000) + 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) save_favorites(favorites) game_card.update_favorite_icon() def _check_portproton(self): """Check if PortProton is available.""" if self.portproton_location is None: - QMessageBox.warning(self.parent, _("Error"), _("PortProton is not found.")) + self._show_warning_dialog( + _("Error"), + _("PortProton is not found.") + ) return False return True @@ -214,33 +440,32 @@ class ContextMenuManager: if entry: exec_line = entry.get("Exec", entry.get("exec", "")).strip() if not exec_line: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("No executable command found in .desktop for game: {0}").format(game_name) ) return None else: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Failed to parse .desktop file for game: {0}").format(game_name) ) return None except Exception as e: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Error reading .desktop file: {0}").format(e) ) return None else: - # Fallback: Search all .desktop files for file in glob.glob(os.path.join(self.portproton_location, "*.desktop")): entry = parse_desktop_entry(file) if entry: exec_line = entry.get("Exec", entry.get("exec", "")).strip() if exec_line: return exec_line - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _(".desktop file not found for game: {0}").format(game_name) ) return None @@ -251,8 +476,8 @@ class ContextMenuManager: try: entry_exec_split = shlex.split(exec_line) if not entry_exec_split: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Invalid executable command: {0}").format(exec_line) ) return None @@ -263,15 +488,15 @@ class ContextMenuManager: else: exe_path = entry_exec_split[-1] if not exe_path or not os.path.exists(exe_path): - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Executable file not found: {0}").format(exe_path or "None") ) return None return exe_path except Exception as e: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Failed to parse executable command: {0}").format(e) ) return None @@ -280,10 +505,17 @@ class ContextMenuManager: """Remove a file and handle errors.""" try: os.remove(file_path) - self.parent.statusBar().showMessage(success_message.format(game_name), 3000) + 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) return True except OSError as e: - QMessageBox.warning(self.parent, _("Error"), error_message.format(e)) + self._show_warning_dialog( + _("Error"), + error_message.format(e) + ) return False def delete_game(self, game_name, exec_line): @@ -304,13 +536,12 @@ class ContextMenuManager: desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Could not locate .desktop file for '{0}'").format(game_name) ) return - # Get exec_line and parse exe_path exec_line = self._get_exec_line(game_name, exec_line) if not exec_line: return @@ -318,7 +549,6 @@ class ContextMenuManager: 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 - # Remove .desktop file if not self._remove_file( desktop_path, _("Failed to delete .desktop file: {0}"), @@ -327,7 +557,6 @@ class ContextMenuManager: ): return - # Remove custom data if we got an exe_name if exe_name: xdg_data_home = os.getenv( "XDG_DATA_HOME", @@ -338,15 +567,11 @@ class ContextMenuManager: try: shutil.rmtree(custom_folder) except OSError as e: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Failed to delete custom data: {0}").format(e) ) - # Refresh UI - self.parent.games = self.load_games() - self.update_game_grid() - def add_to_menu(self, game_name, exec_line): """Copy the .desktop file to ~/.local/share/applications.""" if not self._check_portproton(): @@ -354,25 +579,29 @@ class ContextMenuManager: desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Could not locate .desktop file for '{0}'").format(game_name) ) return - # Destination path 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") - # Copy .desktop file try: shutil.copyfile(desktop_path, dest_path) - os.chmod(dest_path, 0o755) # Ensure executable permissions - self.parent.statusBar().showMessage(_("Game '{0}' added to menu").format(game_name), 3000) + 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 '%s' added to menu", game_name) + else: + logger.warning("Status bar not available when adding '%s' to menu", game_name) except OSError as e: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Failed to add game to menu: {0}").format(str(e)) ) @@ -394,25 +623,29 @@ class ContextMenuManager: desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Could not locate .desktop file for '{0}'").format(game_name) ) return - # Destination path 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") - # Copy .desktop file try: shutil.copyfile(desktop_path, dest_path) - os.chmod(dest_path, 0o755) # Ensure executable permissions - self.parent.statusBar().showMessage(_("Game '{0}' added to desktop").format(game_name), 3000) + 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 '%s' added to desktop", game_name) + else: + logger.warning("Status bar not available when adding '%s' to desktop", game_name) except OSError as e: - QMessageBox.warning( - self.parent, _("Error"), + self._show_warning_dialog( + _("Error"), _("Failed to add game to desktop: {0}").format(str(e)) ) @@ -429,7 +662,6 @@ class ContextMenuManager: def edit_game_shortcut(self, game_name, exec_line, cover_path): """Opens the AddGameDialog in edit mode to modify an existing .desktop file.""" - if not self._check_portproton(): return @@ -441,7 +673,6 @@ class ContextMenuManager: if not exe_path: return - # Open dialog in edit mode dialog = AddGameDialog( parent=self.parent, theme=self.theme, @@ -457,16 +688,20 @@ class ContextMenuManager: new_cover_path = dialog.coverEdit.text().strip() if not new_name or not new_exe_path: - QMessageBox.warning(self.parent, _("Error"), _("Game name and executable path are required.")) + self._show_warning_dialog( + _("Error"), + _("Game name and executable path are required.") + ) return - # Generate new .desktop file content desktop_entry, new_desktop_path = dialog.getDesktopEntryData() if not desktop_entry or not new_desktop_path: - QMessageBox.warning(self.parent, _("Error"), _("Failed to generate .desktop file data.")) + self._show_warning_dialog( + _("Error"), + _("Failed to generate .desktop file data.") + ) return - # If the name has changed, remove the old .desktop file old_desktop_path = self._get_desktop_path(game_name) if game_name != new_name and os.path.exists(old_desktop_path): self._remove_file( @@ -476,16 +711,17 @@ class ContextMenuManager: game_name ) - # Save the updated .desktop file try: with open(new_desktop_path, "w", encoding="utf-8") as f: f.write(desktop_entry) os.chmod(new_desktop_path, 0o755) except OSError as e: - QMessageBox.warning(self.parent, _("Error"), _("Failed to save .desktop file: {0}").format(e)) + self._show_warning_dialog( + _("Error"), + _("Failed to save .desktop file: {0}").format(e) + ) return - # Update custom cover if provided if os.path.isfile(new_cover_path): exe_name = os.path.splitext(os.path.basename(new_exe_path))[0] xdg_data_home = os.getenv( @@ -500,16 +736,14 @@ class ContextMenuManager: try: shutil.copyfile(new_cover_path, os.path.join(custom_folder, f"cover{ext}")) except OSError as e: - QMessageBox.warning(self.parent, _("Error"), _("Failed to copy cover image: {0}").format(e)) + self._show_warning_dialog( + _("Error"), + _("Failed to copy cover image: {0}").format(e) + ) return - # Refresh the game list - self.parent.games = self.load_games() - self.update_game_grid() - def add_to_steam(self, game_name, exec_line, cover_path): """Handle adding a non-Steam game to Steam via steam_api.""" - if not self._check_portproton(): return @@ -521,37 +755,174 @@ class ContextMenuManager: if not exe_path: return - success, message = add_to_steam(game_name, exec_line, cover_path) - if success: - QMessageBox.information( - self.parent, _("Restart Steam"), - _("The game was added successfully.\nPlease restart Steam for changes to take effect.") + def on_add_to_steam_result(result: tuple[bool, str]): + success, message = result + if success: + self.signals.show_status_message.emit( + _("The game was added successfully. Please restart Steam for changes to take effect."), 5000 + ) + else: + self.signals.show_warning_dialog.emit(_("Error"), message) + + if self.parent.statusBar(): + self.parent.statusBar().showMessage( + _("Adding '{0}' to Steam...").format(game_name), 0 ) + logger.debug("Direct status message: Adding '%s' to Steam", game_name) else: - QMessageBox.warning(self.parent, _("Error"), message) - - def remove_from_steam(self, game_name, exec_line): - """Handle removing a non-Steam game from Steam via steam_api.""" + logger.warning("Status bar not available when adding '%s' to Steam", game_name) + add_to_steam(game_name, exec_line, cover_path) + def remove_from_steam(self, game_name, exec_line, game_source): + """Handle removing a game from Steam via steam_api, supporting both EGS and non-EGS games.""" if not self._check_portproton(): return - exec_line = self._get_exec_line(game_name, exec_line) - if not exec_line: - return + def on_remove_from_steam_result(result: tuple[bool, str]): + success, message = result + if success: + self.signals.show_status_message.emit( + _("The game was removed successfully. Please restart Steam for changes to take effect."), 5000 + ) + else: + self.signals.show_warning_dialog.emit(_("Error"), message) - exe_path = self._parse_exe_path(exec_line, game_name) - if not exe_path: - return + if game_source == "epic": + # For EGS games, construct the script path used in Steam shortcuts.vdf + if not self.portproton_location: + self._show_warning_dialog( + _("Error"), + _("PortProton directory not found") + ) + return + steam_scripts_dir = os.path.join(self.portproton_location, "steam_scripts") + safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip()) + script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}_egs.sh") + quoted_script_path = f'"{script_path}"' + + # Directly remove the shortcut by matching AppName and Exe + try: + from portprotonqt.steam_api import get_steam_home, get_last_steam_user, convert_steam_id + steam_home = get_steam_home() + if not steam_home: + self._show_warning_dialog(_("Error"), _("Steam directory not found")) + return + + last_user = get_last_steam_user(steam_home) + if not last_user or 'SteamID' not in last_user: + self._show_warning_dialog(_("Error"), _("Failed to get Steam user ID")) + return + + userdata_dir = os.path.join(steam_home, "userdata") + user_id = last_user['SteamID'] + unsigned_id = convert_steam_id(user_id) + user_dir = os.path.join(userdata_dir, str(unsigned_id)) + steam_shortcuts_path = os.path.join(user_dir, "config", "shortcuts.vdf") + backup_path = f"{steam_shortcuts_path}.backup" + + if not os.path.exists(steam_shortcuts_path): + self._show_warning_dialog( + _("Error"), + _("Steam shortcuts file not found") + ) + return + + # Backup shortcuts.vdf + try: + shutil.copy2(steam_shortcuts_path, backup_path) + logger.info(f"Created backup of shortcuts.vdf at {backup_path}") + except Exception as e: + self._show_warning_dialog( + _("Error"), + _("Failed to create backup of shortcuts.vdf: {0}").format(e) + ) + return + + # Load shortcuts.vdf + try: + with open(steam_shortcuts_path, 'rb') as f: + shortcuts_data = vdf.binary_load(f) + except Exception as e: + self._show_warning_dialog( + _("Error"), + _("Failed to load shortcuts.vdf: {0}").format(e) + ) + return + + shortcuts = shortcuts_data.get("shortcuts", {}) + modified = False + new_shortcuts = {} + index = 0 + + 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") + continue + new_shortcuts[str(index)] = entry + index += 1 + + if not modified: + self._show_warning_dialog( + _("Error"), + _("Game '{0}' not found in Steam shortcuts").format(game_name) + ) + return + + # Save updated shortcuts.vdf + 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")) + except Exception as e: + self._show_warning_dialog( + _("Error"), + _("Failed to update shortcuts.vdf: {0}").format(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}")) + return + + # Optionally, remove the script file + if os.path.exists(script_path): + try: + os.remove(script_path) + logger.info(f"Removed EGS script file: {script_path}") + except Exception as e: + logger.warning(f"Failed to remove EGS script file {script_path}: {e}") + + except Exception as e: + self._show_warning_dialog( + _("Error"), + _("Failed to remove EGS game from Steam: {0}").format(e) + ) + on_remove_from_steam_result((False, f"Failed to remove EGS game from Steam: {e}")) + return - success, message = remove_from_steam(game_name, exec_line) - if success: - QMessageBox.information( - self.parent, _("Restart Steam"), - _("The game was removed successfully.\nPlease restart Steam for changes to take effect.") - ) else: - QMessageBox.warning(self.parent, _("Error"), message) + # For non-EGS games, use the existing logic + 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(): + self.parent.statusBar().showMessage( + _("Removing '{0}' from Steam...").format(game_name), 0 + ) + logger.debug("Direct status message: Removing '%s' from Steam", game_name) + else: + logger.warning("Status bar not available when removing '%s' from Steam", game_name) + remove_from_steam(game_name, exec_line) def open_game_folder(self, game_name, exec_line): """Open the folder containing the game's executable.""" @@ -569,6 +940,15 @@ class ContextMenuManager: try: folder_path = os.path.dirname(os.path.abspath(exe_path)) QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) - self.parent.statusBar().showMessage(_("Opened folder for '{0}'").format(game_name), 3000) + if self.parent.statusBar(): + self.parent.statusBar().showMessage( + _("Opened folder for '{0}'").format(game_name), 3000 + ) + logger.debug("Direct status message: Opened folder for '%s'", game_name) + else: + logger.warning("Status bar not available when opening folder for '%s'", game_name) except Exception as e: - QMessageBox.warning(self.parent, _("Error"), _("Failed to open game folder: {0}").format(str(e))) + self._show_warning_dialog( + _("Error"), + _("Failed to open game folder: {0}").format(str(e)) + ) -- 2.49.0 From 797de29134e54cff6c5c41ef4329f770414539cc Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 22 Jun 2025 17:00:53 +0500 Subject: [PATCH 10/13] feat: added add to desktop and menu to egs context menu Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 205 +++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index fc80f14..89337c2 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -142,6 +142,32 @@ class ContextMenuManager: 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 + 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) + ) if game_card.game_source not in ("steam", "epic"): desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip() @@ -428,6 +454,185 @@ class ContextMenuManager: desktop_path = os.path.join(self.portproton_location, f"{sanitized_name}.desktop") return desktop_path + 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 + + def _create_egs_desktop_file(self, game_name: str, app_name: str) -> bool: + """ + Creates a .desktop file for an EGS game in the PortProton egs_desktops directory. + + Args: + game_name: The display name of the game. + app_name: The Legendary app_name (unique identifier for the game). + + Returns: + bool: True if the .desktop file was created successfully, False otherwise. + """ + if not self._check_portproton(): + return False + + if not os.path.exists(self.legendary_path): + self._show_warning_dialog( + _("Error"), + _("Legendary executable not found at {0}").format(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: + wrapper = start_sh_path + if not os.path.exists(start_sh_path): + self._show_warning_dialog( + _("Error"), + _("start.sh not found at {0}").format(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 +Name={game_name} +Exec="{self.legendary_path}" launch {app_name} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}" "$@" +Icon={icon_path} +Terminal=false +Categories=Game; +""" + try: + 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}") + return True + except Exception as e: + self._show_warning_dialog( + _("Error"), + _("Failed to create .desktop file: {0}").format(str(e)) + ) + return False + + def add_egs_to_desktop(self, game_name: str, app_name: str): + """ + Copies the .desktop file for an EGS game to the Desktop folder. + + Args: + game_name: The display name of the game. + app_name: The Legendary app_name (unique identifier for the 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 '%s' added to desktop", game_name) + else: + logger.warning("Status bar not available when adding '%s' to desktop", game_name) + except OSError as e: + self._show_warning_dialog( + _("Error"), + _("Failed to add game to desktop: {0}").format(str(e)) + ) + + def remove_egs_from_desktop(self, game_name: str): + """ + Removes the .desktop file for an EGS game from the Desktop folder. + + 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 from Desktop: {0}"), + _("Game '{0}' removed from Desktop"), + game_name + ) + + def add_egs_to_menu(self, game_name: str, app_name: str): + """ + Copies the .desktop file for an EGS game to ~/.local/share/applications. + + Args: + game_name: The display name of the game. + app_name: The Legendary app_name (unique identifier for the 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 '%s' added to menu", game_name) + else: + logger.warning("Status bar not available when adding '%s' to menu", game_name) + except OSError as e: + self._show_warning_dialog( + _("Error"), + _("Failed to add game to menu: {0}").format(str(e)) + ) + + def remove_egs_from_menu(self, game_name: str): + """ + Removes the .desktop file for an EGS game 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 from menu: {0}"), + _("Game '{0}' removed from menu"), + game_name + ) + 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": -- 2.49.0 From caed021c480e5403bbf37bc745eed657392afa88 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 22 Jun 2025 17:43:27 +0500 Subject: [PATCH 11/13] fix: QMessageBox on context menu Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 318 +++++++++++---------------- 1 file changed, 126 insertions(+), 192 deletions(-) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index 89337c2..424ac48 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -12,7 +12,7 @@ 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.steam_api import is_game_in_steam, add_to_steam, remove_from_steam +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 @@ -23,6 +23,7 @@ class ContextMenuSignals(QObject): """Signals for thread-safe UI updates from worker threads.""" show_status_message = Signal(str, int) show_warning_dialog = Signal(str, str) + show_info_dialog = Signal(str, str) class ContextMenuManager: """Manages context menu actions for game management in PortProtonQt.""" @@ -65,12 +66,21 @@ class ContextMenuManager: self._show_warning_dialog, Qt.ConnectionType.QueuedConnection ) + self.signals.show_info_dialog.connect( + self._show_info_dialog, + Qt.ConnectionType.QueuedConnection + ) def _show_warning_dialog(self, title: str, message: str): """Show a warning dialog in the main thread.""" logger.debug("Showing warning dialog: %s - %s", title, message) QMessageBox.warning(self.parent, title, message) + def _show_info_dialog(self, title: str, message: str): + """Show an info dialog in the main thread.""" + logger.debug("Showing info dialog: %s - %s", title, message) + QMessageBox.information(self.parent, title, message) + def _is_egs_game_installed(self, app_name: str) -> bool: """ Check if an EGS game is installed by reading installed.json. @@ -123,10 +133,6 @@ class ContextMenuManager: ) # Show other actions only if the game is installed if self._is_egs_game_installed(game_card.appid): - uninstall_action = menu.addAction(_("Uninstall Game")) - uninstall_action.triggered.connect( - lambda: self.uninstall_egs_game(game_card.name, 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")) @@ -227,23 +233,22 @@ class ContextMenuManager: return if not os.path.exists(self.legendary_path): - self._show_warning_dialog(_("Error"), _("Legendary executable not found at {0}").format(self.legendary_path)) + self.signals.show_warning_dialog.emit( + _("Error"), _("Legendary executable not found at {0}").format(self.legendary_path) + ) return def on_add_to_steam_result(result: tuple[bool, str]): success, message = result if success: - self.signals.show_status_message.emit( - _("The game was added successfully. Please restart Steam for changes to take effect."), 5000 + 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) - if self.parent.statusBar(): - self.parent.statusBar().showMessage(_("Adding '{0}' to Steam...").format(game_name), 0) - logger.debug("Direct status message: Adding '%s' to Steam", game_name) - else: - logger.warning("Status bar not available when adding '%s' to Steam", game_name) + logger.debug("Adding '%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): @@ -258,7 +263,7 @@ class ContextMenuManager: exe_path = get_egs_executable(app_name, self.legendary_config_path) if not exe_path or not os.path.exists(exe_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Executable file not found for game: {0}").format(app_name) ) @@ -275,7 +280,7 @@ class ContextMenuManager: else: logger.warning("Status bar not available when opening folder for '%s'", app_name) except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Failed to open game folder: {0}").format(str(e)) ) @@ -305,7 +310,7 @@ class ContextMenuManager: return if not os.path.exists(self.legendary_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Legendary executable not found at {0}").format(self.legendary_path) ) @@ -329,81 +334,6 @@ class ContextMenuManager: logger.warning("Status bar not available when importing '%s'", game_name) threading.Thread(target=run_import, daemon=True).start() - def uninstall_egs_game(self, game_name: str, app_name: str): - """ - Uninstalls an Epic Games Store game using Legendary asynchronously. - - Args: - game_name: The display name of the game. - app_name: The Legendary app_name (unique identifier for the game). - """ - if not self._check_portproton(): - return - - if not os.path.exists(self.legendary_path): - self._show_warning_dialog( - _("Error"), - _("Legendary executable not found at {0}").format(self.legendary_path) - ) - return - - reply = QMessageBox.question( - self.parent, - _("Confirm Uninstallation"), - _("Are you sure you want to uninstall '{0}'? This will remove the game files.").format(game_name), - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No - ) - if reply != QMessageBox.StandardButton.Yes: - return - - def run_uninstall(): - cmd = [self.legendary_path, "uninstall", app_name, "--skip-uninstaller"] - try: - subprocess.run( - cmd, - capture_output=True, - text=True, - check=True, - env={"LEGENDARY_CONFIG_PATH": self.legendary_config_path} - ) - self.signals.show_status_message.emit( - _("Successfully uninstalled '{0}'").format(game_name), 3000 - ) - except subprocess.CalledProcessError as e: - self.signals.show_status_message.emit( - _("Failed to uninstall '{0}'").format(game_name), 3000 - ) - self.signals.show_warning_dialog.emit( - _("Error"), - _("Failed to uninstall '{0}': {1}").format(game_name, e.stderr) - ) - except FileNotFoundError: - self.signals.show_status_message.emit( - _("Legendary executable not found"), 3000 - ) - self.signals.show_warning_dialog.emit( - _("Error"), - _("Legendary executable not found") - ) - except Exception as e: - self.signals.show_status_message.emit( - _("Unexpected error during uninstall"), 3000 - ) - self.signals.show_warning_dialog.emit( - _("Error"), - _("Unexpected error during uninstall: {0}").format(str(e)) - ) - - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Uninstalling '{0}'...").format(game_name), 0 - ) - logger.debug("Direct status message: Uninstalling '%s'", game_name) - else: - logger.warning("Status bar not available when uninstalling '%s'", game_name) - threading.Thread(target=run_uninstall, daemon=True).start() - def toggle_favorite(self, game_card, add: bool): """ Toggle the favorite status of a game and update its icon. @@ -439,7 +369,7 @@ class ContextMenuManager: def _check_portproton(self): """Check if PortProton is available.""" if self.portproton_location is None: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("PortProton is not found.") ) @@ -474,7 +404,7 @@ class ContextMenuManager: return False if not os.path.exists(self.legendary_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Legendary executable not found at {0}").format(self.legendary_path) ) @@ -486,7 +416,7 @@ class ContextMenuManager: if self.portproton_location and ".var" not in self.portproton_location: wrapper = start_sh_path if not os.path.exists(start_sh_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("start.sh not found at {0}").format(start_sh_path) ) @@ -509,10 +439,10 @@ class ContextMenuManager: desktop_entry = f"""[Desktop Entry] Type=Application Name={game_name} -Exec="{self.legendary_path}" launch {app_name} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}" "$@" +Exec="{self.legendary_path}" launch {app_name} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}" Icon={icon_path} Terminal=false -Categories=Game; +Categories=Game """ try: with open(desktop_path, "w", encoding="utf-8") as f: @@ -521,7 +451,7 @@ Categories=Game; logger.info(f"Created .desktop file for EGS game: {desktop_path}") return True except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Failed to create .desktop file: {0}").format(str(e)) ) @@ -555,13 +485,13 @@ Categories=Game; self.parent.statusBar().showMessage( _("Game '{0}' added to desktop").format(game_name), 3000 ) - logger.debug("Direct status message: Game '%s' added to desktop", game_name) + logger.debug("Direct status message: Game '{0}' added to desktop", game_name) else: - logger.warning("Status bar not available when adding '%s' to desktop", game_name) + logger.warning("Status bar not available when adding '{0}' to desktop", game_name) except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game to desktop: {0}").format(str(e)) + _("Failed to add game to desktop: {0}").format(game_name, str(e)) ) def remove_egs_from_desktop(self, game_name: str): @@ -575,8 +505,8 @@ Categories=Game; desktop_path = os.path.join(desktop_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game from Desktop: {0}"), - _("Game '{0}' removed from Desktop"), + _("Failed to remove game '{0}' from Desktop: {{0}}").format(game_name), + _("Successfully removed game '{0}' from Desktop").format(game_name), game_name ) @@ -608,13 +538,13 @@ Categories=Game; self.parent.statusBar().showMessage( _("Game '{0}' added to menu").format(game_name), 3000 ) - logger.debug("Direct status message: Game '%s' added to menu", game_name) + logger.debug("Direct status message: Game '{0}' added to menu", game_name) else: - logger.warning("Status bar not available when adding '%s' to menu", game_name) + logger.warning("Status bar not available when adding '{0}' to menu", game_name) except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game to menu: {0}").format(str(e)) + _("Failed to add game '{0}' to menu: {1}").format(game_name, str(e)) ) def remove_egs_from_menu(self, game_name: str): @@ -628,8 +558,8 @@ Categories=Game; desktop_path = os.path.join(applications_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game from menu: {0}"), - _("Game '{0}' removed from menu"), + _("Failed to remove game '{0}' from menu: {{0}}").format(game_name), + _("Successfully removed game '{0}' from menu").format(game_name), game_name ) @@ -645,21 +575,21 @@ Categories=Game; if entry: exec_line = entry.get("Exec", entry.get("exec", "")).strip() if not exec_line: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("No executable command found in .desktop for game: {0}").format(game_name) + _("No executable command found in .desktop file for game: {0}").format(game_name) ) return None else: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Failed to parse .desktop file for game: {0}").format(game_name) ) return None except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Error reading .desktop file: {0}").format(e) + _("Failed to read .desktop file: {0}").format(str(e)) ) return None else: @@ -669,7 +599,7 @@ Categories=Game; exec_line = entry.get("Exec", entry.get("exec", "")).strip() if exec_line: return exec_line - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _(".desktop file not found for game: {0}").format(game_name) ) @@ -681,7 +611,7 @@ Categories=Game; try: entry_exec_split = shlex.split(exec_line) if not entry_exec_split: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Invalid executable command: {0}").format(exec_line) ) @@ -693,16 +623,16 @@ Categories=Game; else: exe_path = entry_exec_split[-1] if not exe_path or not os.path.exists(exe_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Executable file not found: {0}").format(exe_path or "None") ) return None return exe_path except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to parse executable command: {0}").format(e) + _("Failed to parse executable command: {0}").format(str(e)) ) return None @@ -717,9 +647,9 @@ Categories=Game; logger.warning("Status bar not available when removing file for '%s'", game_name) return True except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - error_message.format(e) + error_message.format(str(e)) ) return False @@ -741,9 +671,9 @@ Categories=Game; desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Could not locate .desktop file for '{0}'").format(game_name) + _("Could not locate .desktop file for game: {0}").format(game_name) ) return @@ -772,9 +702,9 @@ Categories=Game; try: shutil.rmtree(custom_folder) except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to delete custom data: {0}").format(e) + _("Failed to delete custom data: {0}").format(str(e)) ) def add_to_menu(self, game_name, exec_line): @@ -784,9 +714,9 @@ Categories=Game; desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Could not locate .desktop file for '{0}'").format(game_name) + _("Could not locate .desktop file for game: {0}").format(game_name) ) return @@ -801,13 +731,13 @@ Categories=Game; self.parent.statusBar().showMessage( _("Game '{0}' added to menu").format(game_name), 3000 ) - logger.debug("Direct status message: Game '%s' added to menu", game_name) + logger.debug("Direct status message: Game '{0}' added to menu", game_name) else: - logger.warning("Status bar not available when adding '%s' to menu", game_name) + logger.warning("Status bar not available when adding '{0}' to menu", game_name) except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game to menu: {0}").format(str(e)) + _("Failed to add game '{0}' to menu: {1}").format(game_name, str(e)) ) def remove_from_menu(self, game_name): @@ -816,8 +746,8 @@ Categories=Game; desktop_path = os.path.join(applications_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game from menu: {0}"), - _("Game '{0}' removed from menu"), + _("Failed to remove game '{0}' from menu: {{0}}").format(game_name), + _("Successfully removed game '{0}' from menu").format(game_name), game_name ) @@ -828,9 +758,9 @@ Categories=Game; desktop_path = self._get_desktop_path(game_name) if not os.path.exists(desktop_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Could not locate .desktop file for '{0}'").format(game_name) + _("Could not locate .desktop file for game: {0}").format(game_name) ) return @@ -845,13 +775,13 @@ Categories=Game; self.parent.statusBar().showMessage( _("Game '{0}' added to desktop").format(game_name), 3000 ) - logger.debug("Direct status message: Game '%s' added to desktop", game_name) + logger.debug("Direct status message: Game '{0}' added to desktop", game_name) else: - logger.warning("Status bar not available when adding '%s' to desktop", game_name) + logger.warning("Status bar not available when adding '{0}' to desktop", game_name) except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to add game to desktop: {0}").format(str(e)) + _("Failed to add game '{0}' to desktop: {1}").format(game_name, str(e)) ) def remove_from_desktop(self, game_name): @@ -860,8 +790,8 @@ Categories=Game; desktop_path = os.path.join(desktop_dir, f"{game_name}.desktop") self._remove_file( desktop_path, - _("Failed to remove game from Desktop: {0}"), - _("Game '{0}' removed from Desktop"), + _("Failed to remove game '{0}' from Desktop: {{0}}").format(game_name), + _("Successfully removed game '{0}' from Desktop").format(game_name), game_name ) @@ -893,7 +823,7 @@ Categories=Game; new_cover_path = dialog.coverEdit.text().strip() if not new_name or not new_exe_path: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Game name and executable path are required.") ) @@ -901,7 +831,7 @@ Categories=Game; desktop_entry, new_desktop_path = dialog.getDesktopEntryData() if not desktop_entry or not new_desktop_path: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Failed to generate .desktop file data.") ) @@ -921,9 +851,9 @@ Categories=Game; f.write(desktop_entry) os.chmod(new_desktop_path, 0o755) except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to save .desktop file: {0}").format(e) + _("Failed to save .desktop file: {0}").format(str(e)) ) return @@ -941,9 +871,9 @@ Categories=Game; try: shutil.copyfile(new_cover_path, os.path.join(custom_folder, f"cover{ext}")) except OSError as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to copy cover image: {0}").format(e) + _("Failed to copy cover image: {0}").format(str(e)) ) return @@ -960,23 +890,19 @@ Categories=Game; if not exe_path: return - def on_add_to_steam_result(result: tuple[bool, str]): - success, message = result - if success: - self.signals.show_status_message.emit( - _("The game was added successfully. Please restart Steam for changes to take effect."), 5000 - ) - else: - self.signals.show_warning_dialog.emit(_("Error"), message) + logger.debug("Adding '{0}' to Steam", game_name) - if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Adding '{0}' to Steam...").format(game_name), 0 + try: + 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) + ) + except Exception as e: + self.signals.show_warning_dialog.emit( + _("Error"), + _("Failed to add game '{0}' to Steam: {1}").format(game_name, str(e)) ) - logger.debug("Direct status message: Adding '%s' to Steam", game_name) - else: - logger.warning("Status bar not available when adding '%s' to Steam", game_name) - add_to_steam(game_name, exec_line, cover_path) def remove_from_steam(self, game_name, exec_line, game_source): """Handle removing a game from Steam via steam_api, supporting both EGS and non-EGS games.""" @@ -986,8 +912,9 @@ Categories=Game; def on_remove_from_steam_result(result: tuple[bool, str]): success, message = result if success: - self.signals.show_status_message.emit( - _("The game was removed successfully. Please restart Steam for changes to take effect."), 5000 + self.signals.show_info_dialog.emit( + _("Success"), + _("'{0}' was removed from Steam. Please restart Steam for changes to take effect.").format(game_name) ) else: self.signals.show_warning_dialog.emit(_("Error"), message) @@ -995,7 +922,7 @@ Categories=Game; if game_source == "epic": # For EGS games, construct the script path used in Steam shortcuts.vdf if not self.portproton_location: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("PortProton directory not found") ) @@ -1007,15 +934,14 @@ Categories=Game; # Directly remove the shortcut by matching AppName and Exe try: - from portprotonqt.steam_api import get_steam_home, get_last_steam_user, convert_steam_id steam_home = get_steam_home() if not steam_home: - self._show_warning_dialog(_("Error"), _("Steam directory not found")) + self.signals.show_warning_dialog.emit(_("Error"), _("Steam directory not found")) return last_user = get_last_steam_user(steam_home) if not last_user or 'SteamID' not in last_user: - self._show_warning_dialog(_("Error"), _("Failed to get Steam user ID")) + self.signals.show_warning_dialog.emit(_("Error"), _("Failed to get Steam user ID")) return userdata_dir = os.path.join(steam_home, "userdata") @@ -1026,7 +952,7 @@ Categories=Game; backup_path = f"{steam_shortcuts_path}.backup" if not os.path.exists(steam_shortcuts_path): - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Steam shortcuts file not found") ) @@ -1037,9 +963,9 @@ Categories=Game; shutil.copy2(steam_shortcuts_path, backup_path) logger.info(f"Created backup of shortcuts.vdf at {backup_path}") except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to create backup of shortcuts.vdf: {0}").format(e) + _("Failed to create backup of shortcuts.vdf: {0}").format(str(e)) ) return @@ -1048,9 +974,9 @@ Categories=Game; with open(steam_shortcuts_path, 'rb') as f: shortcuts_data = vdf.binary_load(f) except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to load shortcuts.vdf: {0}").format(e) + _("Failed to load shortcuts.vdf: {0}").format(str(e)) ) return @@ -1068,7 +994,7 @@ Categories=Game; index += 1 if not modified: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Game '{0}' not found in Steam shortcuts").format(game_name) ) @@ -1081,9 +1007,9 @@ Categories=Game; logger.info(f"Updated shortcuts.vdf, removed '{game_name}'") on_remove_from_steam_result((True, f"Game '{game_name}' removed from Steam")) except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to update shortcuts.vdf: {0}").format(e) + _("Failed to update shortcuts.vdf: {0}").format(str(e)) ) if os.path.exists(backup_path): try: @@ -1098,20 +1024,20 @@ Categories=Game; if os.path.exists(script_path): try: os.remove(script_path) - logger.info(f"Removed EGS script file: {script_path}") + logger.info(f"Removed EGS script: {script_path}") except Exception as e: - logger.warning(f"Failed to remove EGS script file {script_path}: {e}") + logger.warning(f"Failed to remove EGS script {script_path}: {e}") except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), - _("Failed to remove EGS game from Steam: {0}").format(e) + _("Failed to remove EGS game '{0}' from Steam: {1}").format(game_name, str(e)) ) - on_remove_from_steam_result((False, f"Failed to remove EGS game from Steam: {e}")) + on_remove_from_steam_result((False, f"Failed to remove EGS game '{game_name}' from Steam: {str(e)}")) return else: - # For non-EGS games, use the existing logic + # For non-EGS games, use the existing logic without callback exec_line = self._get_exec_line(game_name, exec_line) if not exec_line: return @@ -1121,13 +1047,21 @@ Categories=Game; return if self.parent.statusBar(): - self.parent.statusBar().showMessage( - _("Removing '{0}' from Steam...").format(game_name), 0 - ) - logger.debug("Direct status message: Removing '%s' from Steam", game_name) + logger.debug("Direct status message: Removing '{0}' from Steam", game_name) else: - logger.warning("Status bar not available when removing '%s' from Steam", game_name) - remove_from_steam(game_name, exec_line) + logger.warning("Status bar not available when removing '{0}' from Steam", game_name) + + try: + 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) + ) + except Exception as e: + self.signals.show_warning_dialog.emit( + _("Error"), + _("Failed to remove game '{0}' from Steam: {1}").format(game_name, str(e)) + ) def open_game_folder(self, game_name, exec_line): """Open the folder containing the game's executable.""" @@ -1147,13 +1081,13 @@ Categories=Game; QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path)) if self.parent.statusBar(): self.parent.statusBar().showMessage( - _("Opened folder for '{0}'").format(game_name), 3000 + _("Successfully opened folder for '{0}'").format(game_name), 3000 ) - logger.debug("Direct status message: Opened folder for '%s'", game_name) + logger.debug("Direct status message: Opened folder for '{0}'", game_name) else: - logger.warning("Status bar not available when opening folder for '%s'", game_name) + logger.warning("Status bar not available when opening folder for '{0}'", game_name) except Exception as e: - self._show_warning_dialog( + self.signals.show_warning_dialog.emit( _("Error"), _("Failed to open game folder: {0}").format(str(e)) ) -- 2.49.0 From 8ff0daafa41aaf37fae3ac4d75de935603e697f5 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 22 Jun 2025 18:24:10 +0500 Subject: [PATCH 12/13] chore(localization): update Signed-off-by: Boris Yumankulov --- dev-scripts/.spellignore | 2 + documentation/localization_guide/README.md | 6 +- documentation/localization_guide/README.ru.md | 6 +- .../locales/de_DE/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/de_DE/LC_MESSAGES/messages.po | 252 +++++++++++----- .../locales/es_ES/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/es_ES/LC_MESSAGES/messages.po | 252 +++++++++++----- portprotonqt/locales/messages.pot | 252 +++++++++++----- .../locales/ru_RU/LC_MESSAGES/messages.mo | Bin 13732 -> 17336 bytes .../locales/ru_RU/LC_MESSAGES/messages.po | 282 ++++++++++++------ 10 files changed, 759 insertions(+), 293 deletions(-) diff --git a/dev-scripts/.spellignore b/dev-scripts/.spellignore index f6c1435..d3ae991 100644 --- a/dev-scripts/.spellignore +++ b/dev-scripts/.spellignore @@ -14,3 +14,5 @@ MIME-Version: Content-Type: Content-Transfer-Encoding: Generated-By: +start.sh +EGS diff --git a/documentation/localization_guide/README.md b/documentation/localization_guide/README.md index 6e7c102..cbde378 100644 --- a/documentation/localization_guide/README.md +++ b/documentation/localization_guide/README.md @@ -20,9 +20,9 @@ Current translation status: | Locale | Progress | Translated | | :----- | -------: | ---------: | -| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 161 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 161 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 of 161 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 194 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 194 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 of 194 | --- diff --git a/documentation/localization_guide/README.ru.md b/documentation/localization_guide/README.ru.md index 7159d35..5e531b6 100644 --- a/documentation/localization_guide/README.ru.md +++ b/documentation/localization_guide/README.ru.md @@ -20,9 +20,9 @@ | Локаль | Прогресс | Переведено | | :----- | -------: | ---------: | -| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 161 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 161 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 из 161 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 194 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 194 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 194 из 194 | --- diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo index b93ff066a17ac807b58c3bc2a5e7babc89bfa1f8..5a02705a016ceeb1ebcc48fe89cac731b5465eea 100644 GIT binary patch delta 19 acmX@ie3*H{1P&u31w#ufBjb%Tv>5?Dcm=iq delta 19 acmX@ie3*H{1P((J1w#WXWAlwOv>5?DTm`fM diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po index 8cdc566..1115c5c 100644 --- a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po +++ b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-14 10:37+0500\n" +"POT-Creation-Date: 2025-06-22 18:23+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: de_DE\n" @@ -26,31 +26,70 @@ msgstr "" msgid "Add to Favorites" msgstr "" +msgid "Import to Legendary" +msgstr "" + +msgid "Remove from Steam" +msgstr "" + +msgid "Add to Steam" +msgstr "" + +msgid "Open Game Folder" +msgstr "" + msgid "Remove from Desktop" msgstr "" msgid "Add to Desktop" msgstr "" -msgid "Edit Shortcut" -msgstr "" - -msgid "Delete from PortProton" -msgstr "" - -msgid "Open Game Folder" -msgstr "" - msgid "Remove from Menu" msgstr "" msgid "Add to Menu" msgstr "" -msgid "Remove from Steam" +msgid "Edit Shortcut" msgstr "" -msgid "Add to Steam" +msgid "Delete from PortProton" +msgstr "" + +msgid "Error" +msgstr "" + +#, python-brace-format +msgid "Legendary executable not found at {0}" +msgstr "" + +msgid "Success" +msgstr "" + +#, python-brace-format +msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect." +msgstr "" + +#, python-brace-format +msgid "Executable file not found for game: {0}" +msgstr "" + +#, python-brace-format +msgid "Opened folder for EGS game '{0}'" +msgstr "" + +#, python-brace-format +msgid "Failed to open game folder: {0}" +msgstr "" + +msgid "Select Game Installation Folder" +msgstr "" + +msgid "No folder selected" +msgstr "" + +#, python-brace-format +msgid "Importing '{0}' to Legendary..." msgstr "" #, python-brace-format @@ -61,14 +100,51 @@ msgstr "" msgid "Removed '{0}' from favorites" msgstr "" -msgid "Error" -msgstr "" - msgid "PortProton is not found." msgstr "" #, python-brace-format -msgid "No executable command found in .desktop for game: {0}" +msgid "start.sh not found at {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to create .desktop file: {0}" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' added to desktop" +msgstr "" + +#, python-brace-format +msgid "Failed to add game to desktop: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Desktop: {{0}}" +msgstr "" + +#, python-brace-format +msgid "Successfully removed game '{0}' from Desktop" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' added to menu" +msgstr "" + +#, python-brace-format +msgid "Failed to add game '{0}' to menu: {1}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from menu: {{0}}" +msgstr "" + +#, python-brace-format +msgid "Successfully removed game '{0}' from menu" +msgstr "" + +#, python-brace-format +msgid "No executable command found in .desktop file for game: {0}" msgstr "" #, python-brace-format @@ -76,7 +152,7 @@ msgid "Failed to parse .desktop file for game: {0}" msgstr "" #, python-brace-format -msgid "Error reading .desktop file: {0}" +msgid "Failed to read .desktop file: {0}" msgstr "" #, python-brace-format @@ -105,7 +181,7 @@ msgid "" msgstr "" #, python-brace-format -msgid "Could not locate .desktop file for '{0}'" +msgid "Could not locate .desktop file for game: {0}" msgstr "" #, python-brace-format @@ -121,35 +197,7 @@ msgid "Failed to delete custom data: {0}" msgstr "" #, python-brace-format -msgid "Game '{0}' added to menu" -msgstr "" - -#, python-brace-format -msgid "Failed to add game to menu: {0}" -msgstr "" - -#, python-brace-format -msgid "Failed to remove game from menu: {0}" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' removed from menu" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' added to desktop" -msgstr "" - -#, python-brace-format -msgid "Failed to add game to desktop: {0}" -msgstr "" - -#, python-brace-format -msgid "Failed to remove game from Desktop: {0}" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' removed from Desktop" +msgid "Failed to add game '{0}' to desktop: {1}" msgstr "" msgid "Game name and executable path are required." @@ -174,25 +222,54 @@ msgstr "" msgid "Failed to copy cover image: {0}" msgstr "" -msgid "Restart Steam" -msgstr "" - -msgid "" -"The game was added successfully.\n" -"Please restart Steam for changes to take effect." -msgstr "" - -msgid "" -"The game was removed successfully.\n" -"Please restart Steam for changes to take effect." +#, python-brace-format +msgid "Failed to add game '{0}' to Steam: {1}" msgstr "" #, python-brace-format -msgid "Opened folder for '{0}'" +msgid "" +"'{0}' was removed from Steam. Please restart Steam for changes to take " +"effect." +msgstr "" + +msgid "PortProton directory not found" +msgstr "" + +msgid "Steam directory not found" +msgstr "" + +msgid "Failed to get Steam user ID" +msgstr "" + +msgid "Steam shortcuts file not found" msgstr "" #, python-brace-format -msgid "Failed to open game folder: {0}" +msgid "Failed to create backup of shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to load shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' not found in Steam shortcuts" +msgstr "" + +#, python-brace-format +msgid "Failed to update shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove EGS game '{0}' from Steam: {1}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Steam: {1}" +msgstr "" + +#, python-brace-format +msgid "Successfully opened folder for '{0}'" msgstr "" msgid "Edit Game" @@ -235,10 +312,10 @@ msgstr "" msgid "Loading Epic Games Store games..." msgstr "" -msgid "No description available" +msgid "Never" msgstr "" -msgid "Never" +msgid "No description available" msgstr "" msgid "Supported" @@ -382,6 +459,21 @@ msgstr "" msgid "Gamepad haptic feedback:" msgstr "" +msgid "Open Legendary Login" +msgstr "" + +msgid "Legendary Authentication:" +msgstr "" + +msgid "Enter Legendary Authorization Code" +msgstr "" + +msgid "Authorization Code:" +msgstr "" + +msgid "Submit Code" +msgstr "" + msgid "Save Settings" msgstr "" @@ -397,6 +489,22 @@ msgstr "" msgid "Failed to open Legendary login page" msgstr "" +msgid "Please enter an authorization code" +msgstr "" + +msgid "Successfully authenticated with Legendary" +msgstr "" + +#, python-brace-format +msgid "Legendary authentication failed: {0}" +msgstr "" + +msgid "Legendary executable not found" +msgstr "" + +msgid "Unexpected error during authentication" +msgstr "" + msgid "Confirm Reset" msgstr "" @@ -478,6 +586,20 @@ msgstr "" msgid "Play" msgstr "" +#, python-brace-format +msgid "Executable not found for EGS game: {0}" +msgstr "" + +msgid "Cannot launch game while another game is running" +msgstr "" + +msgid "Launching" +msgstr "" + +#, python-brace-format +msgid "Failed to launch game: {0}" +msgstr "" + msgid "Invalid command format (native)" msgstr "" @@ -488,12 +610,6 @@ msgstr "" msgid "File not found: {0}" msgstr "" -msgid "Cannot launch game while another game is running" -msgstr "" - -msgid "Launching" -msgstr "" - msgid "Reboot" msgstr "" diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo index 8a9d5ae125fd08871cb461a25896c848714fd80a..137b6917473ee1d5fdbd39e968833c31f8e056d4 100644 GIT binary patch delta 19 acmX@ie3*H{1P&u31w#ufBjb%Tv>5?Dcm=iq delta 19 acmX@ie3*H{1P((J1w#WXWAlwOv>5?DTm`fM diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po index 4d7bc2e..f2b448f 100644 --- a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po +++ b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-14 10:37+0500\n" +"POT-Creation-Date: 2025-06-22 18:23+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es_ES\n" @@ -26,31 +26,70 @@ msgstr "" msgid "Add to Favorites" msgstr "" +msgid "Import to Legendary" +msgstr "" + +msgid "Remove from Steam" +msgstr "" + +msgid "Add to Steam" +msgstr "" + +msgid "Open Game Folder" +msgstr "" + msgid "Remove from Desktop" msgstr "" msgid "Add to Desktop" msgstr "" -msgid "Edit Shortcut" -msgstr "" - -msgid "Delete from PortProton" -msgstr "" - -msgid "Open Game Folder" -msgstr "" - msgid "Remove from Menu" msgstr "" msgid "Add to Menu" msgstr "" -msgid "Remove from Steam" +msgid "Edit Shortcut" msgstr "" -msgid "Add to Steam" +msgid "Delete from PortProton" +msgstr "" + +msgid "Error" +msgstr "" + +#, python-brace-format +msgid "Legendary executable not found at {0}" +msgstr "" + +msgid "Success" +msgstr "" + +#, python-brace-format +msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect." +msgstr "" + +#, python-brace-format +msgid "Executable file not found for game: {0}" +msgstr "" + +#, python-brace-format +msgid "Opened folder for EGS game '{0}'" +msgstr "" + +#, python-brace-format +msgid "Failed to open game folder: {0}" +msgstr "" + +msgid "Select Game Installation Folder" +msgstr "" + +msgid "No folder selected" +msgstr "" + +#, python-brace-format +msgid "Importing '{0}' to Legendary..." msgstr "" #, python-brace-format @@ -61,14 +100,51 @@ msgstr "" msgid "Removed '{0}' from favorites" msgstr "" -msgid "Error" -msgstr "" - msgid "PortProton is not found." msgstr "" #, python-brace-format -msgid "No executable command found in .desktop for game: {0}" +msgid "start.sh not found at {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to create .desktop file: {0}" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' added to desktop" +msgstr "" + +#, python-brace-format +msgid "Failed to add game to desktop: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Desktop: {{0}}" +msgstr "" + +#, python-brace-format +msgid "Successfully removed game '{0}' from Desktop" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' added to menu" +msgstr "" + +#, python-brace-format +msgid "Failed to add game '{0}' to menu: {1}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from menu: {{0}}" +msgstr "" + +#, python-brace-format +msgid "Successfully removed game '{0}' from menu" +msgstr "" + +#, python-brace-format +msgid "No executable command found in .desktop file for game: {0}" msgstr "" #, python-brace-format @@ -76,7 +152,7 @@ msgid "Failed to parse .desktop file for game: {0}" msgstr "" #, python-brace-format -msgid "Error reading .desktop file: {0}" +msgid "Failed to read .desktop file: {0}" msgstr "" #, python-brace-format @@ -105,7 +181,7 @@ msgid "" msgstr "" #, python-brace-format -msgid "Could not locate .desktop file for '{0}'" +msgid "Could not locate .desktop file for game: {0}" msgstr "" #, python-brace-format @@ -121,35 +197,7 @@ msgid "Failed to delete custom data: {0}" msgstr "" #, python-brace-format -msgid "Game '{0}' added to menu" -msgstr "" - -#, python-brace-format -msgid "Failed to add game to menu: {0}" -msgstr "" - -#, python-brace-format -msgid "Failed to remove game from menu: {0}" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' removed from menu" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' added to desktop" -msgstr "" - -#, python-brace-format -msgid "Failed to add game to desktop: {0}" -msgstr "" - -#, python-brace-format -msgid "Failed to remove game from Desktop: {0}" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' removed from Desktop" +msgid "Failed to add game '{0}' to desktop: {1}" msgstr "" msgid "Game name and executable path are required." @@ -174,25 +222,54 @@ msgstr "" msgid "Failed to copy cover image: {0}" msgstr "" -msgid "Restart Steam" -msgstr "" - -msgid "" -"The game was added successfully.\n" -"Please restart Steam for changes to take effect." -msgstr "" - -msgid "" -"The game was removed successfully.\n" -"Please restart Steam for changes to take effect." +#, python-brace-format +msgid "Failed to add game '{0}' to Steam: {1}" msgstr "" #, python-brace-format -msgid "Opened folder for '{0}'" +msgid "" +"'{0}' was removed from Steam. Please restart Steam for changes to take " +"effect." +msgstr "" + +msgid "PortProton directory not found" +msgstr "" + +msgid "Steam directory not found" +msgstr "" + +msgid "Failed to get Steam user ID" +msgstr "" + +msgid "Steam shortcuts file not found" msgstr "" #, python-brace-format -msgid "Failed to open game folder: {0}" +msgid "Failed to create backup of shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to load shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' not found in Steam shortcuts" +msgstr "" + +#, python-brace-format +msgid "Failed to update shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove EGS game '{0}' from Steam: {1}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Steam: {1}" +msgstr "" + +#, python-brace-format +msgid "Successfully opened folder for '{0}'" msgstr "" msgid "Edit Game" @@ -235,10 +312,10 @@ msgstr "" msgid "Loading Epic Games Store games..." msgstr "" -msgid "No description available" +msgid "Never" msgstr "" -msgid "Never" +msgid "No description available" msgstr "" msgid "Supported" @@ -382,6 +459,21 @@ msgstr "" msgid "Gamepad haptic feedback:" msgstr "" +msgid "Open Legendary Login" +msgstr "" + +msgid "Legendary Authentication:" +msgstr "" + +msgid "Enter Legendary Authorization Code" +msgstr "" + +msgid "Authorization Code:" +msgstr "" + +msgid "Submit Code" +msgstr "" + msgid "Save Settings" msgstr "" @@ -397,6 +489,22 @@ msgstr "" msgid "Failed to open Legendary login page" msgstr "" +msgid "Please enter an authorization code" +msgstr "" + +msgid "Successfully authenticated with Legendary" +msgstr "" + +#, python-brace-format +msgid "Legendary authentication failed: {0}" +msgstr "" + +msgid "Legendary executable not found" +msgstr "" + +msgid "Unexpected error during authentication" +msgstr "" + msgid "Confirm Reset" msgstr "" @@ -478,6 +586,20 @@ msgstr "" msgid "Play" msgstr "" +#, python-brace-format +msgid "Executable not found for EGS game: {0}" +msgstr "" + +msgid "Cannot launch game while another game is running" +msgstr "" + +msgid "Launching" +msgstr "" + +#, python-brace-format +msgid "Failed to launch game: {0}" +msgstr "" + msgid "Invalid command format (native)" msgstr "" @@ -488,12 +610,6 @@ msgstr "" msgid "File not found: {0}" msgstr "" -msgid "Cannot launch game while another game is running" -msgstr "" - -msgid "Launching" -msgstr "" - msgid "Reboot" msgstr "" diff --git a/portprotonqt/locales/messages.pot b/portprotonqt/locales/messages.pot index 53f7f74..9b291dc 100644 --- a/portprotonqt/locales/messages.pot +++ b/portprotonqt/locales/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: PortProtonQt 0.1.1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-14 10:37+0500\n" +"POT-Creation-Date: 2025-06-22 18:23+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -24,31 +24,70 @@ msgstr "" msgid "Add to Favorites" msgstr "" +msgid "Import to Legendary" +msgstr "" + +msgid "Remove from Steam" +msgstr "" + +msgid "Add to Steam" +msgstr "" + +msgid "Open Game Folder" +msgstr "" + msgid "Remove from Desktop" msgstr "" msgid "Add to Desktop" msgstr "" -msgid "Edit Shortcut" -msgstr "" - -msgid "Delete from PortProton" -msgstr "" - -msgid "Open Game Folder" -msgstr "" - msgid "Remove from Menu" msgstr "" msgid "Add to Menu" msgstr "" -msgid "Remove from Steam" +msgid "Edit Shortcut" msgstr "" -msgid "Add to Steam" +msgid "Delete from PortProton" +msgstr "" + +msgid "Error" +msgstr "" + +#, python-brace-format +msgid "Legendary executable not found at {0}" +msgstr "" + +msgid "Success" +msgstr "" + +#, python-brace-format +msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect." +msgstr "" + +#, python-brace-format +msgid "Executable file not found for game: {0}" +msgstr "" + +#, python-brace-format +msgid "Opened folder for EGS game '{0}'" +msgstr "" + +#, python-brace-format +msgid "Failed to open game folder: {0}" +msgstr "" + +msgid "Select Game Installation Folder" +msgstr "" + +msgid "No folder selected" +msgstr "" + +#, python-brace-format +msgid "Importing '{0}' to Legendary..." msgstr "" #, python-brace-format @@ -59,14 +98,51 @@ msgstr "" msgid "Removed '{0}' from favorites" msgstr "" -msgid "Error" -msgstr "" - msgid "PortProton is not found." msgstr "" #, python-brace-format -msgid "No executable command found in .desktop for game: {0}" +msgid "start.sh not found at {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to create .desktop file: {0}" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' added to desktop" +msgstr "" + +#, python-brace-format +msgid "Failed to add game to desktop: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Desktop: {{0}}" +msgstr "" + +#, python-brace-format +msgid "Successfully removed game '{0}' from Desktop" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' added to menu" +msgstr "" + +#, python-brace-format +msgid "Failed to add game '{0}' to menu: {1}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from menu: {{0}}" +msgstr "" + +#, python-brace-format +msgid "Successfully removed game '{0}' from menu" +msgstr "" + +#, python-brace-format +msgid "No executable command found in .desktop file for game: {0}" msgstr "" #, python-brace-format @@ -74,7 +150,7 @@ msgid "Failed to parse .desktop file for game: {0}" msgstr "" #, python-brace-format -msgid "Error reading .desktop file: {0}" +msgid "Failed to read .desktop file: {0}" msgstr "" #, python-brace-format @@ -103,7 +179,7 @@ msgid "" msgstr "" #, python-brace-format -msgid "Could not locate .desktop file for '{0}'" +msgid "Could not locate .desktop file for game: {0}" msgstr "" #, python-brace-format @@ -119,35 +195,7 @@ msgid "Failed to delete custom data: {0}" msgstr "" #, python-brace-format -msgid "Game '{0}' added to menu" -msgstr "" - -#, python-brace-format -msgid "Failed to add game to menu: {0}" -msgstr "" - -#, python-brace-format -msgid "Failed to remove game from menu: {0}" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' removed from menu" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' added to desktop" -msgstr "" - -#, python-brace-format -msgid "Failed to add game to desktop: {0}" -msgstr "" - -#, python-brace-format -msgid "Failed to remove game from Desktop: {0}" -msgstr "" - -#, python-brace-format -msgid "Game '{0}' removed from Desktop" +msgid "Failed to add game '{0}' to desktop: {1}" msgstr "" msgid "Game name and executable path are required." @@ -172,25 +220,54 @@ msgstr "" msgid "Failed to copy cover image: {0}" msgstr "" -msgid "Restart Steam" -msgstr "" - -msgid "" -"The game was added successfully.\n" -"Please restart Steam for changes to take effect." -msgstr "" - -msgid "" -"The game was removed successfully.\n" -"Please restart Steam for changes to take effect." +#, python-brace-format +msgid "Failed to add game '{0}' to Steam: {1}" msgstr "" #, python-brace-format -msgid "Opened folder for '{0}'" +msgid "" +"'{0}' was removed from Steam. Please restart Steam for changes to take " +"effect." +msgstr "" + +msgid "PortProton directory not found" +msgstr "" + +msgid "Steam directory not found" +msgstr "" + +msgid "Failed to get Steam user ID" +msgstr "" + +msgid "Steam shortcuts file not found" msgstr "" #, python-brace-format -msgid "Failed to open game folder: {0}" +msgid "Failed to create backup of shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to load shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Game '{0}' not found in Steam shortcuts" +msgstr "" + +#, python-brace-format +msgid "Failed to update shortcuts.vdf: {0}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove EGS game '{0}' from Steam: {1}" +msgstr "" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Steam: {1}" +msgstr "" + +#, python-brace-format +msgid "Successfully opened folder for '{0}'" msgstr "" msgid "Edit Game" @@ -233,10 +310,10 @@ msgstr "" msgid "Loading Epic Games Store games..." msgstr "" -msgid "No description available" +msgid "Never" msgstr "" -msgid "Never" +msgid "No description available" msgstr "" msgid "Supported" @@ -380,6 +457,21 @@ msgstr "" msgid "Gamepad haptic feedback:" msgstr "" +msgid "Open Legendary Login" +msgstr "" + +msgid "Legendary Authentication:" +msgstr "" + +msgid "Enter Legendary Authorization Code" +msgstr "" + +msgid "Authorization Code:" +msgstr "" + +msgid "Submit Code" +msgstr "" + msgid "Save Settings" msgstr "" @@ -395,6 +487,22 @@ msgstr "" msgid "Failed to open Legendary login page" msgstr "" +msgid "Please enter an authorization code" +msgstr "" + +msgid "Successfully authenticated with Legendary" +msgstr "" + +#, python-brace-format +msgid "Legendary authentication failed: {0}" +msgstr "" + +msgid "Legendary executable not found" +msgstr "" + +msgid "Unexpected error during authentication" +msgstr "" + msgid "Confirm Reset" msgstr "" @@ -476,6 +584,20 @@ msgstr "" msgid "Play" msgstr "" +#, python-brace-format +msgid "Executable not found for EGS game: {0}" +msgstr "" + +msgid "Cannot launch game while another game is running" +msgstr "" + +msgid "Launching" +msgstr "" + +#, python-brace-format +msgid "Failed to launch game: {0}" +msgstr "" + msgid "Invalid command format (native)" msgstr "" @@ -486,12 +608,6 @@ msgstr "" msgid "File not found: {0}" msgstr "" -msgid "Cannot launch game while another game is running" -msgstr "" - -msgid "Launching" -msgstr "" - msgid "Reboot" msgstr "" diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo index c4ff2c685767fdc37e79790ec276dcab8969d22f..ca88f21fe9d35e8b884d5d941bfc6ebd9ea36f31 100644 GIT binary patch delta 6295 zcma);33Qaz6~|u$5n@6@_5>5YB!sYp$Ph$iaY2*=A|kj{t>p}v35kSEOeQWUXON)S z0|oVo#SIm;Zq*YA(MT$|oYHed>ddj#da!D%tw(Fs;!+D*`uorKWhOzavi;us-hKDo z`@i?S@X)gEwJDLevj%Q5{CbJMC-8UDAkC7$=A{`km+851KHLl|;0JIpOiwpvD9nSI zumTfq<09RD2- zgr7sj{I$o!a0v5383YG2q1H>_1XuwT;6*T-^UV?liu@|54QpW)+ywDuUi7}lU>@^# zpd5Y$709{1jQsyODuHVz)aOO*)0%(U?-vT3Y@H_)Kd>@X31JPX( zPJlA}36z5s@I-ir$30N?UqLBWmgDX_56b>(sQnfe!!D>Ecn#{@&vMY8y(TBum3lPP z#*?9%W)W0ijZguthmw2;OocB%1@H>Qr}-M@!*uGT{S}Z;=2S>-<^rgmSqe{rjd|#= z0Jic$2R#iP__{a$x5s?Es36#`V zK=sxJD5dU$>bb}c1`dP$P#Js!rN|&UMGlHQmO7Sa z1Jw&pLEVPeyzi=?G9L?NKM6W;Ivm9L=5hv#GyqkTtDy|HKy~}CU?uE?%4|Oz06+BB z{|VI_2jMYL(JGMvPzR?$1)c}BKHOU`fe~d==^4~`oB|cV3@Aru!IR;9Z~Z1HW!Awx zDaJeu?`GcU7;`5aOeag~tx$F^!twAes034K9KMAhDZtj1`yVN%ISM2HXxWgkQk(;oPy@eRvN%6@Cm)f#b&U zn!+H|{8=cw$ma|eFgTNz)q%IeNpL@uVJ3w;2hN2v;T=%l--J3K8+m5Hg&uE#Qs(zi zO6HvC2HFVa|0$^bU&1pv-<(L{^`RB&fG43Geg>sPwa-xFfbKz2`60U>`Ip1t$Fb#eQm64CMbKqi_ z4#RLETn!P${1GaXK~vlxDn5t^=3+Pu{u~a4cf&IHIMjjrU>iIDrC2?(N3t1gV!$`^ z7?c8kh0|f`RQ}%wXG4-Oo1pCXLIv`UH_s&cJmw>z4xR?3;9RJZUJQAg%v~P4J-$B8 zyZ>MCp$LPaoSF~Ezyf#%RHT`o zI-wNV2NBD>1*K&E>F%vMH^M**cS2?G2dF0d7gRvQ>72oE9+Z?9!r5>$)IsmU5%5b$ zQYIH8DYys@g3F-nT0E|W1DM|fl}O}%Z_o)P;^C9Q(L{~5B>zdShA;ShT`t+BKz9Bgss z1(yb!0{)h3oip0PM{KkW>E-sx^n$@oU8u!jvwb*y^oU?>TiCy(G3Ydf!Ys5k5eCa= z&0Zvvp#5$7Sic)-Z}=SPTWQYrH#E9AkzCx=NtgwKt;@op<{vg(9&BopmHjroz#d4? zw_}DngKArX{&3K~kWo_N4xJ_b+GTCcPN>dlC8sbEw^m;jsI!+3&9@7N)|D*{hF#g5 zwpK)#GpkQ@jsCW#+Il-IBR9V>qq&IUfon%eo>Da{? z`Gszx@s(MLRH?&Ct>*Swjh8#IukVVNxr+Bh#zSQZG25C0B!9Fh&&^zOLb65;OIaq`f7ls-6rDX;g()^QBP(luh%RglP|Vg~W3V3U~QQ)}4Y*y!H6v*VH2&vWNTxV_F%Hk){%lT~x-xEx8}EQgxS zqP8W=8^ZBB%(%8T*xK65y0*r~Yx~N}&APfFT%XL>wT!g3x~8G#V3QsUS0i26c-Qs! z*ZZvK9{25LuV;Upu^JW2W;IrDNPuB0X9w(|yi-R8uW63g#tF8x5JI4>MQzr==i2gomgWx7C60=iJi|BOiCH;-lgca*t%$s6WtNr z72Ou?h;EPeM7yHTInnLzCE!6#s|kg&)DkduGlJBMmu6R!R~4H*5dZgTZ@a2v2Rpm*sc-#PP&b=yKvl%b(a&}`G0mN zYiCzE=cGlSfKKVvcg{iEUKiov~H1b$X9&QRyXFUXEVk zdRpwTvOIf9Y0uu?uczcYADuoAz!r*pTe__^9$SeD+|zR$Ad_ z&MYVDep`>bT{!EIhVd-px5u7dk!v?r9BUsa$+O$ai!>gtEGN71<@^x{dlO#Th<&vpOT%k<8FrMAdG|A%)dfofTtVO)0#E;Qu`4FLesioaQFT8Tl$?EZvzwELU6m|YW8+=G!lORP4q#Yklkc(v*PSO?NFP2 ze+TvN!Twu!Q7z8gsRvNQ}bwwFYyBpFGub2j_3|t Lb)j>6{Nnr%P!IUV delta 3407 zcmZwH3vg7`9mnyr1R+33LS7O=k{cl8g-JF90wEA0*!Tj|gjy%nnwTu9B`-FK8Zyj6 zXpx$NQ;v>=>S!@o+rpTzC@6wBl*|-ScW0V9Qw3Wca4fCGma$_;YrntjrNwD8oBiB# z&+C8w=bSzIuxDsP;C#Zw#|?k`_@B-Hg6mZJ?_b4KW3nkO$7pQASZu|3?89mJV~oK) zI1~4y#yjEGzl+tBKf-z}qSLL$1k58mOy-@rNe9%kZKlKD+0v(Lp!%)%y|k3V$ZA3${oVF6yo9Go3z zOd766l^akQ+~JlFy7gnIaX-a$Oe7j@jTZw-%~~E9)7*y|a3g9DccLbK&UF;k@ggcC zQ8SETJ54+)ury4r$+!~}@LANBzBGgUFXG`CFC6?7HDLm+WC@ZbQ;XWm z26V6$wbGraEjffbL$9K?;@7ws&!ATBVU=3gT-4T9qB3!3Vj$9?n-}W135mgMK}EhB z72vN>6Z{diRUacsFw(zXaJRg#{xX4;UH?lgQ%?- zMYhX~p$5EwO7&me`hU9hQ^>0hX)0SO=b<{3p$=Ik=HTt9h`TWg``!BOsI9X&0S8e5J&6inKPsRX-1@_A{Slm|`+w4H zaMm@93gCCBfzD$YeuNq*lYJWX5Gd-9UP>kXIE~AH17@SnL>(&Sn^5&nU@?y1I{X0B znBSCfG#|n`{3V{n0eq0&I`vWHyC36l8}3H>niPIrbSAv0t*Ag{>^4-U?n0f3_3ryk z_-)D$BWp45W1x_SfAF9b&u2MuEiT6gFc%M_4&ynbsrd`)aK$*0!QGzR zjtXciDucUlDUKpT=xzqa@3F%ry@T>ixB?${{Vl3PBEOreUxPLH6yAoP;Eh<$>}t0e z)&B@;0iU7zmClR2{{gDqDO`nL&I?3dRAom7>O*xrfr{`8W3qp zW$JJhA=x%7knEX8{4Q=s9kL6k6@89cNC|0LgZ0Q7%=4&rVbsJIBISTd;RGth`KXDi zFa~RogKq9|?ZZ^cgQ!CmM4jTdkeg^OB6FLskg?4I(xZ0usB#Y~kY`cX@+_w6{(sDa zB8uWmdsm7M-i1_|UQ~d;KxOD8D&-fEkC{Ze6hIm3EYzUd?L^&zAjaY-YAeQ20sIN` znBPn;AwB5D{rG#-z*`nZetHMzyO)rM4X>rnb(Ot)R)!s%_2IOx?nZyNQ`hZlY_q*--?E>iePeM)r>X1d?CkJ&`h!fY zn|0swMOOA;W0%7+iK(lnsma&XwZ5mdwYPApZHe*D{aUN5vHfp5TamupK9hdQ?$2n7 z_Le%{;_`)MHa^q&Pa(J@v(yvZmz5o5_s*%c$FhrpE!pW4+`afo&fE5h+*|CCytv&3 zarV7}nYP92*j;(G_Dud_JDi_pd-G##PC=@Dt030K6?pO8;Jr6Y@!01I*V*?gGVHwt zY1dRPwX2H%Z0F|9u|M)2j}31N9S#kLj)h(f9S;rL^A%b7dqTnRme8=9_OCZ^LPtYK zoY1Rk9^Mt)Sn@YdaHRB2Pq3lPdOT0rw58R_4kI25KdPn-6ds6d$zx0VZC$y~o-Vic z)QSXqa9LjP%CeIl&(rpRlVJ}nKW$%FF=(HzOtxbyt4ps25gG{(5Z$m7-WJ{(dM)%~ scn5)=&>Sa1BSe1aYV7PwVOCG#|LJJ&SeX`87^%0jtF{I&RPFQp8`x?&j{pDw diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po index 00fab1d..0ffd21a 100644 --- a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po +++ b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-14 10:37+0500\n" -"PO-Revision-Date: 2025-06-14 10:37+0500\n" +"POT-Creation-Date: 2025-06-22 18:23+0500\n" +"PO-Revision-Date: 2025-06-22 18:22+0500\n" "Last-Translator: \n" "Language: ru_RU\n" "Language-Team: ru_RU \n" @@ -27,32 +27,73 @@ msgstr "Удалить из Избранного" msgid "Add to Favorites" msgstr "Добавить в Избранное" +msgid "Import to Legendary" +msgstr "Импортировать игру" + +msgid "Remove from Steam" +msgstr "Удалить из Steam" + +msgid "Add to Steam" +msgstr "Добавить в Steam" + +msgid "Open Game Folder" +msgstr "Открыть папку с игрой" + msgid "Remove from Desktop" msgstr "Удалить с рабочего стола" msgid "Add to Desktop" msgstr "Добавить на рабочий стол" -msgid "Edit Shortcut" -msgstr "Редактировать" - -msgid "Delete from PortProton" -msgstr "Удалить из PortProton" - -msgid "Open Game Folder" -msgstr "Открыть папку с игрой" - msgid "Remove from Menu" msgstr "Удалить из меню" msgid "Add to Menu" msgstr "Добавить в меню" -msgid "Remove from Steam" -msgstr "Удалить из Steam" +msgid "Edit Shortcut" +msgstr "Редактировать" -msgid "Add to Steam" -msgstr "Добавить в Steam" +msgid "Delete from PortProton" +msgstr "Удалить из PortProton" + +msgid "Error" +msgstr "Ошибка" + +#, python-brace-format +msgid "Legendary executable not found at {0}" +msgstr "Legendary не найден по пути {0}" + +msgid "Success" +msgstr "Успешно" + +#, python-brace-format +msgid "'{0}' was added to Steam. Please restart Steam for changes to take effect." +msgstr "" +"'{0}' был добавлен в Steam. Пожалуйста, перезапустите Steam, чтобы " +"изменения вступили в силу." + +#, python-brace-format +msgid "Executable file not found for game: {0}" +msgstr "Не найден исполняемый файл для игры: {0}" + +#, python-brace-format +msgid "Opened folder for EGS game '{0}'" +msgstr "Открытие папки для игры EGS '{0}'" + +#, python-brace-format +msgid "Failed to open game folder: {0}" +msgstr "Не удалось открыть папку для игры: {0}" + +msgid "Select Game Installation Folder" +msgstr "Выберите папку установки игры" + +msgid "No folder selected" +msgstr "Не выбрана ни одна папка" + +#, python-brace-format +msgid "Importing '{0}' to Legendary..." +msgstr "Игра '{0}' импортирована" #, python-brace-format msgid "Added '{0}' to favorites" @@ -62,23 +103,60 @@ msgstr "Добавление '{0}' в избранное" msgid "Removed '{0}' from favorites" msgstr "Удаление '{0}' из избранного" -msgid "Error" -msgstr "Ошибка" - msgid "PortProton is not found." msgstr "PortProton не найден." #, python-brace-format -msgid "No executable command found in .desktop for game: {0}" -msgstr "Не найдено ни одной исполняемой команды для игры: {0}" +msgid "start.sh not found at {0}" +msgstr "start.sh не найден по адресу {0}" + +#, python-brace-format +msgid "Failed to create .desktop file: {0}" +msgstr "Не удалось создать файл .desktop: {0}" + +#, python-brace-format +msgid "Game '{0}' added to desktop" +msgstr "Игра '{0}' добавлена на рабочий стол" + +#, python-brace-format +msgid "Failed to add game to desktop: {0}" +msgstr "Не удалось добавить игру на рабочий стол: {0}" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Desktop: {{0}}" +msgstr "Не удалось удалить игру '{0}' с рабочего стола: {{0}}" + +#, python-brace-format +msgid "Successfully removed game '{0}' from Desktop" +msgstr "Успешно удалена игра '{0}' с рабочего стола" + +#, python-brace-format +msgid "Game '{0}' added to menu" +msgstr "Игра '{0}' добавлена в меню" + +#, python-brace-format +msgid "Failed to add game '{0}' to menu: {1}" +msgstr "Не удалось добавить игру в меню: '{0}' в меню: {1}" + +#, python-brace-format +msgid "Failed to remove game '{0}' from menu: {{0}}" +msgstr "Не удалось удалить игру '{0}' из меню: {{0}}" + +#, python-brace-format +msgid "Successfully removed game '{0}' from menu" +msgstr "Успешно удалена игра '{0}' из меню" + +#, python-brace-format +msgid "No executable command found in .desktop file for game: {0}" +msgstr "В файле .desktop для игры не найдено ни одной исполняемой команды: {0}" #, python-brace-format msgid "Failed to parse .desktop file for game: {0}" msgstr "Не удалось удалить файл .desktop: {0}" #, python-brace-format -msgid "Error reading .desktop file: {0}" -msgstr "Не удалось удалить файл .desktop: {0}" +msgid "Failed to read .desktop file: {0}" +msgstr "Не удалось прочитать файл .desktop: {0}" #, python-brace-format msgid ".desktop file not found for game: {0}" @@ -108,8 +186,8 @@ msgstr "" ".desktop и настраиваемых данных." #, python-brace-format -msgid "Could not locate .desktop file for '{0}'" -msgstr "Не удалось найти файл .desktop для '{0}'" +msgid "Could not locate .desktop file for game: {0}" +msgstr "Не удалось найти файл .desktop для игры: {0}" #, python-brace-format msgid "Failed to delete .desktop file: {0}" @@ -124,36 +202,8 @@ msgid "Failed to delete custom data: {0}" msgstr "Не удалось удалить настраиваемые данные: {0}" #, python-brace-format -msgid "Game '{0}' added to menu" -msgstr "Игра '{0}' добавлена в меню" - -#, python-brace-format -msgid "Failed to add game to menu: {0}" -msgstr "Не удалось добавить игру в меню: {0}" - -#, python-brace-format -msgid "Failed to remove game from menu: {0}" -msgstr "Не удалось удалить игру из меню: {0}" - -#, python-brace-format -msgid "Game '{0}' removed from menu" -msgstr "Игра '{0}' удалена из меню" - -#, python-brace-format -msgid "Game '{0}' added to desktop" -msgstr "Игра '{0}' добавлена на рабочий стол" - -#, python-brace-format -msgid "Failed to add game to desktop: {0}" -msgstr "Не удалось добавить игру на рабочий стол: {0}" - -#, python-brace-format -msgid "Failed to remove game from Desktop: {0}" -msgstr "Не удалось удалить игру с рабочего стола: {0}" - -#, python-brace-format -msgid "Game '{0}' removed from Desktop" -msgstr "Игра '{0}' удалена с рабочего стола" +msgid "Failed to add game '{0}' to desktop: {1}" +msgstr "Не удалось добавить игру '{0}' на рабочий стол: {1}" msgid "Game name and executable path are required." msgstr "Необходимо указать название игры и путь к исполняемому файлу." @@ -177,30 +227,57 @@ msgstr "Не удалось удалить файл .desktop: {0}" msgid "Failed to copy cover image: {0}" msgstr "Не удалось удалить игру из меню: {0}" -msgid "Restart Steam" -msgstr "Перезапустите Steam" - -msgid "" -"The game was added successfully.\n" -"Please restart Steam for changes to take effect." -msgstr "" -"Игра была успешно добавлена.\n" -"Пожалуйста, перезапустите Steam, чтобы изменения вступили в силу." - -msgid "" -"The game was removed successfully.\n" -"Please restart Steam for changes to take effect." -msgstr "" -"Игра была успешно удалена..\n" -"Пожалуйста, перезапустите Steam, чтобы изменения вступили в силу." +#, python-brace-format +msgid "Failed to add game '{0}' to Steam: {1}" +msgstr "Не удалось добавить игру '{0}' в Steam: {1}" #, python-brace-format -msgid "Opened folder for '{0}'" -msgstr "Открытие папки для '{0}'" +msgid "" +"'{0}' was removed from Steam. Please restart Steam for changes to take " +"effect." +msgstr "" +"'{0}' был удалён из Steam. Пожалуйста, перезапустите Steam, чтобы " +"изменения вступили в силу." + +msgid "PortProton directory not found" +msgstr "PortProton не найден." + +msgid "Steam directory not found" +msgstr "Каталог Steam не найден" + +msgid "Failed to get Steam user ID" +msgstr "Не удалось получить ID пользователя Steam" + +msgid "Steam shortcuts file not found" +msgstr "Файл ярлыков Steam не найден" #, python-brace-format -msgid "Failed to open game folder: {0}" -msgstr "Не удалось открыть папку для игры: {0}" +msgid "Failed to create backup of shortcuts.vdf: {0}" +msgstr "Не удалось создать резервную копию shortcuts.vdf: {0}" + +#, python-brace-format +msgid "Failed to load shortcuts.vdf: {0}" +msgstr "Не удалось загрузить shortcuts.vdf: {0}" + +#, python-brace-format +msgid "Game '{0}' not found in Steam shortcuts" +msgstr "Игра '{0}' не найдена в ярлыках Steam" + +#, python-brace-format +msgid "Failed to update shortcuts.vdf: {0}" +msgstr "Не удалось обновить shortcuts.vdf: {0}" + +#, python-brace-format +msgid "Failed to remove EGS game '{0}' from Steam: {1}" +msgstr "Не удалось удалить игру EGS '{0}' из Steam: {1}" + +#, python-brace-format +msgid "Failed to remove game '{0}' from Steam: {1}" +msgstr "Не удалось удалить игру '{0}' из Steam: {1}" + +#, python-brace-format +msgid "Successfully opened folder for '{0}'" +msgstr "Успешно открыта папка для '{0}'" msgid "Edit Game" msgstr "Редактировать игру" @@ -242,12 +319,12 @@ msgstr "Запустить игру \"{name}\" с помощью PortProton" msgid "Loading Epic Games Store games..." msgstr "Загрузка игр из Epic Games Store..." -msgid "No description available" -msgstr "Описание не найдено" - msgid "Never" msgstr "Никогда" +msgid "No description available" +msgstr "Описание не найдено" + msgid "Supported" msgstr "Поддерживается" @@ -389,6 +466,21 @@ msgstr "Тактильная отдача на геймпаде" msgid "Gamepad haptic feedback:" msgstr "Тактильная отдача на геймпаде:" +msgid "Open Legendary Login" +msgstr "Открыть браузер для входа в Legendary" + +msgid "Legendary Authentication:" +msgstr "Авторизация в Legendary:" + +msgid "Enter Legendary Authorization Code" +msgstr "Введите код авторизации Legendary" + +msgid "Authorization Code:" +msgstr "Код авторизации;" + +msgid "Submit Code" +msgstr "Отправить код" + msgid "Save Settings" msgstr "Сохранить настройки" @@ -404,6 +496,22 @@ msgstr "Открытие страницы входа в Legendary в брауз msgid "Failed to open Legendary login page" msgstr "Не удалось открыть страницу входа в Legendary" +msgid "Please enter an authorization code" +msgstr "Пожалуйста, введите код авторизации" + +msgid "Successfully authenticated with Legendary" +msgstr "Успешная аутентификация в Legendary" + +#, python-brace-format +msgid "Legendary authentication failed: {0}" +msgstr "Не удалось выполнить аутентификацию Legendary: {0}" + +msgid "Legendary executable not found" +msgstr "Не найден исполняемый файл Legendary" + +msgid "Unexpected error during authentication" +msgstr "Неожиданная ошибка при аутентификации" + msgid "Confirm Reset" msgstr "Подтвердите удаление" @@ -487,6 +595,20 @@ msgstr "Остановить" msgid "Play" msgstr "Играть" +#, python-brace-format +msgid "Executable not found for EGS game: {0}" +msgstr "Не найден исполняемый файл для игры EGS: {0}" + +msgid "Cannot launch game while another game is running" +msgstr "Невозможно запустить игру пока запущена другая" + +msgid "Launching" +msgstr "Идёт запуск" + +#, python-brace-format +msgid "Failed to launch game: {0}" +msgstr "Не удалось запустить игру: {0}" + msgid "Invalid command format (native)" msgstr "Неправильный формат команды (нативная версия)" @@ -497,12 +619,6 @@ msgstr "Неправильный формат команды (flatpak)" msgid "File not found: {0}" msgstr "Файл не найден: {0}" -msgid "Cannot launch game while another game is running" -msgstr "Невозможно запустить игру пока запущена другая" - -msgid "Launching" -msgstr "Идёт запуск" - msgid "Reboot" msgstr "Перезагрузить" -- 2.49.0 From 34dff96e9a9ad8921704ad86571be67b26215c21 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 22 Jun 2025 18:26:30 +0500 Subject: [PATCH 13/13] chore(readme): update todo Signed-off-by: Boris Yumankulov --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 256b087..85b5f4e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор) - [ ] Продумать систему вкладок вместо текущей - [ ] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) -- [ ] Разобраться почему теряется часть стилей в Gamescope +- [X] Разобраться почему теряется часть стилей в Gamescope - [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800) - [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) - [X] Получать описания и названия игр из базы данных Steam @@ -39,12 +39,11 @@ - [X] Добавить в карточки данные с AreWeAntiCheatYet - [X] Продублировать бейджи с карточки на страницу с деталями игры - [X] Добавить парсинг ярлыков из Steam -- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки) +- [X] Добавить парсинг ярлыков из EGS - [ ] Избавиться от бинарника legendary - [X] Добавить запуск игр из EGS - [ ] Добавить скачивание игр из EGS - [ ] Добавить поддержку запуска сторонних игр из EGS -- [ ] Добавить поддержку запуска игр с EOS - [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода - [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api) - [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql) @@ -58,6 +57,7 @@ - [X] Добавить систему избранного для карточек - [X] Заменить все `print` на `logging` - [ ] Привести все логи к единому языку +- [ ] Уменьшить количество строк для перевода - [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog) - [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py` - [X] Исправить частичное применение тем на лету -- 2.49.0