Compare commits
	
		
			12 Commits
		
	
	
		
			v0.1.7
			...
			fdd5a0a3d5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fdd5a0a3d5 | |||
| 792e52d981 | |||
| 84d5e46a74 | |||
| 4bc764d568 | |||
| 9a18aa037e | |||
| ed62d2d1c4 | |||
| accc9b18b6 | |||
| 82249d7eab | |||
| 476c896940 | |||
| b1047ba18e | |||
| 987199d8e6 | |||
|  | ef1acd4581 | 
| @@ -94,7 +94,7 @@ jobs: | ||||
|     name: Build Arch Package | ||||
|     runs-on: ubuntu-22.04 | ||||
|     container: | ||||
|       image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166 | ||||
|       image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821 | ||||
|       volumes: | ||||
|         - /usr:/usr-host | ||||
|         - /opt:/opt-host | ||||
|   | ||||
| @@ -180,6 +180,8 @@ jobs: | ||||
|  | ||||
|       - name: Release | ||||
|         uses: https://gitea.com/actions/gitea-release-action@v1 | ||||
|         env: | ||||
|             NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 | ||||
|         with: | ||||
|           body_path: changelog.txt | ||||
|           token: ${{ env.GITEA_TOKEN }} | ||||
|   | ||||
| @@ -138,7 +138,7 @@ jobs: | ||||
|     needs: changes | ||||
|     if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch' | ||||
|     container: | ||||
|       image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166 | ||||
|       image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821 | ||||
|       volumes: | ||||
|         - /usr:/usr-host | ||||
|         - /opt:/opt-host | ||||
|   | ||||
							
								
								
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -3,6 +3,23 @@ | ||||
| Все заметные изменения в этом проекте фиксируются в этом файле. | ||||
| Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/). | ||||
|  | ||||
| ## [Unreleased] | ||||
|  | ||||
| ### Added | ||||
| - В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению | ||||
|  | ||||
| ### Changed | ||||
| - При завершении автоустановки приложение больше не перезапускается | ||||
|  | ||||
| ### Fixed | ||||
| - Исправлено наложение карточек при смене фильтра игр | ||||
|  | ||||
|  | ||||
| ### Contributors | ||||
| - @Vector_null | ||||
|  | ||||
| --- | ||||
|  | ||||
| ## [0.1.7] - 2025-10-12 | ||||
|  | ||||
| ### Added | ||||
|   | ||||
							
								
								
									
										15
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								TODO.md
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| - [X] Адаптировать структуру проекта для поддержки инструментов сборки | ||||
| - [X] Добавить возможность управления с геймпада | ||||
| - [ ] Добавить возможность управления с тачскрина | ||||
| - [X] Добавить возможность управления с тачскрина (Формально и так есть) | ||||
| - [X] Добавить возможность управления с мыши и клавиатуры | ||||
| - [X] Добавить систему тем [Документация](documentation/theme_guide) | ||||
| - [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено) | ||||
| @@ -11,18 +11,18 @@ | ||||
| - [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800) | ||||
| - [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) | ||||
| - [X] Получать описания и названия игр из базы данных Steam | ||||
| - [X] Получать обложки для игр из SteamGridDB или CDN Steam | ||||
| - [X] Получать обложки для игр из CDN Steam | ||||
| - [X] Оптимизировать работу со Steam API для ускорения времени запуска | ||||
| - [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley) | ||||
| - [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода | ||||
| - [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода | ||||
| - [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки) | ||||
| - [X] Избавиться от вызовов yad | ||||
| - [X] Реализовать собственный системный трей вместо использования трея PortProton | ||||
| - [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.) | ||||
| - [X] Добавить экранную клавиатуру в поиск | ||||
| - [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту) | ||||
| - [X] Добавить индикацию запуска приложения | ||||
| - [X] Достигнуть паритета функциональности с Ingame | ||||
| - [ ] Достигнуть паритета функциональности с PortProton | ||||
| - [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов) | ||||
| - [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}` | ||||
| - [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/) | ||||
| - [X] Добавить переводы в переопределения | ||||
| @@ -49,7 +49,7 @@ | ||||
| - [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter) | ||||
| - [X] Добавить систему избранного для карточек | ||||
| - [X] Заменить все `print` на `logging` | ||||
| - [ ] Привести все логи к единому языку | ||||
| - [X] Привести все логи к единому языку | ||||
| - [X] Уменьшить количество подстановок в переводах | ||||
| - [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog) | ||||
| - [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py` | ||||
| @@ -62,7 +62,6 @@ | ||||
| - [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63) | ||||
| - [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)) | ||||
| - [X] Скопировать логику управления с D-pad на стрелки с клавиатуры | ||||
| - [ ] Доделать светлую тему | ||||
| - [ ] Добавить подсказки к управлению с геймпада | ||||
| - [X] Добавить подсказки к управлению с геймпада | ||||
| - [X] Добавить миниатюры к выбору файлов в диалоге добавления игры | ||||
| - [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры | ||||
|   | ||||
| @@ -21,9 +21,9 @@ Current translation status: | ||||
|  | ||||
| | Locale | Progress | Translated | | ||||
| | :----- | -------: | ---------: | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 240 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 240 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 of 240 | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 247 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 247 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 247 of 247 | | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
| @@ -21,9 +21,9 @@ | ||||
|  | ||||
| | Локаль | Прогресс | Переведено | | ||||
| | :----- | -------: | ---------: | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 240 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 240 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 из 240 | | ||||
| | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 247 | | ||||
| | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 247 | | ||||
| | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 247 из 247 | | ||||
|  | ||||
| --- | ||||
|  | ||||
|   | ||||
| @@ -259,6 +259,25 @@ def save_rumble_config(rumble_enabled): | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
| def read_gamepad_type(): | ||||
|     """Reads the gamepad type from the [Gamepad] section. | ||||
|     Returns 'xbox' if the parameter is missing. | ||||
|     """ | ||||
|     cp = read_config_safely(CONFIG_FILE) | ||||
|     if cp is None or not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "type"): | ||||
|         save_gamepad_type("xbox") | ||||
|         return "xbox" | ||||
|     return cp.get("Gamepad", "type", fallback="xbox").lower() | ||||
|  | ||||
| def save_gamepad_type(gpad_type): | ||||
|     """Saves the gamepad type to the [Gamepad] section.""" | ||||
|     cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser() | ||||
|     if "Gamepad" not in cp: | ||||
|         cp["Gamepad"] = {} | ||||
|     cp["Gamepad"]["type"] = gpad_type | ||||
|     with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: | ||||
|         cp.write(configfile) | ||||
|  | ||||
| def ensure_default_proxy_config(): | ||||
|     """Ensures the [Proxy] section exists in the configuration file. | ||||
|     Creates it with empty values if missing. | ||||
|   | ||||
| @@ -91,6 +91,130 @@ def generate_thumbnail(inputfile, outfile, size=128, force_resize=True): | ||||
|         logger.error(f"Ошибка при сохранении миниатюры: {e}") | ||||
|         return False | ||||
|  | ||||
| def create_dialog_hints_widget(theme, main_window, input_manager, context='default'): | ||||
|     """ | ||||
|     Common function to create hints widget for all dialogs. | ||||
|     Uses main_window for get_button_icon/get_nav_icon, input_manager for gamepad detection. | ||||
|     """ | ||||
|     theme_manager = ThemeManager() | ||||
|     current_theme_name = read_theme_from_config() | ||||
|  | ||||
|     hintsWidget = QWidget() | ||||
|     hintsWidget.setStyleSheet(theme.STATUS_BAR_STYLE) | ||||
|     hintsLayout = QHBoxLayout(hintsWidget) | ||||
|     hintsLayout.setContentsMargins(10, 0, 10, 0) | ||||
|     hintsLayout.setSpacing(20) | ||||
|  | ||||
|     dialog_actions = [] | ||||
|  | ||||
|     # Context-specific actions (gamepad only, no keyboard) | ||||
|     if context == 'file_explorer': | ||||
|         dialog_actions = [ | ||||
|             ("confirm", _("Open")),        # A / Cross | ||||
|             ("add_game", _("Select Dir")), # X / Triangle | ||||
|             ("prev_dir", _("Prev Dir")),   # Y / Square | ||||
|             ("back", _("Cancel")),         # B / Circle | ||||
|             ("context_menu", _("Menu")),   # Start / Options | ||||
|         ] | ||||
|     elif context == 'winetricks': | ||||
|         dialog_actions = [ | ||||
|             ("confirm", _("Toggle")),         # A / Cross | ||||
|             ("add_game", _("Install")),       # X / Triangle | ||||
|             ("prev_dir", _("Force Install")), # Y / Square | ||||
|             ("back", _("Cancel")),            # B / Circle | ||||
|             ("prev_tab", _("Prev Tab")),      # LB / L1 | ||||
|             ("next_tab", _("Next Tab")),      # RB / R1 | ||||
|         ] | ||||
|  | ||||
|     hints_labels = []  # Store for updates (returned for class storage) | ||||
|  | ||||
|     def make_hint(icon_name, text, action=None): | ||||
|         container = QWidget() | ||||
|         hlayout = QHBoxLayout(container) | ||||
|         hlayout.setContentsMargins(0, 5, 0, 0) | ||||
|         hlayout.setSpacing(6) | ||||
|  | ||||
|         icon_label = QLabel() | ||||
|         icon_label.setFixedSize(26, 26) | ||||
|         icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter) | ||||
|  | ||||
|         pixmap = QPixmap() | ||||
|         icon_path = theme_manager.get_theme_image(icon_name, current_theme_name) | ||||
|         if icon_path: | ||||
|             pixmap.load(str(icon_path)) | ||||
|         if not pixmap.isNull(): | ||||
|             icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) | ||||
|  | ||||
|         hlayout.addWidget(icon_label) | ||||
|  | ||||
|         text_label = QLabel(text) | ||||
|         text_label.setStyleSheet(theme.LAST_LAUNCH_VALUE_STYLE) | ||||
|         text_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft) | ||||
|         hlayout.addWidget(text_label) | ||||
|  | ||||
|         # Initially hidden; show only if gamepad connected | ||||
|         container.setVisible(False) | ||||
|         hints_labels.append((container, icon_label, action)) | ||||
|  | ||||
|         hintsLayout.addWidget(container) | ||||
|  | ||||
|     # Add gamepad hints only | ||||
|     for action, text in dialog_actions: | ||||
|         make_hint("placeholder", text, action) | ||||
|  | ||||
|     hintsLayout.addStretch() | ||||
|  | ||||
|     # Return widget and labels for class storage | ||||
|     return hintsWidget, hints_labels | ||||
|  | ||||
| def update_dialog_hints(hints_labels, main_window, input_manager, theme_manager, current_theme_name): | ||||
|     """ | ||||
|     Common function to update hints for any dialog. | ||||
|     """ | ||||
|     if not input_manager or not main_window: | ||||
|         # Hide all if no input_manager or main_window | ||||
|         for container, _, _ in hints_labels: | ||||
|             container.setVisible(False) | ||||
|         return | ||||
|  | ||||
|     is_gamepad = input_manager.gamepad is not None | ||||
|     if not is_gamepad: | ||||
|         # Hide all hints if no gamepad | ||||
|         for container, _, _ in hints_labels: | ||||
|             container.setVisible(False) | ||||
|         return | ||||
|  | ||||
|     gtype = input_manager.gamepad_type | ||||
|     gamepad_actions = ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir', 'prev_tab', 'next_tab'] | ||||
|  | ||||
|     for container, icon_label, action in hints_labels: | ||||
|         if action and action in gamepad_actions: | ||||
|             container.setVisible(True) | ||||
|             # Update icon using main_window methods | ||||
|             if action in ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir']: | ||||
|                 icon_name = main_window.get_button_icon(action, gtype) | ||||
|             else:  # only prev_tab/next_tab (treat as nav) | ||||
|                 direction = 'left' if action == 'prev_tab' else 'right' | ||||
|                 icon_name = main_window.get_nav_icon(direction, gtype) | ||||
|             icon_path = theme_manager.get_theme_image(icon_name, current_theme_name) | ||||
|             pixmap = QPixmap() | ||||
|             if icon_path: | ||||
|                 pixmap.load(str(icon_path)) | ||||
|             if not pixmap.isNull(): | ||||
|                 icon_label.setPixmap(pixmap.scaled( | ||||
|                     26, 26, | ||||
|                     Qt.AspectRatioMode.KeepAspectRatio, | ||||
|                     Qt.TransformationMode.SmoothTransformation | ||||
|                 )) | ||||
|             else: | ||||
|                 # Fallback to placeholder | ||||
|                 placeholder = theme_manager.get_theme_image("placeholder", current_theme_name) | ||||
|                 if placeholder: | ||||
|                     pixmap.load(str(placeholder)) | ||||
|                     icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) | ||||
|         else: | ||||
|             container.setVisible(False) | ||||
|  | ||||
| class FileSelectedSignal(QObject): | ||||
|     file_selected = Signal(str)  # Сигнал с путем к выбранному файлу | ||||
|  | ||||
| @@ -185,6 +309,7 @@ class FileExplorer(QDialog): | ||||
|         self.initial_path = initial_path  # Store initial path if provided | ||||
|         self.thumbnail_cache = {}  # Cache for loaded thumbnails | ||||
|         self.pending_thumbnails = set()  # Track files pending thumbnail loading | ||||
|         self.main_window = None  # Add reference to MainWindow | ||||
|         self.setup_ui() | ||||
|  | ||||
|         # Window settings | ||||
| @@ -198,6 +323,7 @@ class FileExplorer(QDialog): | ||||
|         while parent: | ||||
|             if hasattr(parent, 'input_manager'): | ||||
|                 self.input_manager = cast("MainWindow", parent).input_manager | ||||
|                 self.main_window = parent | ||||
|             if hasattr(parent, 'context_menu_manager'): | ||||
|                 self.context_menu_manager = cast("MainWindow", parent).context_menu_manager | ||||
|             parent = parent.parent() | ||||
| @@ -214,6 +340,17 @@ class FileExplorer(QDialog): | ||||
|             self.current_path = os.path.expanduser("~")  # Fallback to home if initial path is invalid | ||||
|         self.update_file_list() | ||||
|  | ||||
|         # Create hints widget using common function | ||||
|         self.current_theme_name = read_theme_from_config() | ||||
|         self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='file_explorer') | ||||
|         self.main_layout.addWidget(self.hints_widget) | ||||
|  | ||||
|         # Connect signals | ||||
|         if self.input_manager: | ||||
|             self.input_manager.button_event.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)) | ||||
|             self.input_manager.dpad_moved.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)) | ||||
|             update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name) | ||||
|  | ||||
|     class ThumbnailLoader(QRunnable): | ||||
|         """Class for asynchronous thumbnail loading in a separate thread.""" | ||||
|         class Signals(QObject): | ||||
| @@ -1037,8 +1174,6 @@ Icon={icon_path} | ||||
|         return desktop_entry, desktop_path | ||||
|  | ||||
| class WinetricksDialog(QDialog): | ||||
|     """Dialog for managing Winetricks components in a prefix.""" | ||||
|  | ||||
|     def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None): | ||||
|         super().__init__(parent) | ||||
|         self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config()) | ||||
| @@ -1071,6 +1206,36 @@ class WinetricksDialog(QDialog): | ||||
|         self.setup_ui() | ||||
|         self.load_lists() | ||||
|  | ||||
|         # Find input_manager and main_window | ||||
|         self.input_manager = None | ||||
|         self.main_window = None | ||||
|         parent = self.parent() | ||||
|         while parent: | ||||
|             if hasattr(parent, 'input_manager'): | ||||
|                 self.input_manager = cast("MainWindow", parent).input_manager | ||||
|                 self.main_window = parent | ||||
|             parent = parent.parent() | ||||
|  | ||||
|         self.current_theme_name = read_theme_from_config() | ||||
|  | ||||
|         # Enable Winetricks-specific mode | ||||
|         if self.input_manager: | ||||
|             self.input_manager.enable_winetricks_mode(self) | ||||
|  | ||||
|         # Create hints widget using common function | ||||
|         self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='winetricks') | ||||
|         self.main_layout.addWidget(self.hints_widget) | ||||
|  | ||||
|         # Connect signals (use self.theme_manager) | ||||
|         if self.input_manager: | ||||
|             self.input_manager.button_event.connect( | ||||
|                 lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name) | ||||
|             ) | ||||
|             self.input_manager.dpad_moved.connect( | ||||
|                 lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name) | ||||
|             ) | ||||
|             update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name) | ||||
|  | ||||
|     def update_winetricks(self): | ||||
|         """Update the winetricks script.""" | ||||
|         if not self.downloader.has_internet(): | ||||
| @@ -1143,15 +1308,15 @@ class WinetricksDialog(QDialog): | ||||
|  | ||||
|     def setup_ui(self): | ||||
|         """Set up the user interface with tabs and tables.""" | ||||
|         main_layout = QVBoxLayout(self) | ||||
|         main_layout.setContentsMargins(10, 10, 10, 10) | ||||
|         main_layout.setSpacing(10) | ||||
|         self.main_layout = QVBoxLayout(self) | ||||
|         self.main_layout.setContentsMargins(10, 10, 10, 10) | ||||
|         self.main_layout.setSpacing(10) | ||||
|  | ||||
|         # Log output | ||||
|         self.log_output = QTextEdit() | ||||
|         self.log_output.setReadOnly(True) | ||||
|         self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE) | ||||
|         main_layout.addWidget(self.log_output) | ||||
|         self.main_layout.addWidget(self.log_output) | ||||
|  | ||||
|         # Tab widget | ||||
|         self.tab_widget = QTabWidget() | ||||
| @@ -1258,7 +1423,7 @@ class WinetricksDialog(QDialog): | ||||
|             "settings": self.settings_container | ||||
|         } | ||||
|  | ||||
|         main_layout.addWidget(self.tab_widget) | ||||
|         self.main_layout.addWidget(self.tab_widget) | ||||
|  | ||||
|         # Buttons | ||||
|         button_layout = QHBoxLayout() | ||||
| @@ -1272,7 +1437,7 @@ class WinetricksDialog(QDialog): | ||||
|         button_layout.addWidget(self.cancel_button) | ||||
|         button_layout.addWidget(self.force_button) | ||||
|         button_layout.addWidget(self.install_button) | ||||
|         main_layout.addLayout(button_layout) | ||||
|         self.main_layout.addLayout(button_layout) | ||||
|  | ||||
|         self.cancel_button.clicked.connect(self.reject) | ||||
|         self.force_button.clicked.connect(lambda: self.install_selected(force=True)) | ||||
| @@ -1497,3 +1662,15 @@ class WinetricksDialog(QDialog): | ||||
|         """Добавляет в лог.""" | ||||
|         self.log_output.append(message) | ||||
|         self.log_output.moveCursor(QTextCursor.MoveOperation.End) | ||||
|  | ||||
|     def closeEvent(self, event): | ||||
|         """Disable mode on close.""" | ||||
|         if self.input_manager: | ||||
|             self.input_manager.disable_winetricks_mode() | ||||
|         super().closeEvent(event) | ||||
|  | ||||
|     def reject(self): | ||||
|         """Disable mode on reject.""" | ||||
|         if self.input_manager: | ||||
|             self.input_manager.disable_winetricks_mode() | ||||
|         super().reject() | ||||
|   | ||||
| @@ -217,6 +217,16 @@ class GameLibraryManager: | ||||
|         else: | ||||
|             self._update_game_grid_immediate() | ||||
|  | ||||
|     def force_update_cards_library(self): | ||||
|         if self.gamesListWidget and self.gamesListLayout: | ||||
|             self.gamesListLayout.invalidate() | ||||
|             self.gamesListWidget.updateGeometry() | ||||
|             widget = self.gamesListWidget | ||||
|             QTimer.singleShot(0, lambda: ( | ||||
|                 widget.adjustSize(), | ||||
|                 widget.updateGeometry() | ||||
|             )) | ||||
|  | ||||
|     def _update_game_grid_immediate(self): | ||||
|         """Updates the game grid with the provided or current game list.""" | ||||
|         if self.gamesListLayout is None or self.gamesListWidget is None: | ||||
| @@ -346,6 +356,8 @@ class GameLibraryManager: | ||||
|                 self.gamesListWidget.updateGeometry() | ||||
|                 self.main_window._last_card_width = self.card_width | ||||
|  | ||||
|                 self.force_update_cards_library() | ||||
|  | ||||
|         self.is_filtering = False  # Reset flag in any case | ||||
|  | ||||
|     def _apply_filter_visibility(self, search_text: str): | ||||
| @@ -453,11 +465,3 @@ class GameLibraryManager: | ||||
|     def filter_games_delayed(self): | ||||
|         """Filters games based on search text and updates the grid.""" | ||||
|         self.update_game_grid(is_filter=True) | ||||
|  | ||||
|     def calculate_columns(self, card_width: int) -> int: | ||||
|         """Calculate the number of columns based on card width and assumed container width.""" | ||||
|         # Assuming a typical container width; adjust as needed | ||||
|         available_width = 1200  # Example width, can be dynamic if widget access is added | ||||
|         spacing = 15  # Assumed spacing between cards | ||||
|         columns = max(1, (available_width - spacing) // (card_width + spacing)) | ||||
|         return min(columns, 8)  # Cap at reasonable max | ||||
|   | ||||
| @@ -5,15 +5,15 @@ from typing import Protocol, cast | ||||
| from evdev import InputDevice, InputEvent, ecodes, list_devices, ff | ||||
| from enum import Enum | ||||
| from pyudev import Context, Monitor, MonitorObserver, Device | ||||
| from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView | ||||
| from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox, QListWidget, QTableWidget, QAbstractItemView, QTableWidgetItem | ||||
| from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer | ||||
| from PySide6.QtGui import QKeyEvent, QMouseEvent | ||||
| from portprotonqt.logger import get_logger | ||||
| from portprotonqt.image_utils import FullscreenDialog | ||||
| from portprotonqt.custom_widgets import NavLabel, AutoSizeButton | ||||
| from portprotonqt.game_card import GameCard | ||||
| from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config | ||||
| from portprotonqt.dialogs import AddGameDialog, WinetricksDialog | ||||
| from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config, read_gamepad_type | ||||
| from portprotonqt.dialogs import AddGameDialog | ||||
| from portprotonqt.virtual_keyboard import VirtualKeyboard | ||||
|  | ||||
| logger = get_logger(__name__) | ||||
| @@ -87,8 +87,13 @@ class InputManager(QObject): | ||||
|         super().__init__(cast(QObject, main_window)) | ||||
|         self._parent = main_window | ||||
|         self._gamepad_handling_enabled = True | ||||
|         self.gamepad_type = GamepadType.UNKNOWN | ||||
|         # Ensure attributes exist on main_window | ||||
|         type_str = read_gamepad_type() | ||||
|         if type_str == "playstation": | ||||
|             self.gamepad_type = GamepadType.PLAYSTATION | ||||
|         elif type_str == "xbox": | ||||
|             self.gamepad_type = GamepadType.XBOX | ||||
|         else: | ||||
|             self.gamepad_type = GamepadType.UNKNOWN | ||||
|         self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None) | ||||
|         self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None) | ||||
|         self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None) | ||||
| @@ -271,38 +276,6 @@ class InputManager(QObject): | ||||
|                 elif current_row_idx == 0: | ||||
|                     self._parent.tabButtons[tab_index].setFocus(Qt.FocusReason.OtherFocusReason) | ||||
|  | ||||
|     def detect_gamepad_type(self, device: InputDevice) -> GamepadType: | ||||
|         """ | ||||
|         Определяет тип геймпада по capabilities | ||||
|         """ | ||||
|         caps = device.capabilities() | ||||
|         keys = set(caps.get(ecodes.EV_KEY, [])) | ||||
|  | ||||
|         # Для EV_ABS вытаскиваем только коды (первый элемент кортежа) | ||||
|         abs_axes = {a if isinstance(a, int) else a[0] for a in caps.get(ecodes.EV_ABS, [])} | ||||
|  | ||||
|         # Xbox layout | ||||
|         if {ecodes.BTN_SOUTH, ecodes.BTN_EAST, ecodes.BTN_NORTH, ecodes.BTN_WEST}.issubset(keys): | ||||
|             if {ecodes.ABS_X, ecodes.ABS_Y, ecodes.ABS_RX, ecodes.ABS_RY}.issubset(abs_axes): | ||||
|                 self.gamepad_type = GamepadType.XBOX | ||||
|                 return GamepadType.XBOX | ||||
|  | ||||
|         # PlayStation layout | ||||
|         if ecodes.BTN_TOUCH in keys or (ecodes.BTN_DPAD_UP in keys and ecodes.BTN_EAST in keys): | ||||
|             self.gamepad_type = GamepadType.PLAYSTATION | ||||
|             logger.info(f"Detected {self.gamepad_type.value} controller: {device.name}") | ||||
|             return GamepadType.PLAYSTATION | ||||
|  | ||||
|         # Steam Controller / Deck (трекпады) | ||||
|         if any(a for a in abs_axes if a >= ecodes.ABS_MT_SLOT): | ||||
|             self.gamepad_type = GamepadType.XBOX | ||||
|             logger.info(f"Detected {self.gamepad_type.value} controller: {device.name}") | ||||
|             return GamepadType.XBOX | ||||
|  | ||||
|         # Fallback | ||||
|         self.gamepad_type = GamepadType.XBOX | ||||
|         return GamepadType.XBOX | ||||
|  | ||||
|     def enable_file_explorer_mode(self, file_explorer): | ||||
|         """Настройка обработки геймпада для FileExplorer""" | ||||
|         try: | ||||
| @@ -482,6 +455,171 @@ class InputManager(QObject): | ||||
|         except Exception as e: | ||||
|             logger.error("Error in FileExplorer dpad handler: %s", e) | ||||
|  | ||||
|     def enable_winetricks_mode(self, winetricks_dialog): | ||||
|         """Setup gamepad handling for WinetricksDialog""" | ||||
|         try: | ||||
|             self.winetricks_dialog = winetricks_dialog | ||||
|             self.original_button_handler = self.handle_button_slot | ||||
|             self.original_dpad_handler = self.handle_dpad_slot | ||||
|             self.original_gamepad_state = self._gamepad_handling_enabled | ||||
|             self.handle_button_slot = self.handle_winetricks_button | ||||
|             self.handle_dpad_slot = self.handle_winetricks_dpad | ||||
|             self._gamepad_handling_enabled = True | ||||
|             # Reset dpad timer for table nav | ||||
|             self.dpad_timer.stop() | ||||
|             self.current_dpad_code = None | ||||
|             self.current_dpad_value = 0 | ||||
|             logger.debug("Gamepad handling successfully connected for WinetricksDialog") | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error connecting gamepad handlers for Winetricks: {e}") | ||||
|  | ||||
|     def disable_winetricks_mode(self): | ||||
|         """Restore original main window handlers""" | ||||
|         try: | ||||
|             if self.winetricks_dialog: | ||||
|                 self.handle_button_slot = self.original_button_handler | ||||
|                 self.handle_dpad_slot = self.original_dpad_handler | ||||
|                 self._gamepad_handling_enabled = self.original_gamepad_state | ||||
|                 self.winetricks_dialog = None | ||||
|                 self.dpad_timer.stop() | ||||
|                 self.current_dpad_code = None | ||||
|                 self.current_dpad_value = 0 | ||||
|                 logger.debug("Gamepad handling successfully restored from Winetricks") | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error restoring gamepad handlers from Winetricks: {e}") | ||||
|  | ||||
|     def handle_winetricks_button(self, button_code, value): | ||||
|         if self.winetricks_dialog is None: | ||||
|             return | ||||
|         if value == 0:  # Ignore releases | ||||
|             return | ||||
|         try: | ||||
|             # Always check for popups first, including QMessageBox | ||||
|             popup = QApplication.activePopupWidget() | ||||
|             if popup: | ||||
|                 if isinstance(popup, QMessageBox): | ||||
|                     if button_code in BUTTONS['confirm'] or button_code in BUTTONS['back']: | ||||
|                         popup.accept()  # Close QMessageBox with A or B | ||||
|                         return | ||||
|                 elif isinstance(popup, QMenu): | ||||
|                     if button_code in BUTTONS['confirm']:  # A: Select menu item | ||||
|                         focused = popup.activeAction() | ||||
|                         if focused: | ||||
|                             focused.trigger() | ||||
|                         return | ||||
|                     elif button_code in BUTTONS['back']:  # B: Close menu | ||||
|                         popup.close() | ||||
|                         return | ||||
|  | ||||
|             # Additional check for top-level QMessageBox (in case not active popup yet) | ||||
|             for widget in QApplication.topLevelWidgets(): | ||||
|                 if isinstance(widget, QMessageBox) and widget.isVisible(): | ||||
|                     if button_code in BUTTONS['confirm'] or button_code in BUTTONS['back']: | ||||
|                         widget.accept() | ||||
|                         return | ||||
|  | ||||
|             focused = QApplication.focusWidget() | ||||
|             if button_code in BUTTONS['confirm']:  # A: Toggle checkbox | ||||
|                 if isinstance(focused, QTableWidget): | ||||
|                     current_row = focused.currentRow() | ||||
|                     if current_row >= 0: | ||||
|                         checkbox_item = focused.item(current_row, 0) | ||||
|                         if checkbox_item and isinstance(checkbox_item, QTableWidgetItem): | ||||
|                             new_state = Qt.CheckState.Checked if checkbox_item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked | ||||
|                             checkbox_item.setCheckState(new_state) | ||||
|                 return | ||||
|             elif button_code in BUTTONS['add_game']:  # X: Install (no force) | ||||
|                 self.winetricks_dialog.install_selected(force=False) | ||||
|                 return | ||||
|             elif button_code in BUTTONS['prev_dir']:  # Y: Force Install | ||||
|                 self.winetricks_dialog.install_selected(force=True) | ||||
|                 return | ||||
|             elif button_code in BUTTONS['back']:  # B: Cancel | ||||
|                 self.winetricks_dialog.reject() | ||||
|                 return | ||||
|             elif button_code in BUTTONS['prev_tab']:  # LB: Prev Tab | ||||
|                 current_index = self.winetricks_dialog.tab_widget.currentIndex() | ||||
|                 new_index = max(0, current_index - 1) | ||||
|                 self.winetricks_dialog.tab_widget.setCurrentIndex(new_index) | ||||
|                 self._focus_first_row_in_current_table() | ||||
|                 return | ||||
|             elif button_code in BUTTONS['next_tab']:  # RB: Next Tab | ||||
|                 current_index = self.winetricks_dialog.tab_widget.currentIndex() | ||||
|                 new_index = min(self.winetricks_dialog.tab_widget.count() - 1, current_index + 1) | ||||
|                 self.winetricks_dialog.tab_widget.setCurrentIndex(new_index) | ||||
|                 self._focus_first_row_in_current_table() | ||||
|                 return | ||||
|             # Fallback: Activate focused widget (e.g., buttons) | ||||
|             self._parent.activateFocusedWidget() | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error in handle_winetricks_button: {e}") | ||||
|  | ||||
|     def handle_winetricks_dpad(self, code, value, now): | ||||
|         if self.winetricks_dialog is None: | ||||
|             return | ||||
|         try: | ||||
|             if value == 0:  # Release: Stop repeat | ||||
|                 self.dpad_timer.stop() | ||||
|                 self.current_dpad_code = None | ||||
|                 self.current_dpad_value = 0 | ||||
|                 return | ||||
|  | ||||
|             # Start/update repeat timer for hold navigation | ||||
|             if self.current_dpad_code != code or self.current_dpad_value != value: | ||||
|                 self.dpad_timer.stop() | ||||
|                 self.dpad_timer.setInterval(150 if self.dpad_timer.isActive() else 300)  # Initial slower, then faster repeat | ||||
|                 self.dpad_timer.start() | ||||
|                 self.current_dpad_code = code | ||||
|                 self.current_dpad_value = value | ||||
|  | ||||
|             table = self._get_current_table() | ||||
|             if not table or table.rowCount() == 0: | ||||
|                 return | ||||
|  | ||||
|             current_row = table.currentRow() | ||||
|             if code == ecodes.ABS_HAT0Y:  # Up/Down: Navigate rows | ||||
|                 if value < 0:  # Up | ||||
|                     new_row = max(0, current_row - 1) | ||||
|                 elif value > 0:  # Down | ||||
|                     new_row = min(table.rowCount() - 1, current_row + 1) | ||||
|                 else: | ||||
|                     return | ||||
|                 if new_row != current_row: | ||||
|                     table.setCurrentCell(new_row, 0)  # Focus checkbox column | ||||
|                     table.setFocus(Qt.FocusReason.OtherFocusReason) | ||||
|             elif code == ecodes.ABS_HAT0X:  # Left/Right: Switch tabs | ||||
|                 if value < 0:  # Left: Prev tab | ||||
|                     current_index = self.winetricks_dialog.tab_widget.currentIndex() | ||||
|                     new_index = max(0, current_index - 1) | ||||
|                     self.winetricks_dialog.tab_widget.setCurrentIndex(new_index) | ||||
|                 elif value > 0:  # Right: Next tab | ||||
|                     current_index = self.winetricks_dialog.tab_widget.currentIndex() | ||||
|                     new_index = min(self.winetricks_dialog.tab_widget.count() - 1, current_index + 1) | ||||
|                     self.winetricks_dialog.tab_widget.setCurrentIndex(new_index) | ||||
|                 self._focus_first_row_in_current_table() | ||||
|         except Exception as e: | ||||
|             logger.error(f"Error in handle_winetricks_dpad: {e}") | ||||
|  | ||||
|     def _get_current_table(self): | ||||
|         """Get the current visible table from the tab widget's stacked container.""" | ||||
|         if self.winetricks_dialog is None: | ||||
|             return None | ||||
|         current_container = self.winetricks_dialog.tab_widget.currentWidget() | ||||
|         if current_container and isinstance(current_container, QStackedWidget): | ||||
|             current_table = current_container.widget(1)  # Table is at index 1 (after preloader) | ||||
|             if isinstance(current_table, QTableWidget): | ||||
|                 return current_table | ||||
|         return None | ||||
|  | ||||
|     def _focus_first_row_in_current_table(self): | ||||
|         """Focus the first row in the current table after tab switch.""" | ||||
|         if self.winetricks_dialog is None: | ||||
|             return | ||||
|         table = self._get_current_table() | ||||
|         if table and table.rowCount() > 0: | ||||
|             table.setCurrentCell(0, 0) | ||||
|             table.setFocus(Qt.FocusReason.OtherFocusReason) | ||||
|  | ||||
|     def handle_navigation_repeat(self): | ||||
|         """Плавное повторение движения с переменной скоростью для FileExplorer""" | ||||
|         try: | ||||
| @@ -732,39 +870,6 @@ class InputManager(QObject): | ||||
|                     self._parent.toggleGame(self._parent.current_exec_line, None) | ||||
|                     return | ||||
|  | ||||
|  | ||||
|             if isinstance(active, WinetricksDialog): | ||||
|                 if button_code in BUTTONS['confirm']:  # A button - toggle checkbox | ||||
|                     current_table = active.tab_widget.currentWidget() | ||||
|                     if isinstance(current_table, QTableWidget): | ||||
|                         current_row = current_table.currentRow() | ||||
|                         if current_row >= 0: | ||||
|                             checkbox = current_table.item(current_row, 0) | ||||
|                             if checkbox: | ||||
|                                 checkbox.setCheckState( | ||||
|                                     Qt.CheckState.Unchecked if checkbox.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked | ||||
|                                 ) | ||||
|                     return | ||||
|                 elif button_code in BUTTONS['add_game']:  # X button - install | ||||
|                     active.install_selected(force=False) | ||||
|                     return | ||||
|                 elif button_code in BUTTONS['prev_dir']:  # Y button - force install | ||||
|                     active.install_selected(force=True) | ||||
|                     return | ||||
|                 elif button_code in BUTTONS['back']:  # B button - close dialog | ||||
|                     active.reject() | ||||
|                     return | ||||
|                 elif button_code in BUTTONS['prev_tab']:  # LB - previous tab | ||||
|                     current_idx = active.tab_widget.currentIndex() | ||||
|                     new_idx = (current_idx - 1) % active.tab_widget.count() | ||||
|                     active.tab_widget.setCurrentIndex(new_idx) | ||||
|                     return | ||||
|                 elif button_code in BUTTONS['next_tab']:  # RB - next tab | ||||
|                     current_idx = active.tab_widget.currentIndex() | ||||
|                     new_idx = (current_idx + 1) % active.tab_widget.count() | ||||
|                     active.tab_widget.setCurrentIndex(new_idx) | ||||
|                     return | ||||
|  | ||||
|             # Standard navigation | ||||
|             if button_code in BUTTONS['confirm']: | ||||
|                 self._parent.activateFocusedWidget() | ||||
| @@ -1369,8 +1474,6 @@ class InputManager(QObject): | ||||
|             new_gamepad = self.find_gamepad() | ||||
|             if new_gamepad and new_gamepad != self.gamepad: | ||||
|                 logger.info(f"Gamepad connected: {new_gamepad.name}") | ||||
|                 self.detect_gamepad_type(new_gamepad) | ||||
|                 logger.info(f"Detected gamepad type: {self.gamepad_type.value}") | ||||
|                 self.stop_rumble() | ||||
|                 self.gamepad = new_gamepad | ||||
|                 if self.gamepad_thread: | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -9,7 +9,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PROJECT VERSION\n" | ||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||
| "POT-Creation-Date: 2025-10-12 17:14+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 10:43+0500\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language: de_DE\n" | ||||
| @@ -252,13 +252,37 @@ msgstr "" | ||||
| msgid "Select All" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgid "Open" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -326,12 +350,6 @@ msgstr "" | ||||
| msgid "Settings" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -579,6 +597,9 @@ msgstr "" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -9,7 +9,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PROJECT VERSION\n" | ||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||
| "POT-Creation-Date: 2025-10-12 17:14+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 10:43+0500\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language: es_ES\n" | ||||
| @@ -252,13 +252,37 @@ msgstr "" | ||||
| msgid "Select All" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgid "Open" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -326,12 +350,6 @@ msgstr "" | ||||
| msgid "Settings" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -579,6 +597,9 @@ msgstr "" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
| @@ -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-10-12 17:14+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 10:43+0500\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @@ -250,13 +250,37 @@ msgstr "" | ||||
| msgid "Select All" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgid "Open" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -324,12 +348,6 @@ msgstr "" | ||||
| msgid "Settings" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -577,6 +595,9 @@ msgstr "" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "" | ||||
|  | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -9,8 +9,8 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PROJECT VERSION\n" | ||||
| "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" | ||||
| "POT-Creation-Date: 2025-10-12 17:14+0500\n" | ||||
| "PO-Revision-Date: 2025-10-12 17:13+0500\n" | ||||
| "POT-Creation-Date: 2025-10-16 10:43+0500\n" | ||||
| "PO-Revision-Date: 2025-10-16 10:43+0500\n" | ||||
| "Last-Translator: \n" | ||||
| "Language: ru_RU\n" | ||||
| "Language-Team: ru_RU <LL@li.org>\n" | ||||
| @@ -259,13 +259,37 @@ msgstr "Удалить" | ||||
| msgid "Select All" | ||||
| msgstr "Выбрать всё" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "Идёт запуск {0}" | ||||
| msgid "Open" | ||||
| msgstr "Открыть" | ||||
|  | ||||
| msgid "Select Dir" | ||||
| msgstr "Выбрать папку" | ||||
|  | ||||
| msgid "Prev Dir" | ||||
| msgstr "Предыдущий каталог" | ||||
|  | ||||
| msgid "Cancel" | ||||
| msgstr "Отмена" | ||||
|  | ||||
| msgid "Toggle" | ||||
| msgstr "Переключить" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "Установить" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "Принудительно установить" | ||||
|  | ||||
| msgid "Prev Tab" | ||||
| msgstr "Предыдущая вкладка" | ||||
|  | ||||
| msgid "Next Tab" | ||||
| msgstr "Следующая вкладка" | ||||
|  | ||||
| #, python-brace-format | ||||
| msgid "Launching {0}" | ||||
| msgstr "Идёт запуск {0}" | ||||
|  | ||||
| msgid "File Explorer" | ||||
| msgstr "Проводник" | ||||
|  | ||||
| @@ -333,12 +357,6 @@ msgstr "Шрифты" | ||||
| msgid "Settings" | ||||
| msgstr "Настройки" | ||||
|  | ||||
| msgid "Force Install" | ||||
| msgstr "Принудительно установить" | ||||
|  | ||||
| msgid "Install" | ||||
| msgstr "Установить" | ||||
|  | ||||
| msgid "Winetricks not found. Please try again." | ||||
| msgstr "Winetricks не найден. Повторите попытку." | ||||
|  | ||||
| @@ -588,6 +606,9 @@ msgstr "все" | ||||
| msgid "Games Display Filter:" | ||||
| msgstr "Фильтр игр:" | ||||
|  | ||||
| msgid "Gamepad Type:" | ||||
| msgstr "Тип геймпада:" | ||||
|  | ||||
| msgid "Proxy URL" | ||||
| msgstr "Адрес прокси" | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,7 @@ from portprotonqt.config_utils import ( | ||||
|     read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method, | ||||
|     save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config, | ||||
|     save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config, | ||||
|     clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config | ||||
|     clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type | ||||
| ) | ||||
| from portprotonqt.localization import _, get_egs_language, read_metadata_translations | ||||
| from portprotonqt.howlongtobeat_api import HowLongToBeat | ||||
| @@ -260,6 +260,10 @@ class MainWindow(QMainWindow): | ||||
|                 GamepadType.XBOX: "xbox_y", | ||||
|                 GamepadType.PLAYSTATION: "ps_square", | ||||
|             }, | ||||
|             'prev_dir': { | ||||
|                 GamepadType.XBOX: "xbox_y", | ||||
|                 GamepadType.PLAYSTATION: "ps_square", | ||||
|             }, | ||||
|         } | ||||
|         return mappings.get(action, {}).get(gtype, "placeholder") | ||||
|  | ||||
| @@ -516,12 +520,26 @@ class MainWindow(QMainWindow): | ||||
|             self.install_monitor_timer = None | ||||
|         self.progress_bar.setRange(0, 100) | ||||
|         self.progress_bar.setValue(100) | ||||
|  | ||||
|         if exit_code == 0: | ||||
|             self.update_status_message.emit(_("Installation completed successfully."), 5000) | ||||
|             QTimer.singleShot(500, lambda: self.restart_application()) | ||||
|  | ||||
|             desktop_dir = self.portproton_location or "" | ||||
|             new_desktops = [e.path for e in os.scandir(desktop_dir) if e.name.endswith(".desktop")] | ||||
|             if new_desktops: | ||||
|                 latest = max(new_desktops, key=os.path.getmtime) | ||||
|                 self._process_desktop_file_async( | ||||
|                     latest, | ||||
|                     lambda result: ( | ||||
|                         self.game_library_manager.add_game_incremental(result) | ||||
|                         if result else None | ||||
|                     ) | ||||
|                 ) | ||||
|  | ||||
|         else: | ||||
|             self.update_status_message.emit(_("Installation failed."), 5000) | ||||
|             QMessageBox.warning(self, _("Error"), f"Installation failed (code: {exit_code}).") | ||||
|  | ||||
|         self.progress_bar.setVisible(False) | ||||
|         self.current_install_script = None | ||||
|         if self.install_process: | ||||
| @@ -820,6 +838,25 @@ class MainWindow(QMainWindow): | ||||
|         for i, btn in self.tabButtons.items(): | ||||
|             btn.setChecked(i == index) | ||||
|         self.stackedWidget.setCurrentIndex(index) | ||||
|         if hasattr(self, "game_library_manager"): | ||||
|             mgr = self.game_library_manager | ||||
|             if mgr.gamesListWidget and mgr.gamesListLayout: | ||||
|                 games_layout = mgr.gamesListLayout | ||||
|                 games_widget = mgr.gamesListWidget | ||||
|                 QTimer.singleShot(0, lambda: ( | ||||
|                     games_layout.invalidate(), | ||||
|                     games_widget.adjustSize(), | ||||
|                     games_widget.updateGeometry() | ||||
|                 )) | ||||
|         if hasattr(self, "autoInstallContainer") and hasattr(self, "autoInstallContainerLayout"): | ||||
|             auto_layout = self.autoInstallContainerLayout | ||||
|             auto_widget = self.autoInstallContainer | ||||
|             QTimer.singleShot(0, lambda: ( | ||||
|                 auto_layout.invalidate(), | ||||
|                 auto_widget.adjustSize(), | ||||
|                 auto_widget.updateGeometry() | ||||
|             )) | ||||
|  | ||||
|  | ||||
|     def openSystemOverlay(self): | ||||
|         """Opens the system overlay dialog.""" | ||||
| @@ -1765,7 +1802,22 @@ class MainWindow(QMainWindow): | ||||
|         self.gamesDisplayCombo.setCurrentIndex(idx) | ||||
|         formLayout.addRow(self.gamesDisplayTitle, self.gamesDisplayCombo) | ||||
|  | ||||
|         # 4. Proxy settings | ||||
|         # 4 Gamepad Type | ||||
|         self.gamepadTypeCombo = QComboBox() | ||||
|         self.gamepadTypeCombo.addItems(["Xbox", "PlayStation"]) | ||||
|         self.gamepadTypeCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||
|         self.gamepadTypeCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE) | ||||
|         self.gamepadTypeTitle = QLabel(_("Gamepad Type:")) | ||||
|         self.gamepadTypeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE) | ||||
|         self.gamepadTypeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) | ||||
|         current_type_str = read_gamepad_type() | ||||
|         if current_type_str == "playstation": | ||||
|             self.gamepadTypeCombo.setCurrentText("PlayStation") | ||||
|         else: | ||||
|             self.gamepadTypeCombo.setCurrentText("Xbox") | ||||
|         formLayout.addRow(self.gamepadTypeTitle, self.gamepadTypeCombo) | ||||
|  | ||||
|         # 5. Proxy settings | ||||
|         self.proxyUrlEdit = CustomLineEdit(self, theme=self.theme) | ||||
|         self.proxyUrlEdit.setPlaceholderText(_("Proxy URL")) | ||||
|         self.proxyUrlEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE) | ||||
| @@ -1797,7 +1849,7 @@ class MainWindow(QMainWindow): | ||||
|         self.proxyPasswordTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) | ||||
|         formLayout.addRow(self.proxyPasswordTitle, self.proxyPasswordEdit) | ||||
|  | ||||
|         # 5. Fullscreen setting for application | ||||
|         # 6. Fullscreen setting for application | ||||
|         self.fullscreenCheckBox = QCheckBox(_("Launch Application in Fullscreen")) | ||||
|         self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) | ||||
|         self.fullscreenCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||
| @@ -1808,7 +1860,7 @@ class MainWindow(QMainWindow): | ||||
|         self.fullscreenCheckBox.setChecked(current_fullscreen) | ||||
|         formLayout.addRow(self.fullscreenTitle, self.fullscreenCheckBox) | ||||
|  | ||||
|         # 6. Automatic fullscreen on gamepad connection | ||||
|         # 7. Automatic fullscreen on gamepad connection | ||||
|         self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected")) | ||||
|         self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) | ||||
|         self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||
| @@ -1820,7 +1872,7 @@ class MainWindow(QMainWindow): | ||||
|         self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen) | ||||
|         formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox) | ||||
|  | ||||
|         # 7. Gamepad haptic feedback config | ||||
|         # 8. Gamepad haptic feedback config | ||||
|         self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback")) | ||||
|         self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus) | ||||
|         self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) | ||||
| @@ -1831,7 +1883,7 @@ class MainWindow(QMainWindow): | ||||
|         self.gamepadRumbleCheckBox.setChecked(current_rumble_state) | ||||
|         formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox) | ||||
|  | ||||
|         # # 8. Legendary Authentication | ||||
|         # # 9. Legendary Authentication | ||||
|         # self.legendaryAuthButton = AutoSizeButton( | ||||
|         #     _("Open Legendary Login"), | ||||
|         #     icon=self.theme_manager.get_icon("login")self.theme_manager.get_icon("login") | ||||
| @@ -2011,6 +2063,19 @@ class MainWindow(QMainWindow): | ||||
|         rumble_enabled = self.gamepadRumbleCheckBox.isChecked() | ||||
|         save_rumble_config(rumble_enabled) | ||||
|  | ||||
|         gamepad_type_text = self.gamepadTypeCombo.currentText() | ||||
|         gpad_type = "playstation" if gamepad_type_text == "PlayStation" else "xbox" | ||||
|         save_gamepad_type(gpad_type) | ||||
|  | ||||
|         if hasattr(self, 'input_manager'): | ||||
|             if gpad_type == "playstation": | ||||
|                 self.input_manager.gamepad_type = GamepadType.PLAYSTATION | ||||
|             elif gpad_type == "xbox": | ||||
|                 self.input_manager.gamepad_type = GamepadType.XBOX | ||||
|             else: | ||||
|                 self.input_manager.gamepad_type = GamepadType.UNKNOWN | ||||
|             self.updateControlHints() | ||||
|  | ||||
|         for card in self.game_library_manager.game_card_cache.values(): | ||||
|             card.update_badge_visibility(filter_key) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user