forked from Boria138/PortProtonQt
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			5189474631
			...
			8fd44c575b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8fd44c575b | |||
| 65b43c1572 | |||
| f35276abfe | |||
| 6fea9a9a7e | 
| @@ -15,6 +15,7 @@ | |||||||
| - Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений | - Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений | ||||||
| - Исправлено зависание при добавлении или удалении игры в Wayland | - Исправлено зависание при добавлении или удалении игры в Wayland | ||||||
| - Исправлено зависание при поиске игр | - Исправлено зависание при поиске игр | ||||||
|  | - Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity) | ||||||
|  |  | ||||||
| ### Contributors | ### Contributors | ||||||
|  |  | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ class MainWindowProtocol(Protocol): | |||||||
|     _last_card_width: int |     _last_card_width: int | ||||||
|     current_hovered_card: GameCard | None |     current_hovered_card: GameCard | None | ||||||
|     current_focused_card: GameCard | None |     current_focused_card: GameCard | None | ||||||
|  |     gamesListWidget: QWidget | None | ||||||
|  |  | ||||||
| class GameLibraryManager: | class GameLibraryManager: | ||||||
|     def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None): |     def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None): | ||||||
|   | |||||||
| @@ -597,6 +597,9 @@ class InputManager(QObject): | |||||||
|     def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None: |     def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None: | ||||||
|         if not self._gamepad_handling_enabled: |         if not self._gamepad_handling_enabled: | ||||||
|             return |             return | ||||||
|  |         if not hasattr(self._parent, 'gamesListWidget') or self._parent.gamesListWidget is None: | ||||||
|  |             logger.error("gamesListWidget not available yet, skipping D-pad navigation") | ||||||
|  |             return | ||||||
|         try: |         try: | ||||||
|  |  | ||||||
|             app = QApplication.instance() |             app = QApplication.instance() | ||||||
|   | |||||||
| @@ -5,9 +5,6 @@ import signal | |||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
| import psutil | import psutil | ||||||
| import tarfile |  | ||||||
| import glob |  | ||||||
| import re |  | ||||||
|  |  | ||||||
| from portprotonqt.logger import get_logger | from portprotonqt.logger import get_logger | ||||||
| from portprotonqt.dialogs import AddGameDialog, FileExplorer | from portprotonqt.dialogs import AddGameDialog, FileExplorer | ||||||
| @@ -41,7 +38,7 @@ from portprotonqt.game_library_manager import GameLibraryManager | |||||||
|  |  | ||||||
|  |  | ||||||
| from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, | from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox, | ||||||
|                                QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy) |                                QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout) | ||||||
| from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot | from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot | ||||||
| from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices | from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices | ||||||
| from typing import cast | from typing import cast | ||||||
| @@ -772,6 +769,7 @@ class MainWindow(QMainWindow): | |||||||
|     def createInstalledTab(self): |     def createInstalledTab(self): | ||||||
|         self.gamesLibraryWidget = self.game_library_manager.create_games_library_widget() |         self.gamesLibraryWidget = self.game_library_manager.create_games_library_widget() | ||||||
|         self.stackedWidget.addWidget(self.gamesLibraryWidget) |         self.stackedWidget.addWidget(self.gamesLibraryWidget) | ||||||
|  |         self.gamesListWidget = self.game_library_manager.gamesListWidget | ||||||
|         self.game_library_manager.update_game_grid() |         self.game_library_manager.update_game_grid() | ||||||
|  |  | ||||||
|     def resizeEvent(self, event): |     def resizeEvent(self, event): | ||||||
| @@ -1010,24 +1008,12 @@ class MainWindow(QMainWindow): | |||||||
|  |  | ||||||
|         # Путь к дистрибутивам Wine/Proton |         # Путь к дистрибутивам Wine/Proton | ||||||
|         if self.portproton_location is None: |         if self.portproton_location is None: | ||||||
|             content = QLabel(_("PortProton location not set")) |  | ||||||
|             content.setStyleSheet(self.theme.CONTENT_STYLE) |  | ||||||
|             content.setObjectName("tabContent") |  | ||||||
|             layout.addWidget(content) |  | ||||||
|             layout.addStretch(1) |  | ||||||
|             self.stackedWidget.addWidget(self.wineWidget) |  | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         dist_path = os.path.join(self.portproton_location, "data", "dist") |         dist_path = os.path.join(self.portproton_location, "data", "dist") | ||||||
|         prefixes_path = os.path.join(self.portproton_location, "data", "prefixes") |         prefixes_path = os.path.join(self.portproton_location, "data", "prefixes") | ||||||
|  |  | ||||||
|         if not os.path.exists(dist_path): |         if not os.path.exists(dist_path): | ||||||
|             content = QLabel(_("PortProton data/dist not found")) |  | ||||||
|             content.setStyleSheet(self.theme.CONTENT_STYLE) |  | ||||||
|             content.setObjectName("tabContent") |  | ||||||
|             layout.addWidget(content) |  | ||||||
|             layout.addStretch(1) |  | ||||||
|             self.stackedWidget.addWidget(self.wineWidget) |  | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         # Форма с настройками |         # Форма с настройками | ||||||
| @@ -1066,9 +1052,10 @@ class MainWindow(QMainWindow): | |||||||
|  |  | ||||||
|         layout.addLayout(formLayout) |         layout.addLayout(formLayout) | ||||||
|  |  | ||||||
|         # Кнопки для стандартных инструментов Wine |         # Кнопки для стандартных инструментов Wine в сетке 2x3 | ||||||
|         toolsLayout = QHBoxLayout() |         tools_grid = QGridLayout() | ||||||
|         toolsLayout.setSpacing(10) |         tools_grid.setSpacing(10) | ||||||
|  |         tools_grid.setContentsMargins(0, 0, 0, 0) | ||||||
|  |  | ||||||
|         tools = [ |         tools = [ | ||||||
|             ("winecfg", _("Wine Configuration")), |             ("winecfg", _("Wine Configuration")), | ||||||
| @@ -1079,264 +1066,59 @@ class MainWindow(QMainWindow): | |||||||
|             ("cmd", _("Command Prompt")), |             ("cmd", _("Command Prompt")), | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|         for tool_cmd, tool_name in tools: |         for i, (_tool_cmd, tool_name) in enumerate(tools): | ||||||
|             btn = AutoSizeButton(tool_name) |             row = i // 3 | ||||||
|  |             col = i % 3 | ||||||
|  |             btn = AutoSizeButton(tool_name, update_size=False)  # Отключаем авторазмер для избежания проблем | ||||||
|             btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) |             btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) | ||||||
|             btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus) |             btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||||
|             btn.clicked.connect(lambda checked, t=tool_cmd: self.run_wine_tool(t)) |             tools_grid.addWidget(btn, row, col) | ||||||
|             toolsLayout.addWidget(btn) |  | ||||||
|  |         # Растягиваем столбцы равномерно | ||||||
|  |         for col in range(3): | ||||||
|  |             tools_grid.setColumnStretch(col, 1) | ||||||
|  |  | ||||||
|  |         layout.addLayout(tools_grid) | ||||||
|  |  | ||||||
|  |         # Дополнительные инструменты в сетке 1x4 или 2x2 если нужно | ||||||
|  |         additional_grid = QGridLayout() | ||||||
|  |         additional_grid.setSpacing(10) | ||||||
|  |         additional_grid.setContentsMargins(0, 0, 0, 0) | ||||||
|  |  | ||||||
|  |         # Wine Uninstaller | ||||||
|  |         uninstaller_btn = AutoSizeButton(_("Uninstaller"), update_size=False) | ||||||
|  |         uninstaller_btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) | ||||||
|  |         uninstaller_btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||||
|  |         additional_grid.addWidget(uninstaller_btn, 0, 0) | ||||||
|  |  | ||||||
|  |         # Winetricks | ||||||
|  |         winetricks_btn = AutoSizeButton(_("Winetricks"), update_size=False) | ||||||
|  |         winetricks_btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) | ||||||
|  |         winetricks_btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||||
|  |         additional_grid.addWidget(winetricks_btn, 0, 1) | ||||||
|  |  | ||||||
|  |         # Create Backup | ||||||
|  |         create_backup_btn = AutoSizeButton(_("Create Prefix Backup"), update_size=False) | ||||||
|  |         create_backup_btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) | ||||||
|  |         create_backup_btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||||
|  |         additional_grid.addWidget(create_backup_btn, 0, 2) | ||||||
|  |  | ||||||
|  |         # Load Backup | ||||||
|  |         load_backup_btn = AutoSizeButton(_("Load Prefix Backup"), update_size=False) | ||||||
|  |         load_backup_btn.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) | ||||||
|  |         load_backup_btn.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||||
|  |         additional_grid.addWidget(load_backup_btn, 0, 3) | ||||||
|  |  | ||||||
|  |         # Растягиваем столбцы равномерно | ||||||
|  |         for col in range(4): | ||||||
|  |             additional_grid.setColumnStretch(col, 1) | ||||||
|  |  | ||||||
|  |         layout.addLayout(additional_grid) | ||||||
|  |  | ||||||
|         layout.addLayout(toolsLayout) |  | ||||||
|         layout.addStretch(1) |         layout.addStretch(1) | ||||||
|  |  | ||||||
|         self.stackedWidget.addWidget(self.wineWidget) |         self.stackedWidget.addWidget(self.wineWidget) | ||||||
|  |  | ||||||
|     def prepare_wine(self, version): |  | ||||||
|         """Подготавливает окружение Wine/Proton для выбранной версии.""" |  | ||||||
|         if not version: |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         if self.portproton_location is None: |  | ||||||
|             logger.warning("PortProton location not set") |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         dist_path = os.path.join(self.portproton_location, "data", "dist") |  | ||||||
|         winendir = os.path.join(dist_path, version) |  | ||||||
|         if not os.path.exists(winendir): |  | ||||||
|             logger.warning(f"Wine directory not found: {winendir}") |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         files_dir = os.path.join(winendir, "files") |  | ||||||
|         dist_dir = os.path.join(winendir, "dist") |  | ||||||
|         proton_tar = os.path.join(winendir, "proton_dist.tar") |  | ||||||
|  |  | ||||||
|         if os.path.isdir(files_dir) and not os.path.isdir(dist_dir): |  | ||||||
|             for item in os.listdir(winendir): |  | ||||||
|                 if item not in ["files", "version"]: |  | ||||||
|                     item_path = os.path.join(winendir, item) |  | ||||||
|                     if os.path.isdir(item_path): |  | ||||||
|                         shutil.rmtree(item_path) |  | ||||||
|                     else: |  | ||||||
|                         os.remove(item_path) |  | ||||||
|             if os.path.exists(files_dir): |  | ||||||
|                 for item in os.listdir(files_dir): |  | ||||||
|                     shutil.move(os.path.join(files_dir, item), winendir) |  | ||||||
|                 os.rmdir(files_dir) |  | ||||||
|         elif not os.path.isdir(files_dir) and os.path.isdir(dist_dir): |  | ||||||
|             for item in os.listdir(winendir): |  | ||||||
|                 if item not in ["dist", "version"]: |  | ||||||
|                     item_path = os.path.join(winendir, item) |  | ||||||
|                     if os.path.isdir(item_path): |  | ||||||
|                         shutil.rmtree(item_path) |  | ||||||
|                     else: |  | ||||||
|                         os.remove(item_path) |  | ||||||
|             if os.path.exists(dist_dir): |  | ||||||
|                 for item in os.listdir(dist_dir): |  | ||||||
|                     shutil.move(os.path.join(dist_dir, item), winendir) |  | ||||||
|                 os.rmdir(dist_dir) |  | ||||||
|         elif os.path.isfile(proton_tar): |  | ||||||
|             with tarfile.open(proton_tar) as tar: |  | ||||||
|                 tar.extractall(winendir) |  | ||||||
|             os.remove(proton_tar) |  | ||||||
|             for item in os.listdir(winendir): |  | ||||||
|                 if item not in ["bin", "lib", "lib64", "share", "version"]: |  | ||||||
|                     item_path = os.path.join(winendir, item) |  | ||||||
|                     if os.path.isdir(item_path): |  | ||||||
|                         shutil.rmtree(item_path) |  | ||||||
|                     else: |  | ||||||
|                         os.remove(item_path) |  | ||||||
|  |  | ||||||
|         if os.path.exists(winendir): |  | ||||||
|             # Создать файл version |  | ||||||
|             version_file = os.path.join(winendir, "version") |  | ||||||
|             if not os.path.exists(version_file): |  | ||||||
|                 with open(version_file, "w") as f: |  | ||||||
|                     f.write(version) |  | ||||||
|  |  | ||||||
|             # Симлинк lib64/wine |  | ||||||
|             lib_wine = os.path.join(winendir, "lib", "wine", "x86_64-unix") |  | ||||||
|             lib64_wine = os.path.join(winendir, "lib64", "wine") |  | ||||||
|             if not os.path.lexists(lib64_wine) and os.path.exists(lib_wine): |  | ||||||
|                 os.makedirs(os.path.join(winendir, "lib64"), exist_ok=True) |  | ||||||
|                 self.safe_symlink(os.path.join(winendir, "lib", "wine"), lib64_wine) |  | ||||||
|  |  | ||||||
|             # Обработка mono и gecko |  | ||||||
|             tmp_path = os.path.join(self.portproton_location, "tmp") |  | ||||||
|             os.makedirs(tmp_path, exist_ok=True) |  | ||||||
|             for component in ["mono", "gecko"]: |  | ||||||
|                 share_wine_comp = os.path.join(winendir, "share", "wine", component) |  | ||||||
|                 tmp_comp = os.path.join(tmp_path, component) |  | ||||||
|                 if os.path.lexists(share_wine_comp) and os.path.islink(share_wine_comp): |  | ||||||
|                     logger.info(f"{share_wine_comp} is symlink. OK.") |  | ||||||
|                 elif os.path.isdir(share_wine_comp): |  | ||||||
|                     self.safe_copytree(share_wine_comp, tmp_comp) |  | ||||||
|                     self.safe_rmtree(share_wine_comp) |  | ||||||
|                     self.safe_symlink(tmp_comp, share_wine_comp) |  | ||||||
|                     logger.info(f"Copied {component} to tmp and created symlink. OK.") |  | ||||||
|                 else: |  | ||||||
|                     self.safe_rmtree(share_wine_comp) |  | ||||||
|                     if os.path.exists(tmp_comp): |  | ||||||
|                         self.safe_symlink(tmp_comp, share_wine_comp) |  | ||||||
|                     logger.warning(f"{share_wine_comp} is broken symlink. Repaired.") |  | ||||||
|  |  | ||||||
|             # Модификация wine.inf |  | ||||||
|             wine_inf = os.path.join(winendir, "share", "wine", "wine.inf") |  | ||||||
|             if os.path.exists(wine_inf): |  | ||||||
|                 with open(wine_inf) as f: |  | ||||||
|                     lines = f.readlines() |  | ||||||
|  |  | ||||||
|                 nvidia_uuid = 'Global,"{41FCC608-8496-4DEF-B43E-7D9BD675A6FF}",0x10001,0x00000001' |  | ||||||
|                 has_nvidia = any(nvidia_uuid in line for line in lines) |  | ||||||
|                 if not has_nvidia: |  | ||||||
|                     lines.append('HKLM,Software\\NVIDIA Corporation\\Global,"{41FCC608-8496-4DEF-B43E-7D9BD675A6FF}",0x10001,0x00000001\n') |  | ||||||
|                     lines.append('HKLM,System\\ControlSet001\\Services\\nvlddmkm,"{41FCC608-8496-4DEF-B43E-7D9BD675A6FF}",0x10001,0x00000001\n') |  | ||||||
|  |  | ||||||
|                 new_lines = [] |  | ||||||
|                 for line in lines: |  | ||||||
|                     if 'Steam.exe' in line or r'\\Valve\\Steam' in line or 'winemenubuilder' in line: |  | ||||||
|                         continue |  | ||||||
|                     new_lines.append(line) |  | ||||||
|                 lines = new_lines |  | ||||||
|  |  | ||||||
|                 with open(wine_inf, "w") as f: |  | ||||||
|                     f.writelines(lines) |  | ||||||
|  |  | ||||||
|             # Удаление steam и winemenubuilder файлов |  | ||||||
|             for libdir in ["lib", "lib64"]: |  | ||||||
|                 lib_path = os.path.join(winendir, libdir) |  | ||||||
|                 if os.path.exists(lib_path): |  | ||||||
|                     # *steam* |  | ||||||
|                     for pattern in [ |  | ||||||
|                         os.path.join(lib_path, "*steam*"), |  | ||||||
|                         os.path.join(lib_path, "wine", "*", "*steam*"), |  | ||||||
|                         os.path.join(lib_path, "wine", "*-windows", "winemenubuilder.exe") |  | ||||||
|                     ]: |  | ||||||
|                         for file_path in glob.glob(pattern, recursive=True): |  | ||||||
|                             try: |  | ||||||
|                                 os.remove(file_path) |  | ||||||
|                             except Exception: |  | ||||||
|                                 pass |  | ||||||
|  |  | ||||||
|     def safe_symlink(self, src, dst): |  | ||||||
|         """Создает симлинк, удаляя dst если существует.""" |  | ||||||
|         if os.path.exists(dst): |  | ||||||
|             if os.path.islink(dst): |  | ||||||
|                 os.remove(dst) |  | ||||||
|             else: |  | ||||||
|                 shutil.rmtree(dst) |  | ||||||
|         os.symlink(src, dst) |  | ||||||
|  |  | ||||||
|     def safe_copytree(self, src, dst): |  | ||||||
|         """Копирует директорию, удаляя dst если существует.""" |  | ||||||
|         if os.path.exists(dst): |  | ||||||
|             shutil.rmtree(dst) |  | ||||||
|         shutil.copytree(src, dst) |  | ||||||
|  |  | ||||||
|     def safe_rmtree(self, path): |  | ||||||
|         """Удаляет директорию если существует.""" |  | ||||||
|         if os.path.exists(path): |  | ||||||
|             shutil.rmtree(path) |  | ||||||
|  |  | ||||||
|     def clean_wine_dist_dirs(self): |  | ||||||
|         """Normalizes Wine dist directory names to uppercase with underscores.""" |  | ||||||
|         if self.portproton_location is None: |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         dist_path = os.path.join(self.portproton_location, "data", "dist") |  | ||||||
|         if not os.path.exists(dist_path): |  | ||||||
|             return |  | ||||||
|         for entry in os.scandir(dist_path): |  | ||||||
|             if entry.is_dir(): |  | ||||||
|                 dist_dir = entry.name |  | ||||||
|                 dist_dir_stripped = re.sub(r'\s+', ' ', dist_dir.strip()) |  | ||||||
|                 dist_dir_new = dist_dir_stripped.replace(' ', '_').upper() |  | ||||||
|                 if dist_dir_new != dist_dir: |  | ||||||
|                     new_path = os.path.join(dist_path, dist_dir_new) |  | ||||||
|                     if not os.path.exists(new_path): |  | ||||||
|                         try: |  | ||||||
|                             os.rename(entry.path, new_path) |  | ||||||
|                             logger.info(f"Renamed {dist_dir} to {dist_dir_new}") |  | ||||||
|                         except Exception as e: |  | ||||||
|                             logger.error(f"Failed to rename {dist_dir} to {dist_dir_new}: {e}") |  | ||||||
|  |  | ||||||
|     def run_wine_tool(self, tool_cmd: str): |  | ||||||
|         """Запускает инструмент Wine с выбранной версией и префиксом.""" |  | ||||||
|         version = self.wineCombo.currentText() |  | ||||||
|         prefix = self.prefixCombo.currentText() |  | ||||||
|  |  | ||||||
|         if not version: |  | ||||||
|             QMessageBox.warning(self, _("Error"), _("Please select a Wine/Proton version")) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         if not prefix: |  | ||||||
|             QMessageBox.warning(self, _("Error"), _("Please select a prefix")) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         if self.portproton_location is None: |  | ||||||
|             QMessageBox.warning(self, _("Error"), _("PortProton location not set")) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         # Clean and normalize dist directories |  | ||||||
|         self.clean_wine_dist_dirs() |  | ||||||
|  |  | ||||||
|         # Repopulate wineCombo with normalized names |  | ||||||
|         dist_path = os.path.join(self.portproton_location, "data", "dist") |  | ||||||
|         self.wine_versions = sorted([d for d in os.listdir(dist_path) if os.path.isdir(os.path.join(dist_path, d))]) |  | ||||||
|         self.wineCombo.clear() |  | ||||||
|         self.wineCombo.addItems(self.wine_versions) |  | ||||||
|  |  | ||||||
|         # Try to select the normalized original version |  | ||||||
|         version_normalized = version.strip().replace(' ', '_').upper() |  | ||||||
|         index = self.wineCombo.findText(version_normalized) |  | ||||||
|         if index != -1: |  | ||||||
|             self.wineCombo.setCurrentIndex(index) |  | ||||||
|             version = version_normalized |  | ||||||
|         elif self.wine_versions: |  | ||||||
|             self.wineCombo.setCurrentIndex(0) |  | ||||||
|             version = self.wine_versions[0] |  | ||||||
|         else: |  | ||||||
|             QMessageBox.warning(self, _("Error"), _("No Wine versions found after cleaning")) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         # Prepare Wine for the (possibly updated) version |  | ||||||
|         self.prepare_wine(version) |  | ||||||
|  |  | ||||||
|         prefixes_path = os.path.join(self.portproton_location, "data", "prefixes") |  | ||||||
|         winendir = os.path.join(dist_path, version) |  | ||||||
|         wine_bin = os.path.join(winendir, "bin", "wine") |  | ||||||
|         wineserver_bin = os.path.join(winendir, "bin", "wineserver") |  | ||||||
|  |  | ||||||
|         if not os.path.exists(wine_bin): |  | ||||||
|             QMessageBox.warning(self, _("Error"), _("Wine binary not found: {}").format(wine_bin)) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         prefix_dir = os.path.join(prefixes_path, prefix) |  | ||||||
|         if not os.path.exists(prefix_dir): |  | ||||||
|             QMessageBox.warning(self, _("Error"), _("Prefix not found: {}").format(prefix_dir)) |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         env = os.environ.copy() |  | ||||||
|         env['WINEPREFIX'] = prefix_dir |  | ||||||
|         env['WINEDIR'] = winendir |  | ||||||
|         env['WINE'] = wine_bin |  | ||||||
|         env['WINELOADER'] = wine_bin |  | ||||||
|         env['WINESERVER'] = wineserver_bin |  | ||||||
|         env['WINEDEBUG'] = '-all' |  | ||||||
|         env['WINEDLLOVERRIDES'] = "steam_api,steam_api64,steamclient,steamclient64=n;dotnetfx35.exe,dotnetfx35setup.exe=b;winemenubuilder.exe=;mscoree=" |  | ||||||
|  |  | ||||||
|         try: |  | ||||||
|             if tool_cmd == "cmd": |  | ||||||
|                 # Open Command Prompt in a separate terminal |  | ||||||
|                 term_cmd = ["x-terminal-emulator", "-e", wine_bin, tool_cmd] |  | ||||||
|                 subprocess.Popen(term_cmd, env=env, start_new_session=True) |  | ||||||
|             else: |  | ||||||
|                 cmd = [wine_bin, tool_cmd] |  | ||||||
|                 subprocess.Popen(cmd, env=env, start_new_session=True) |  | ||||||
|             self.statusBar().showMessage(_("Launched {} for prefix {}").format(tool_cmd, prefix), 3000) |  | ||||||
|         except Exception as e: |  | ||||||
|             logger.error(f"Failed to launch {tool_cmd}: {e}") |  | ||||||
|             QMessageBox.warning(self, _("Error"), _("Failed to launch {}: {}").format(tool_cmd, str(e))) |  | ||||||
|  |  | ||||||
|     def createPortProtonTab(self): |     def createPortProtonTab(self): | ||||||
|         """Вкладка 'PortProton Settings'.""" |         """Вкладка 'PortProton Settings'.""" | ||||||
|         self.portProtonWidget = QWidget() |         self.portProtonWidget = QWidget() | ||||||
|   | |||||||
| @@ -211,14 +211,28 @@ def normalize_name(s): | |||||||
|  |  | ||||||
| def is_valid_candidate(candidate): | def is_valid_candidate(candidate): | ||||||
|     """ |     """ | ||||||
|     Checks if a candidate contains forbidden substrings: |     Determines whether a given candidate string is valid for use as a game name. | ||||||
|       - win32 |  | ||||||
|       - win64 |     The function performs the following checks: | ||||||
|       - gamelauncher |       1. Normalizes the candidate using `normalize_name()`. | ||||||
|     Additionally checks the string without spaces. |       2. Rejects the candidate if the normalized name is exactly "game" | ||||||
|     Returns True if the candidate is valid, otherwise False. |          (to avoid overly generic names). | ||||||
|  |       3. Removes spaces and checks for forbidden substrings: | ||||||
|  |          - "win32" | ||||||
|  |          - "win64" | ||||||
|  |          - "gamelauncher" | ||||||
|  |          These are checked in the space-free version of the string. | ||||||
|  |       4. Returns True only if none of the forbidden conditions are met. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         candidate (str): The candidate string to validate. | ||||||
|  |  | ||||||
|  |     Returns: | ||||||
|  |         bool: True if the candidate is valid, False otherwise. | ||||||
|     """ |     """ | ||||||
|     normalized_candidate = normalize_name(candidate) |     normalized_candidate = normalize_name(candidate) | ||||||
|  |     if normalized_candidate == "game": | ||||||
|  |         return False | ||||||
|     normalized_no_space = normalized_candidate.replace(" ", "") |     normalized_no_space = normalized_candidate.replace(" ", "") | ||||||
|     forbidden = ["win32", "win64", "gamelauncher"] |     forbidden = ["win32", "win64", "gamelauncher"] | ||||||
|     for token in forbidden: |     for token in forbidden: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user