From 2ae3831662ee9c5cb2305fef9ccd4c7920e3294b Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 10:39:50 +0500 Subject: [PATCH 01/47] fix(input_manager): restore keyboard navigation with Up/Down keys Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index daaafbc..6a97dc8 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -390,6 +390,23 @@ class InputManager(QObject): focused._show_context_menu(pos) return True + # Handle Up/Down keys for non-GameCard tabs + if key in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not isinstance(focused, GameCard): + page = self._parent.stackedWidget.currentWidget() + if key == Qt.Key.Key_Down: + if isinstance(focused, NavLabel): + focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively) + focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus] + if focusables: + focusables[0].setFocus() + return True + elif focused: + focused.focusNextChild() + return True + elif key == Qt.Key.Key_Up and focused: + focused.focusPreviousChild() + return True + # Tab switching with Left/Right keys (non-GameCard focus or no focus) idx = self._parent.stackedWidget.currentIndex() total = len(self._parent.tabButtons) From c037af43145bdd54e8286e1427d561de361d8e2c Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 11:21:51 +0500 Subject: [PATCH 02/47] feat(input_manager): Added QMenu handler for Gamepad Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 41 +++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 6a97dc8..55c9674 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -3,7 +3,7 @@ import threading from typing import Protocol, cast from evdev import InputDevice, ecodes, list_devices import pyudev -from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog +from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot from PySide6.QtGui import QKeyEvent from portprotonqt.logger import get_logger @@ -37,8 +37,8 @@ BUTTONS = { 'confirm': {ecodes.BTN_A}, 'back': {ecodes.BTN_B}, 'add_game': {ecodes.BTN_Y}, - 'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TRIGGER_HAPPY7}, - 'next_tab': {ecodes.BTN_TR, ecodes.BTN_TRIGGER_HAPPY5}, + 'prev_tab': {ecodes.BTN_TL}, + 'next_tab': {ecodes.BTN_TR}, 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR}, 'context_menu': {ecodes.BTN_START}, 'menu': {ecodes.BTN_SELECT}, @@ -129,6 +129,21 @@ class InputManager(QObject): return active = QApplication.activeWindow() focused = QApplication.focusWidget() + popup = QApplication.activePopupWidget() + + # Handle QMenu (context menu) + if isinstance(popup, QMenu): + if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: + # Trigger the currently highlighted menu action and close the menu + if popup.activeAction(): + popup.activeAction().trigger() + popup.close() + return + elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']: + # Close the menu + popup.close() + return + return # Закрытие AddGameDialog на кнопку B if button_code in BUTTONS['back'] and isinstance(active, QDialog): @@ -149,7 +164,9 @@ class InputManager(QObject): if isinstance(focused, GameCard): if button_code in BUTTONS['context_menu']: pos = QPoint(focused.width() // 2, focused.height() // 2) - focused._show_context_menu(pos) + menu = focused._show_context_menu(pos) + if menu: + menu.setFocus(Qt.FocusReason.OtherFocusReason) return # Game launch on detail page @@ -188,6 +205,22 @@ class InputManager(QObject): if not app: return active = QApplication.activeWindow() + popup = QApplication.activePopupWidget() + + # Handle QMenu navigation with D-pad + if isinstance(popup, QMenu): + if code == ecodes.ABS_HAT0Y and value != 0: + actions = popup.actions() + if actions: + current_idx = actions.index(popup.activeAction()) if popup.activeAction() in actions else 0 + if value < 0: # Up + next_idx = (current_idx - 1) % len(actions) + popup.setActiveAction(actions[next_idx]) + elif value > 0: # Down + next_idx = (current_idx + 1) % len(actions) + popup.setActiveAction(actions[next_idx]) + return + return # Fullscreen horizontal navigation if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X: From 364e1dd02a316a7e6a721e29c16a64f84cc2dfd2 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 15:16:42 +0500 Subject: [PATCH 03/47] feat(input_manager): Added QComboBox and QListView handler for Gamepad Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 2 +- portprotonqt/input_manager.py | 61 +++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index 6d9eca2..a524c05 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -9,6 +9,7 @@ from PySide6.QtGui import QDesktopServices from portprotonqt.config_utils import parse_desktop_entry from portprotonqt.localization import _ from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam +from portprotonqt.dialogs import AddGameDialog class ContextMenuManager: """Manages context menu actions for game management in PortProtonQT.""" @@ -321,7 +322,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.""" - from portprotonqt.dialogs import AddGameDialog # Local import to avoid circular dependency if not self._check_portproton(): return diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 55c9674..e030ad3 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -3,7 +3,7 @@ import threading from typing import Protocol, cast from evdev import InputDevice, ecodes, list_devices import pyudev -from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu +from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot from PySide6.QtGui import QKeyEvent from portprotonqt.logger import get_logger @@ -134,20 +134,55 @@ class InputManager(QObject): # Handle QMenu (context menu) if isinstance(popup, QMenu): if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: - # Trigger the currently highlighted menu action and close the menu if popup.activeAction(): popup.activeAction().trigger() popup.close() return elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']: - # Close the menu popup.close() return return + # Handle QComboBox + if isinstance(focused, QComboBox): + if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: + focused.showPopup() + return + + # Handle QListView + if isinstance(focused, QListView): + combo = None + parent = focused.parentWidget() + while parent: + if isinstance(parent, QComboBox): + combo = parent + break + parent = parent.parentWidget() + + if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: + idx = focused.currentIndex() + if idx.isValid(): + if combo: + combo.setCurrentIndex(idx.row()) + combo.hidePopup() + combo.setFocus(Qt.FocusReason.OtherFocusReason) + else: + focused.activated.emit(idx) + focused.clicked.emit(idx) + focused.hide() + return + + if button_code in BUTTONS['back']: + if combo: + combo.hidePopup() + combo.setFocus(Qt.FocusReason.OtherFocusReason) + else: + focused.clearSelection() + focused.hide() + # Закрытие AddGameDialog на кнопку B if button_code in BUTTONS['back'] and isinstance(active, QDialog): - active.reject() # Закрываем диалог + active.reject() return # FullscreenDialog @@ -193,7 +228,6 @@ class InputManager(QObject): except Exception as e: logger.error(f"Error in handle_button_slot: {e}", exc_info=True) - @Slot(int, int, float) def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None: try: @@ -205,6 +239,7 @@ class InputManager(QObject): if not app: return active = QApplication.activeWindow() + focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() # Handle QMenu navigation with D-pad @@ -222,6 +257,22 @@ class InputManager(QObject): return return + # Handle QListView navigation with D-pad + if isinstance(focused, QListView) and code == ecodes.ABS_HAT0Y and value != 0: + model = focused.model() + current_index = focused.currentIndex() + if model and current_index.isValid(): + row_count = model.rowCount() + current_row = current_index.row() + if value > 0: # Down + next_row = min(current_row + 1, row_count - 1) + focused.setCurrentIndex(model.index(next_row, current_index.column())) + elif value < 0: # Up + prev_row = max(current_row - 1, 0) + focused.setCurrentIndex(model.index(prev_row, current_index.column())) + focused.scrollTo(focused.currentIndex(), QListView.ScrollHint.PositionAtCenter) + return + # Fullscreen horizontal navigation if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X: if value < 0: From 0f59c46d362baa81f79490231fc2f4ac23b7035a Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 15:26:37 +0500 Subject: [PATCH 04/47] fix(input_manager): handle AddGameDialog navigation with D-pad Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 29 +++++++++++++++++++++++++---- portprotonqt/main_window.py | 1 + 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index e030ad3..2f0caff 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -216,7 +216,9 @@ class InputManager(QObject): elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']: self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None)) elif button_code in BUTTONS['add_game']: - self._parent.openAddGameDialog() + # Only open AddGameDialog if in library tab (index 0) + if self._parent.stackedWidget.currentIndex() == 0: + self._parent.openAddGameDialog() elif button_code in BUTTONS['prev_tab']: idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons) self._parent.switchTab(idx) @@ -242,6 +244,21 @@ class InputManager(QObject): focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() + # Handle AddGameDialog navigation with D-pad + if isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: + if not focused or not active.focusWidget(): + # If no widget is focused, focus the first focusable widget + focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively) + focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus] + if focusables: + focusables[0].setFocus(Qt.FocusReason.OtherFocusReason) + return + if value > 0: # Down + active.focusNextChild() + elif value < 0: # Up + active.focusPreviousChild() + return + # Handle QMenu navigation with D-pad if isinstance(popup, QMenu): if code == ecodes.ABS_HAT0Y and value != 0: @@ -364,7 +381,6 @@ class InputManager(QObject): next_card.setFocus() if scroll_area: scroll_area.ensureWidgetVisible(next_card, 50, 50) - elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down if value > 0: # Down next_row_idx = current_row_idx + 1 @@ -621,6 +637,9 @@ class InputManager(QObject): if focusables: focusables[0].setFocus() return True + elif focused: + focused.focusNextChild() + return True # Navigate up through tab content if key == Qt.Key.Key_Up: if isinstance(focused, NavLabel): @@ -641,8 +660,10 @@ class InputManager(QObject): elif key == Qt.Key.Key_E: if isinstance(focused, QLineEdit): return False - self._parent.openAddGameDialog() - return True + # Only open AddGameDialog if in library tab (index 0) + if self._parent.stackedWidget.currentIndex() == 0: + self._parent.openAddGameDialog() + return True # Toggle fullscreen with F11 if key == Qt.Key.Key_F11: diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index b31f398..77cb547 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -742,6 +742,7 @@ class MainWindow(QMainWindow): return dialog = AddGameDialog(self, self.theme) + dialog.setFocus(Qt.FocusReason.OtherFocusReason) self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог # Предзаполняем путь к .exe при drag-and-drop From 9c4ad0b7bacac08849aff9036561de7b88a9bad2 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 15:28:41 +0500 Subject: [PATCH 05/47] chore(changelog): update Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6472f4..d352e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Пункт в контекстное меню "Удалить из Steam” - Метод сортировки сначала избранное - Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено) +- Обработчики для QMenu и QComboBox на геймпаде ### Changed - Обновлены все иконки @@ -39,6 +40,7 @@ - Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку - D-pad больше не переключает вкладки только RB и LB - Кнопка добавления игры больше не фокусируется +- Диалог добавления игры теперь открывается только в библиотеке ### Fixed - Обработка несуществующей темы с возвратом к “standart” From 3e493571528b6791d93b60cda7fac5d4f2204684 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 19:02:24 +0500 Subject: [PATCH 06/47] feat(build): add appstream metainfo files Signed-off-by: Boris Yumankulov --- README.md | 3 +- build-aux/fedora-git.spec | 6 +- build-aux/fedora.spec | 6 +- .../ru.linux_gaming.PortProtonQt.metainfo.xml | 61 +++++++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml diff --git a/README.md b/README.md index 87e3dcb..598d136 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## В планах - [X] Адаптировать структуру проекта для поддержки инструментов сборки -- [ ] Добавить возможность управление с геймпада +- [X] Добавить возможность управление с геймпада - [ ] Добавить возможность управление с тачскрина - [X] Добавить возможность управление с мыши и клавиатуры - [X] Добавить систему тем [Документация](documentation/theme_guide) @@ -16,6 +16,7 @@ - [ ] Продумать систему вкладок вместо той что есть сейчас - [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS - [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800) +- [ ] Переделать скриншоты для соответсвия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) - [X] Брать описание и названия игр с базы данных Steam - [X] Брать обложки для игр со SteamGridDB или CDN Steam - [X] Оптимизировать работу со SteamApi что бы ускорить время запуска diff --git a/build-aux/fedora-git.spec b/build-aux/fedora-git.spec index 3eace5a..feb0d77 100644 --- a/build-aux/fedora-git.spec +++ b/build-aux/fedora-git.spec @@ -45,7 +45,7 @@ Requires: perl-Image-ExifTool Requires: xdg-utils %description -n python3-%{pypi_name}-git -PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store. +This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup. %prep git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git @@ -62,6 +62,8 @@ cp -r build-aux/share %{buildroot}/usr/ %files -n python3-%{pypi_name}-git -f %{pyproject_files} %{_bindir}/%{pypi_name} -%{_datadir}/* +%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg +%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml +%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop %changelog diff --git a/build-aux/fedora.spec b/build-aux/fedora.spec index 24c5f1c..ba26d25 100644 --- a/build-aux/fedora.spec +++ b/build-aux/fedora.spec @@ -42,7 +42,7 @@ Requires: perl-Image-ExifTool Requires: xdg-utils %description -n python3-%{pypi_name} -PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store. +This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup. %prep git clone https://git.linux-gaming.ru/Boria138/PortProtonQt @@ -61,6 +61,8 @@ cp -r build-aux/share %{buildroot}/usr/ %files -n python3-%{pypi_name} -f %{pyproject_files} %{_bindir}/%{pypi_name} -%{_datadir}/* +%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg +%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml +%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop %changelog diff --git a/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml b/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml new file mode 100644 index 0000000..42af19e --- /dev/null +++ b/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml @@ -0,0 +1,61 @@ + + + PortProtonQt + ru.linux_gaming.PortProtonQt + CC0-1.0 + GPL-3.0-or-later + Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store + Современный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store + +

This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.

+
+ ru.linux_gaming.PortProtonQt.desktop + + Boria138 + + + keyboard + pointing + touch + gamepad + + + #007AFF + #09BEC8 + + + Game + Utility + + https://git.linux-gaming.ru/Boria138/PortProtonQt + https://git.linux-gaming.ru/Boria138/PortProtonQt/issues + + + https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%91%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B0.png + Library + Библиотека + + + https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9A%D0%B0%D1%80%D1%82%D0%BE%D1%87%D0%BA%D0%B0.png + Card detail page + Детали игры + + + https://git.linux-gaming.ru/Boria138/PortProtonQt/src/commit/9c4ad0b7bacac08849aff9036561de7b88a9bad2/portprotonqt/themes/standart/images/screenshots/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8.png + Settings + Настройки + + + + wine + proton + steam + windows + epic games store + egs + qt + portproton + games + + +
From a5977f0f59cdf585e21614a514e3943961ffd158 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 19:08:53 +0500 Subject: [PATCH 07/47] fix: correct badge positioning in GameCard.update_badge_visibility Signed-off-by: Boris Yumankulov --- portprotonqt/game_card.py | 48 ++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/portprotonqt/game_card.py b/portprotonqt/game_card.py index e24e3be..64e7468 100644 --- a/portprotonqt/game_card.py +++ b/portprotonqt/game_card.py @@ -272,35 +272,27 @@ class GameCard(QFrame): top_y = 10 badge_y_positions = [] badge_width = int(self.coverLabel.width() * 2/3) - if self.steam_visible: - steam_x = self.coverLabel.width() - badge_width - right_margin - self.steamLabel.move(steam_x, top_y) - badge_y_positions.append(top_y + self.steamLabel.height()) - if self.egs_visible: - egs_x = self.coverLabel.width() - badge_width - right_margin - egs_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y - self.egsLabel.move(egs_x, egs_y) - badge_y_positions.append(egs_y + self.egsLabel.height()) - if self.portproton_visible: - portproton_x = self.coverLabel.width() - badge_width - right_margin - portproton_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y - self.portprotonLabel.move(portproton_x, portproton_y) - badge_y_positions.append(portproton_y + self.portprotonLabel.height()) - if self.protondbLabel.isVisible(): - protondb_x = self.coverLabel.width() - badge_width - right_margin - protondb_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y - self.protondbLabel.move(protondb_x, protondb_y) - badge_y_positions.append(protondb_y + self.protondbLabel.height()) - if self.anticheatLabel.isVisible(): - anticheat_x = self.coverLabel.width() - badge_width - right_margin - anticheat_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y - self.anticheatLabel.move(anticheat_x, anticheat_y) - self.anticheatLabel.raise_() - self.protondbLabel.raise_() - self.portprotonLabel.raise_() - self.egsLabel.raise_() - self.steamLabel.raise_() + badges = [ + (self.steam_visible, self.steamLabel), + (self.egs_visible, self.egsLabel), + (self.portproton_visible, self.portprotonLabel), + (self.protondbLabel.isVisible(), self.protondbLabel), + (self.anticheatLabel.isVisible(), self.anticheatLabel), + ] + + for is_visible, badge in badges: + if is_visible: + badge_x = self.coverLabel.width() - badge_width - right_margin + badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y + badge.move(badge_x, badge_y) + badge_y_positions.append(badge_y + badge.height()) + + self.anticheatLabel.raise_() + self.protondbLabel.raise_() + self.portprotonLabel.raise_() + self.egsLabel.raise_() + self.steamLabel.raise_() def _show_context_menu(self, pos): """Delegate context menu display to ContextMenuManager.""" From 2377426b27c3ba39a6210f3e45f830a672ee2a0f Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 21:31:07 +0500 Subject: [PATCH 08/47] fix: correct badge positioning in GameCard on display filter change (again) Signed-off-by: Boris Yumankulov --- portprotonqt/game_card.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/portprotonqt/game_card.py b/portprotonqt/game_card.py index 64e7468..e9699a7 100644 --- a/portprotonqt/game_card.py +++ b/portprotonqt/game_card.py @@ -266,13 +266,6 @@ class GameCard(QFrame): self.egsLabel.setVisible(self.egs_visible) self.portprotonLabel.setVisible(self.portproton_visible) - # Reposition badges - right_margin = 8 - badge_spacing = 5 - top_y = 10 - badge_y_positions = [] - badge_width = int(self.coverLabel.width() * 2/3) - badges = [ (self.steam_visible, self.steamLabel), (self.egs_visible, self.egsLabel), @@ -281,6 +274,12 @@ class GameCard(QFrame): (self.anticheatLabel.isVisible(), self.anticheatLabel), ] + right_margin = 8 + badge_spacing = 5 + top_y = 10 + badge_y_positions = [] + badge_width = int(self.coverLabel.width() * 2/3) + for is_visible, badge in badges: if is_visible: badge_x = self.coverLabel.width() - badge_width - right_margin From 83455bc33f163dce94dd0b4383a55bc464a48557 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 21:40:27 +0500 Subject: [PATCH 09/47] fix: restore correct badge positioning on visibility change in GameCard Signed-off-by: Boris Yumankulov --- portprotonqt/game_card.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/portprotonqt/game_card.py b/portprotonqt/game_card.py index e9699a7..42b558a 100644 --- a/portprotonqt/game_card.py +++ b/portprotonqt/game_card.py @@ -261,19 +261,26 @@ class GameCard(QFrame): self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites")) self.egs_visible = (str(self.game_source).lower() == "epic" and display_filter in ("all", "favorites")) self.portproton_visible = (str(self.game_source).lower() == "portproton" and display_filter in ("all", "favorites")) + protondb_visible = bool(self.getProtonDBText(self.protondb_tier)) + anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status)) + # Обновляем видимость бейджей self.steamLabel.setVisible(self.steam_visible) self.egsLabel.setVisible(self.egs_visible) self.portprotonLabel.setVisible(self.portproton_visible) + self.protondbLabel.setVisible(protondb_visible) + self.anticheatLabel.setVisible(anticheat_visible) + # Подготавливаем список всех бейджей с их текущей видимостью badges = [ (self.steam_visible, self.steamLabel), (self.egs_visible, self.egsLabel), (self.portproton_visible, self.portprotonLabel), - (self.protondbLabel.isVisible(), self.protondbLabel), - (self.anticheatLabel.isVisible(), self.anticheatLabel), + (protondb_visible, self.protondbLabel), + (anticheat_visible, self.anticheatLabel), ] + # Пересчитываем позиции бейджей right_margin = 8 badge_spacing = 5 top_y = 10 @@ -287,6 +294,7 @@ class GameCard(QFrame): badge.move(badge_x, badge_y) badge_y_positions.append(badge_y + badge.height()) + # Поднимаем бейджи в правильном порядке (от нижнего к верхнему) self.anticheatLabel.raise_() self.protondbLabel.raise_() self.portprotonLabel.raise_() From bcf319c0241580ffd2b2910614897cc8266570f5 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sat, 7 Jun 2025 22:11:36 +0500 Subject: [PATCH 10/47] feat: optimize slider code Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 77cb547..df5a9fa 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -539,6 +539,12 @@ class MainWindow(QMainWindow): def startSearchDebounce(self, text): self.searchDebounceTimer.start() + def on_slider_value_changed(self, value: int): + self.card_width = value + self.sizeSlider.setToolTip(f"{value} px") + save_card_size(value) + self.updateGameGrid() + def filterGamesDelayed(self): """Filters games based on search text and updates the grid.""" text = self.searchEdit.text().strip().lower() @@ -579,33 +585,16 @@ class MainWindow(QMainWindow): self.sizeSlider.setFixedWidth(150) self.sizeSlider.setToolTip(f"{self.card_width} px") self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE) + self.sizeSlider.valueChanged.connect(self.on_slider_value_changed) sliderLayout.addWidget(self.sizeSlider) layout.addLayout(sliderLayout) - self.sliderDebounceTimer = QTimer(self) - self.sliderDebounceTimer.setSingleShot(True) - self.sliderDebounceTimer.setInterval(40) - - def on_slider_value_changed(): - self.setUpdatesEnabled(False) - self.card_width = self.sizeSlider.value() - self.sizeSlider.setToolTip(f"{self.card_width} px") - self.updateGameGrid() - self.setUpdatesEnabled(True) - self.sizeSlider.valueChanged.connect(lambda val: self.sliderDebounceTimer.start()) - self.sliderDebounceTimer.timeout.connect(on_slider_value_changed) - def calculate_card_width(): available_width = scrollArea.width() - 20 spacing = self.gamesListLayout._spacing target_cards_per_row = 8 calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row calculated_width = max(200, min(calculated_width, 250)) - if not self.sizeSlider.value() == self.card_width: - self.card_width = calculated_width - self.sizeSlider.setValue(self.card_width) - self.sizeSlider.setToolTip(f"{self.card_width} px") - self.updateGameGrid() QTimer.singleShot(0, calculate_card_width) @@ -621,7 +610,6 @@ class MainWindow(QMainWindow): self._last_width = self.width() if abs(self.width() - self._last_width) > 10: self._last_width = self.width() - self.sliderDebounceTimer.start() def loadVisibleImages(self): visible_region = self.gamesListWidget.visibleRegion() From 4de4bdb99ddda46a28b7381616fb6c22b0d70645 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 07:16:02 +0500 Subject: [PATCH 11/47] feat: added system overlay to guide button Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 11 +++- portprotonqt/main_window.py | 6 ++ portprotonqt/system_overlay.py | 100 +++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 portprotonqt/system_overlay.py diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 2f0caff..a9e29d5 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -25,6 +25,8 @@ class MainWindowProtocol(Protocol): ... def toggleGame(self, exec_line: str | None, button: QWidget | None = None) -> None: ... + def openSystemOverlay(self) -> None: + ... stackedWidget: QStackedWidget tabButtons: dict[int, QWidget] gamesListWidget: QWidget @@ -42,6 +44,7 @@ BUTTONS = { 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR}, 'context_menu': {ecodes.BTN_START}, 'menu': {ecodes.BTN_SELECT}, + 'guide': {ecodes.BTN_MODE}, } class InputManager(QObject): @@ -131,6 +134,12 @@ class InputManager(QObject): focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() + # Handle Guide button to open system overlay + if button_code in BUTTONS['guide']: + if not popup and not isinstance(active, QDialog): + self._parent.openSystemOverlay() + return + # Handle QMenu (context menu) if isinstance(popup, QMenu): if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: @@ -244,7 +253,7 @@ class InputManager(QObject): focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() - # Handle AddGameDialog navigation with D-pad + # Handle SystemOverlay or AddGameDialog navigation with D-pad if isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: if not focused or not active.focusWidget(): # If no widget is focused, focus the first focusable widget diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index df5a9fa..90a5f6b 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -13,6 +13,7 @@ from portprotonqt.game_card import GameCard from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel from portprotonqt.input_manager import InputManager from portprotonqt.context_menu_manager import ContextMenuManager +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 @@ -500,6 +501,11 @@ class MainWindow(QMainWindow): btn.setChecked(i == index) self.stackedWidget.setCurrentIndex(index) + def openSystemOverlay(self): + """Opens the system overlay dialog.""" + overlay = SystemOverlay(self, self.theme) + overlay.exec() + def createSearchWidget(self) -> tuple[QWidget, QLineEdit]: self.container = QWidget() self.container.setStyleSheet(self.theme.CONTAINER_STYLE) diff --git a/portprotonqt/system_overlay.py b/portprotonqt/system_overlay.py new file mode 100644 index 0000000..a64b45e --- /dev/null +++ b/portprotonqt/system_overlay.py @@ -0,0 +1,100 @@ +import subprocess +from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QLabel, QMessageBox +from PySide6.QtWidgets import QApplication +from PySide6.QtCore import Qt +from portprotonqt.logger import get_logger +from portprotonqt.localization import _ + +logger = get_logger(__name__) + +class SystemOverlay(QDialog): + """Overlay dialog for system actions like reboot, sleep, shutdown, suspend, and exit.""" + def __init__(self, parent, theme): + super().__init__(parent) + self.theme = theme + self.setWindowTitle(_("System Overlay")) + self.setModal(True) + self.setFixedSize(400, 300) + + layout = QVBoxLayout(self) + layout.setContentsMargins(20, 20, 20, 20) + layout.setSpacing(10) + + title = QLabel(_("System Actions")) + title.setStyleSheet(self.theme.TAB_TITLE_STYLE) + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(title) + + # Reboot button + reboot_button = QPushButton(_("Reboot")) + #reboot_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + reboot_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + reboot_button.clicked.connect(self.reboot) + layout.addWidget(reboot_button) + + # Shutdown button + shutdown_button = QPushButton(_("Shutdown")) + #shutdown_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + shutdown_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + shutdown_button.clicked.connect(self.shutdown) + layout.addWidget(shutdown_button) + + # Suspend button + suspend_button = QPushButton(_("Suspend")) + #suspend_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + suspend_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + suspend_button.clicked.connect(self.suspend) + layout.addWidget(suspend_button) + + # Exit application button + exit_button = QPushButton(_("Exit Application")) + #exit_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + exit_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + exit_button.clicked.connect(self.exit_application) + layout.addWidget(exit_button) + + # Cancel button + cancel_button = QPushButton(_("Cancel")) + #cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + cancel_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + cancel_button.clicked.connect(self.reject) + layout.addWidget(cancel_button) + + # Set focus to the first button + reboot_button.setFocus() + + def reboot(self): + try: + subprocess.run(["systemctl", "reboot"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to reboot: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to reboot the system")) + self.accept() + + def sleep(self): + try: + subprocess.run(["systemctl", "suspend-then-hibernate"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to sleep: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to put the system to sleep")) + self.accept() + + def shutdown(self): + try: + subprocess.run(["systemctl", "poweroff"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to shutdown: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to shutdown the system")) + self.accept() + + def suspend(self): + try: + subprocess.run(["systemctl", "suspend"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to suspend: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to suspend the system")) + self.accept() + + def exit_application(self): + QApplication.quit() + self.accept() From 1ea5fd710c5303ea975148df52a66f7c4aef8d2b Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 09:07:18 +0500 Subject: [PATCH 12/47] feat: added --fullscreen cli argument Signed-off-by: Boris Yumankulov --- portprotonqt/app.py | 15 ++++++++++++++- portprotonqt/cli.py | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 portprotonqt/cli.py diff --git a/portprotonqt/app.py b/portprotonqt/app.py index fdd2bbf..d215643 100644 --- a/portprotonqt/app.py +++ b/portprotonqt/app.py @@ -4,8 +4,9 @@ from PySide6.QtWidgets import QApplication from PySide6.QtGui import QIcon from portprotonqt.main_window import MainWindow from portprotonqt.tray import SystemTray -from portprotonqt.config_utils import read_theme_from_config +from portprotonqt.config_utils import read_theme_from_config, save_fullscreen_config from portprotonqt.logger import get_logger +from portprotonqt.cli import parse_args logger = get_logger(__name__) @@ -28,7 +29,17 @@ def main(): else: logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}") + # Парсинг аргументов командной строки + args = parse_args() + window = MainWindow() + + # Обработка флага --fullscreen + if args.fullscreen: + logger.info("Запуск в полноэкранном режиме по флагу --fullscreen") + save_fullscreen_config(True) + window.showFullScreen() + current_theme_name = read_theme_from_config() tray = SystemTray(app, current_theme_name) tray.show_action.triggered.connect(window.show) @@ -43,7 +54,9 @@ def main(): tray.hide_action.triggered.connect(window.hide) window.settings_saved.connect(recreate_tray) + window.show() + sys.exit(app.exec()) if __name__ == '__main__': diff --git a/portprotonqt/cli.py b/portprotonqt/cli.py new file mode 100644 index 0000000..ed7d096 --- /dev/null +++ b/portprotonqt/cli.py @@ -0,0 +1,16 @@ +import argparse +from portprotonqt.logger import get_logger + +logger = get_logger(__name__) + +def parse_args(): + """ + Парсит аргументы командной строки. + """ + parser = argparse.ArgumentParser(description="PortProtonQT CLI") + parser.add_argument( + "--fullscreen", + action="store_true", + help="Запустить приложение в полноэкранном режиме и сохранить эту настройку" + ) + return parser.parse_args() From a21705da154b4cf896cb67a08cd6b8262164daac Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 09:11:52 +0500 Subject: [PATCH 13/47] feat: hide the games from EGS until after the workout Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 88 +++---------------------------------- 1 file changed, 7 insertions(+), 81 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 90a5f6b..e5d1246 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -260,25 +260,19 @@ class MainWindow(QMainWindow): self.update_status_message.emit ) elif display_filter == "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] + def on_all_games(portproton_games, steam_games): + games = [game for game in portproton_games + steam_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: 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 - ) + lambda sg: on_all_games(pg, sg) ) ) else: - def on_all_games(portproton_games, steam_games, epic_games): + def on_all_games(portproton_games, steam_games): seen = set() games = [] - for game in portproton_games + steam_games + epic_games: + for game in portproton_games + steam_games: name = game[0] if name not in seen: seen.add(name) @@ -286,13 +280,7 @@ class MainWindow(QMainWindow): self.games_loaded.emit(games) self._load_portproton_games_async( lambda pg: self._load_steam_games_async( - 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 - ) + lambda sg: on_all_games(pg, sg) ) ) return [] @@ -915,7 +903,7 @@ class MainWindow(QMainWindow): # 3. Games display_filter self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"] - self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"] + self.filter_labels = [_("all"), "steam", "portproton", _("favorites")] self.gamesDisplayCombo = QComboBox() self.gamesDisplayCombo.addItems(self.filter_labels) self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE) @@ -984,37 +972,6 @@ class MainWindow(QMainWindow): self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen) formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox) - # 7. 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) # Кнопки @@ -1065,37 +1022,6 @@ 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( From 34e70d05f320d7ea440222eb728c1493cde8e4d1 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 09:20:53 +0500 Subject: [PATCH 14/47] feat: add continuous D-pad navigation Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 49 ++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index a9e29d5..4393236 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -4,7 +4,7 @@ from typing import Protocol, cast from evdev import InputDevice, ecodes, list_devices import pyudev from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView -from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot +from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer from PySide6.QtGui import QKeyEvent from portprotonqt.logger import get_logger from portprotonqt.image_utils import FullscreenDialog @@ -72,7 +72,6 @@ class InputManager(QObject): 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) - self.axis_deadzone = axis_deadzone self.initial_axis_move_delay = initial_axis_move_delay self.repeat_axis_move_delay = repeat_axis_move_delay @@ -84,6 +83,12 @@ class InputManager(QObject): self.running = True self._is_fullscreen = read_fullscreen_config() + # Add variables for continuous D-pad movement + self.dpad_timer = QTimer(self) + self.dpad_timer.timeout.connect(self.handle_dpad_repeat) + self.current_dpad_code = None # Tracks the current D-pad axis (e.g., ABS_HAT0X, ABS_HAT0Y) + self.current_dpad_value = 0 # Tracks the current D-pad direction value (e.g., -1, 1) + # Connect signals to slots self.button_pressed.connect(self.handle_button_slot) self.dpad_moved.connect(self.handle_dpad_slot) @@ -239,6 +244,15 @@ class InputManager(QObject): except Exception as e: logger.error(f"Error in handle_button_slot: {e}", exc_info=True) + def handle_dpad_repeat(self) -> None: + """Handle repeated D-pad input while the D-pad is held.""" + if self.current_dpad_code is not None and self.current_dpad_value != 0: + now = time.time() + if (now - self.last_move_time) >= self.current_axis_delay: + self.handle_dpad_slot(self.current_dpad_code, self.current_dpad_value, now) + self.last_move_time = now + self.current_axis_delay = self.repeat_axis_move_delay + @Slot(int, int, float) def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None: try: @@ -253,6 +267,23 @@ class InputManager(QObject): focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() + # Update D-pad state + if value != 0: + self.current_dpad_code = code + self.current_dpad_value = value + if not self.axis_moving: + self.axis_moving = True + self.last_move_time = current_time + self.current_axis_delay = self.initial_axis_move_delay + self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds) + else: + self.current_dpad_code = None + self.current_dpad_value = 0 + self.axis_moving = False + self.current_axis_delay = self.initial_axis_move_delay + self.dpad_timer.stop() # Stop timer when D-pad is released + return + # Handle SystemOverlay or AddGameDialog navigation with D-pad if isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: if not focused or not active.focusWidget(): @@ -307,19 +338,6 @@ class InputManager(QObject): active.show_next() return - # Handle repeated D-pad movement - if value != 0: - if not self.axis_moving: - self.axis_moving = True - elif (current_time - self.last_move_time) < self.current_axis_delay: - return - self.last_move_time = current_time - self.current_axis_delay = self.repeat_axis_move_delay - else: - self.axis_moving = False - self.current_axis_delay = self.initial_axis_move_delay - return - # Library tab navigation (index 0) if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y): focused = QApplication.focusWidget() @@ -783,6 +801,7 @@ class InputManager(QObject): def cleanup(self) -> None: try: self.running = False + self.dpad_timer.stop() if self.gamepad_thread: self.gamepad_thread.join() if self.gamepad: From 14dc44d4f7e55a6b45ed39bdb4528433fadab1d0 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 09:22:51 +0500 Subject: [PATCH 15/47] chore(changelog): update Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d352e6b..516b015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,6 @@ ### Added - Кнопки сброса настроек и очистки кэша -- Начальная интеграция с EGS с помощью [Legendary](https://github.com/derrod/legendary) -- Бейдж EGS - Бейдж PortProton - Зависимость на `xdg-utils` - Интеграция статуса WeAntiCheatYet в карточку @@ -38,9 +36,12 @@ - Установка ширины бейджа в две трети ширины карточки - Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites` - Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку +- Теперь D-pad можно зажимать для переключения карточек - D-pad больше не переключает вкладки только RB и LB - Кнопка добавления игры больше не фокусируется - Диалог добавления игры теперь открывается только в библиотеке +- Аргумент --fullscreen для открытия приложения в режиме полноэкранного отображения +- Оверлей на кнопку Xbox / PS для закрытия приложения, выключения, перезагрузки и ухода в сон ### Fixed - Обработка несуществующей темы с возвратом к “standart” From 647394ca92d3acb415f9e1da28baa586f3125159 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 09:34:24 +0500 Subject: [PATCH 16/47] chore(localization): update Signed-off-by: Boris Yumankulov --- 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 | 60 ++++++++--------- .../locales/es_ES/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/es_ES/LC_MESSAGES/messages.po | 60 ++++++++--------- portprotonqt/locales/messages.pot | 60 ++++++++--------- .../locales/ru_RU/LC_MESSAGES/messages.mo | Bin 13281 -> 12956 bytes .../locales/ru_RU/LC_MESSAGES/messages.po | 62 ++++++++---------- portprotonqt/system_overlay.py | 15 +---- 10 files changed, 120 insertions(+), 149 deletions(-) diff --git a/documentation/localization_guide/README.md b/documentation/localization_guide/README.md index 84fdcc5..d4b50af 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 154 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 154 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 of 154 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 153 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 153 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 of 153 | --- diff --git a/documentation/localization_guide/README.ru.md b/documentation/localization_guide/README.ru.md index eb4ccca..1c4c7e0 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 из 154 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 154 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 из 154 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 153 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 153 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 из 153 | --- diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo index 0f3b2716b55ae08460c7354be3f460f4ddd4a5dc..71e89d71fd38dbc3f0b0bbe95dcf91bd71e501ba 100644 GIT binary patch delta 17 YcmX@ie3*H{L^cZr14}F8jnj1)0W`%0R{#J2 delta 17 YcmX@ie3*H{L^d-8BLgdgjnj1)0W@<3O8@`> diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po index 882d392..4145e5e 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-06 20:01+0500\n" +"POT-Creation-Date: 2025-06-08 09:31+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: de_DE\n" @@ -362,21 +362,6 @@ msgstr "" msgid "Auto Fullscreen on Gamepad connected:" 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 "" @@ -392,22 +377,6 @@ 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 "" @@ -505,6 +474,33 @@ msgstr "" msgid "Launching" msgstr "" +msgid "System Overlay" +msgstr "" + +msgid "Reboot" +msgstr "" + +msgid "Shutdown" +msgstr "" + +msgid "Suspend" +msgstr "" + +msgid "Exit Application" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Failed to reboot the system" +msgstr "" + +msgid "Failed to shutdown the system" +msgstr "" + +msgid "Failed to suspend the system" +msgstr "" + msgid "just now" msgstr "" diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo index 50770aafe487a049f1dcf75e6fbc437b043c3864..1c9c621316c433ce09621e87918dc6b8898ca4e6 100644 GIT binary patch delta 17 YcmX@ie3*H{L^cZr14}F8jnj1)0W`%0R{#J2 delta 17 YcmX@ie3*H{L^d-8BLgdgjnj1)0W@<3O8@`> diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po index b67ae5b..74cfe8c 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-06 20:01+0500\n" +"POT-Creation-Date: 2025-06-08 09:31+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es_ES\n" @@ -362,21 +362,6 @@ msgstr "" msgid "Auto Fullscreen on Gamepad connected:" 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 "" @@ -392,22 +377,6 @@ 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 "" @@ -505,6 +474,33 @@ msgstr "" msgid "Launching" msgstr "" +msgid "System Overlay" +msgstr "" + +msgid "Reboot" +msgstr "" + +msgid "Shutdown" +msgstr "" + +msgid "Suspend" +msgstr "" + +msgid "Exit Application" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Failed to reboot the system" +msgstr "" + +msgid "Failed to shutdown the system" +msgstr "" + +msgid "Failed to suspend the system" +msgstr "" + msgid "just now" msgstr "" diff --git a/portprotonqt/locales/messages.pot b/portprotonqt/locales/messages.pot index 46314b4..14160cf 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-06 20:01+0500\n" +"POT-Creation-Date: 2025-06-08 09:31+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -360,21 +360,6 @@ msgstr "" msgid "Auto Fullscreen on Gamepad connected:" 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 "" @@ -390,22 +375,6 @@ 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 "" @@ -503,6 +472,33 @@ msgstr "" msgid "Launching" msgstr "" +msgid "System Overlay" +msgstr "" + +msgid "Reboot" +msgstr "" + +msgid "Shutdown" +msgstr "" + +msgid "Suspend" +msgstr "" + +msgid "Exit Application" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Failed to reboot the system" +msgstr "" + +msgid "Failed to shutdown the system" +msgstr "" + +msgid "Failed to suspend the system" +msgstr "" + msgid "just now" msgstr "" diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo index d774613691595af35eabde04fe679ac19651fe3d..6ce2539ba3f46b89cc5f313d42c068e26e5fbdbb 100644 GIT binary patch delta 3126 zcmajgdrXye9LMqB0a5`4L6lUCN4y{*f|%wdGqcoLYD(U!<&9&K=s`GmD?0+27s#|n zans7x)M>Y=M-dd1Hq(u4K4-0^%b6|zXzJ{?(kX4dKj%DV{n28_=k@zN&+qd6eSg2_ z@%xwF@dhp>$80nF?ciS;|E|ZVcJHq-)|ft2o3I`JfC+dV(WAEyrHew_#7_Hw_f};ZRsLZM%E)u%{!=ZKElBL6wXl4gqK|vzB*dSqRdES+sp)1K+`Yi%aS|$^ z1<22oa8bY4U29Np$5vDZ-bH@q11_4d4wd?INRrJZ)Oa@n6qMRqZi8s@ss_oZ! z9ctiYREKUj1+%aKUqK7cq5A)fn)oKFUmW|QGn9;(n2ri~97gN?pXN50h1#a(4!ZY`;b{+t6w{gCyuk)_rNLgz!DiIy zzv14G=i#%|`=AcnDqMlJ_#r0p5vayvs6fWjs1m1Q6@HESPRwFZZM7f6smJ!rZ#Gd- zN_L|5dcPiE6VAXE)FI4Z;|AbxR4R*Hx8O+X4cHHFqRx=T*GM0vJT5vrlTl}34(hO% zVnC7AQqZ0qL`{4OAI1<4!&qKker608y`J-NGOou*@w{uQ6=^>m^?WlvgO~9s9LPKS zIIc#G+uW1WNvc@*-q1)=+rMCwUtY; zKkmXu@GNFyY@bL52KNa>QZtJiOL_1zK8P2Q7s0gUBcN2Lp)&IX&ci9Fi4LRA#&0+V z$J00$YfynT;?sB$M`ITI!;4`Sp~kNWP|)7j;#Ay?d3X~QNWZMe|BeMXkorp09)5&6 zL`_JN%voe{<|lj+Q|PR-RE9%w9TJ0S#)uD{geDq++T&+Y?Y5u}^-)Z~7F5bFpys=QS$hBD*)YzZ8I1dI z2JS(Pt_k*XX@}pS4&P}8!+*hwiBF&f=| zE9LA%?og(nsCb#T)D-xAC8nUfq{LV1_ZCtLk7WwNF<6snQapF1y{Ge);DIiwo;E|R zykWx!*iHKcjmzio0z8S?cp5V?GsBqi zSd1E9j=EoqORxb~;bvThAL049fKCO*#LZ$F<=nUl73f~ngug@OvKPnVpzjD0+dPR` z_$*fAKar2gWH1w(V$8)#RKF{+0+*o{(3QL&H)aEkB5w4d0{s#Bm_2+c&Mjn|MPR3rGh>xM>9riteaqZdXG+2x&q+V=~DMJOGi4*V^ zEW|M8<3`lhY{AQMJ6ia6RDhGd1$k#GHwU#fwWxj#s0D=c*ngGS;l?D~;Xil+Rl-+L zhvmPhQlCfkxi}8B;;T?AY({P2efR_1jOy?D9zdorhf$SCpb~#KpZY6+Mcq{T3M6?` zk6QWdsI7@&F>XU8Jb--62wxiiH{X{~Z^`SZ3cZ6m10NtClgp$^pd7VD3*$7XzNtq| zd>d*D+WZc8`OkY$6ZD}Hd>pl+A=E_AAjinOh)U>9|N4DovE~%2e-`_wex;~N#b?sc z1lOTX|0-1GLDZhL;dG3m65NiB_y=5ppQDBK)L-N8Lqk7cdwc}h|i%Mt!=iw+`fuA8CQ*9Y@Jub&Z z_*>uCQT?Xyj_Uc%xEy!mb@&C=;WcIKzdCN>9nl0&q6P%WO4p63=Y77%P=V9=_28J8 za#X(ss4ZKIGjKOvi7#L!=1)8GdM`p%pc(JNu4!@hoyJ?-;0-ak99fle8fxVY*o>=C zfexY0z-e5JjdZ>YH=`0eg7x?o*5YjTffvNYkX+3Ds7mg|1^9NHh5}#2JElFKk2m18 zSdH6I2_8doG@s%uoXpKUY(SlzJCNd;Fe=c)*o6C0XQ+@e%*W|SZl)Pkq4@nY8fZL& zEUh&P~X!C}c`KSq2BHL~vXrY5jWDr%6*ypS8)kiv(HSp8kNX8)N8l{wc>x_ zSUiaee9AXx4nKUibEBL_K34cHK$Z4K>VUUm0p5WM(1qutgQ~=%sPX%-0FR)azdXl1 zIPTs+&gw`sv^E$EMZ(tNNUQD4%G~7sB{LA18jjgftKM$6!>z&SJyvaZ?3-N=k8gHX zW)-uES zUUya}dyU=F9Sb&h*j6|av)Urv;a2zQy!JrxS=rS`+CyP;eWx9^TbV7Vs~}5b$4{! zlN|0RYqwf=hhnQ!@#)#w8HvUSUQzgFxxJ>-ZsEeRqtQszYVD4O!tG~IK(*a*MHdFf z&b4M&*Hk;xi_83`+gO~IUh->i)El(Ces5o5gEyM!@rJ!8y#B<`aoAlxIUI10mz)Y* z{)o5F8}bIdVR{W&-oft}mCDt5YRXP$qE+B-FC9p8vdbnqyUI>DSC?9DNqJj(yzkpF z6AxM5zSIOY#@p!)5bFRLCVCayJH+>JVk7w-{9m5uj!AB+w?k$w7dbTKs4$wl<2V-K9Wo;@iQ&j>pv%&`}U<{0=19v zWIwS+7\n" @@ -369,21 +369,6 @@ msgstr "Режим полноэкранного отображения прил msgid "Auto Fullscreen on Gamepad connected:" 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 "Сохранить настройки" @@ -399,22 +384,6 @@ 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 "Подтвердите удаление" @@ -514,6 +483,33 @@ msgstr "Невозможно запустить игру пока запущен msgid "Launching" msgstr "Идёт запуск" +msgid "System Overlay" +msgstr "Системный оверлей" + +msgid "Reboot" +msgstr "Перезагрузить" + +msgid "Shutdown" +msgstr "Выключить" + +msgid "Suspend" +msgstr "Перейти в ждущий режим" + +msgid "Exit Application" +msgstr "Выйти из приложения" + +msgid "Cancel" +msgstr "Отмена" + +msgid "Failed to reboot the system" +msgstr "Не удалось перезагрузить систему" + +msgid "Failed to shutdown the system" +msgstr "Не удалось завершить работу системы" + +msgid "Failed to suspend the system" +msgstr "Не удалось перейти в ждущий режим" + msgid "just now" msgstr "только что" diff --git a/portprotonqt/system_overlay.py b/portprotonqt/system_overlay.py index a64b45e..f62bb10 100644 --- a/portprotonqt/system_overlay.py +++ b/portprotonqt/system_overlay.py @@ -1,5 +1,5 @@ import subprocess -from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QLabel, QMessageBox +from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QMessageBox from PySide6.QtWidgets import QApplication from PySide6.QtCore import Qt from portprotonqt.logger import get_logger @@ -20,11 +20,6 @@ class SystemOverlay(QDialog): layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(10) - title = QLabel(_("System Actions")) - title.setStyleSheet(self.theme.TAB_TITLE_STYLE) - title.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(title) - # Reboot button reboot_button = QPushButton(_("Reboot")) #reboot_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) @@ -71,14 +66,6 @@ class SystemOverlay(QDialog): QMessageBox.warning(self, _("Error"), _("Failed to reboot the system")) self.accept() - def sleep(self): - try: - subprocess.run(["systemctl", "suspend-then-hibernate"], check=True) - except subprocess.CalledProcessError as e: - logger.error(f"Failed to sleep: {e}") - QMessageBox.warning(self, _("Error"), _("Failed to put the system to sleep")) - self.accept() - def shutdown(self): try: subprocess.run(["systemctl", "poweroff"], check=True) From 08f4a0215bd8ee295ef03ce1ae70f9d180f47818 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 15:02:03 +0500 Subject: [PATCH 17/47] feat: added ecodes.KEY_HOMEPAGE (PS button) for overlay open Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 4393236..1b4fa7a 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -44,7 +44,7 @@ BUTTONS = { 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR}, 'context_menu': {ecodes.BTN_START}, 'menu': {ecodes.BTN_SELECT}, - 'guide': {ecodes.BTN_MODE}, + 'guide': {ecodes.BTN_MODE, ecodes.KEY_HOMEPAGE}, } class InputManager(QObject): From 23bcae32d26e663bfaf640850c0156b0efa63f8d Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 19:36:44 +0500 Subject: [PATCH 18/47] chore(changelog): update with grammar and typo fixes Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 61 ++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 516b015..820b8bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,52 +8,52 @@ ### Added - Кнопки сброса настроек и очистки кэша - Бейдж PortProton -- Зависимость на `xdg-utils` +- Зависимость от `xdg-utils` - Интеграция статуса WeAntiCheatYet в карточку -- Стили в AddGameDialog -- Переключение полноэкранного режима через F11 или Select на геймпаде -- Выбор QCheckBox через Enter или кнопку A геймпада -- Закрытие диалога добавления игры через ESC или кнопку B геймпада +- Стили в AddGameDialog +- Переключение полноэкранного режима через F11 или кнопку Select на геймпаде +- Выбор QCheckBox через Enter или кнопку A на геймпаде +- Закрытие диалога добавления игры через ESC или кнопку B на геймпаде - Закрытие окна приложения по комбинации клавиш Ctrl+Q -- Сохранение и восстановление размера при рестарте +- Сохранение и восстановление размера окна при перезапуске - Переключатель полноэкранного режима приложения -- Пункт в контекстное меню “Открыть папку игры” -- Пункт в контекстное меню “Добавить в Steam” -- Пункт в контекстное меню "Удалить из Steam” -- Метод сортировки сначала избранное -- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено) -- Обработчики для QMenu и QComboBox на геймпаде +- Пункт в контекстном меню «Открыть папку игры» +- Пункт в контекстном меню «Добавить в Steam» +- Пункт в контекстном меню «Удалить из Steam» +- Метод сортировки «Сначала избранное» +- Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена) +- Обработчики для QMenu и QComboBox при управлении геймпадом +- Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме +- Оверлей на кнопку Xbox/PS для закрытия приложения, выключения, перезагрузки и перехода в спящий режим ### Changed - Обновлены все иконки -- Переименован `_get_steam_home` → `get_steam_home` -- Переименован `steam_game` → `game_source` -- Догика контекстного меню вынесена в `ContextMenuManager` +- Переименована функция `_get_steam_home` в `get_steam_home` +- Переименован `steam_game` в `game_source` +- Логика контекстного меню вынесена в `ContextMenuManager` - Бейдж Steam теперь открывает Steam Community - Изменена лицензия с MIT на GPL-3.0 для совместимости с кодом от legendary -- Оптимизирована генерация карточек для предотвращения лагов при поиске и изменения размера окна -- Бейджи с карточек так же теперь дублируются и на странице с деталями, а не только в библиотеке -- Установка ширины бейджа в две трети ширины карточки +- Оптимизирована генерация карточек для предотвращения задержек при поиске и изменении размера окна +- Бейджи с карточек теперь отображаются также на странице с деталями, а не только в библиотеке +- Установлена ширина бейджа в две трети ширины карточки - Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites` -- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку +- Карточки теперь фокусируются в направлении движения стрелок или D-pad: например, при нажатии D-pad вниз фокус переходит на карточку в следующей колонке, а не по порядку - Теперь D-pad можно зажимать для переключения карточек -- D-pad больше не переключает вкладки только RB и LB +- D-pad больше не переключает вкладки, только RB и LB - Кнопка добавления игры больше не фокусируется - Диалог добавления игры теперь открывается только в библиотеке -- Аргумент --fullscreen для открытия приложения в режиме полноэкранного отображения -- Оверлей на кнопку Xbox / PS для закрытия приложения, выключения, перезагрузки и ухода в сон ### Fixed -- Обработка несуществующей темы с возвратом к “standart” +- Обработка несуществующей темы с возвратом к «standard» - Открытие контекстного меню - Запуск при отсутствии exiftool - Переводы пунктов настроек -- Бесконечное обращение к get_portproton_location +- Бесконечное обращение к `get_portproton_location` - Ссылки на документацию в README -- traceback при загрузке placeholder при отсутствии обложек +- Traceback при загрузке placeholder при отсутствии обложек - Утечки памяти при загрузке обложек - Ошибки при подключении геймпада из-за работы в разных потоках -- Множественное открытие диалога добавления игры на геймпаде +- Многократное открытие диалога добавления игры при использовании геймпада - Перехват событий геймпада во время работы игры --- @@ -67,16 +67,15 @@ - Сборка AppImage ### Changed -- Удалён жёстко заданный ресайз окна -- Использован icoextract как python модуль +- Удалён жёстко заданный размер окна +- Использован `icoextract` как Python-модуль ### Fixed - Скрытие статус-бара - Чтение списка Steam-игр -- Подвисание GUI -- Краш при повреждённом Steam +- Зависание GUI +- Сбой при повреждённом Steam --- - > См. подробности по каждому коммиту в истории репозитория. From e1d7bca05e25a5d427d22fd6231d7d8f3ea446ad Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 19:54:04 +0500 Subject: [PATCH 19/47] chore(readme): update with grammar and typo fixes Signed-off-by: Boris Yumankulov --- README.md | 115 +++++++++++++++++++++++++++++------------------------- 1 file changed, 62 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 598d136..aa5f634 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,70 @@
- +

PortProtonQt

-

Современный, удобный графический интерфейс, написанный с использованием PySide6(Qt6) и предназначенный для упрощения управления и запуска игр на различных платформах, включая PortProton, Steam и Epic Games Store.

+

Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.

+ ## В планах - [X] Адаптировать структуру проекта для поддержки инструментов сборки -- [X] Добавить возможность управление с геймпада -- [ ] Добавить возможность управление с тачскрина -- [X] Добавить возможность управление с мыши и клавиатуры +- [X] Добавить возможность управления с геймпада +- [ ] Добавить возможность управления с тачскрина +- [X] Добавить возможность управления с мыши и клавиатуры - [X] Добавить систему тем [Документация](documentation/theme_guide) -- [X] Вынести все константы такие как уровень закругления карточек в темы (Частично вынесено) -- [X] Добавить метадату для тем (скришоты, описание, домащняя страница и автор) -- [ ] Продумать систему вкладок вместо той что есть сейчас -- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS -- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800) -- [ ] Переделать скриншоты для соответсвия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) -- [X] Брать описание и названия игр с базы данных Steam -- [X] Брать обложки для игр со SteamGridDB или CDN Steam -- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска -- [X] Улучшить функцию поиска SteamApi что бы исправить некорректное определение ID (Graven определается как ENGRAVEN или GRAVENFALL, Spore определается как SporeBound или Spore Valley) -- [ ] Убрать логи со SteamApi в релизной версии потому что логи замедляют код -- [X] Что-то придумать с ограничением SteamApi в 50 тысяч игр за один запрос (иногда туда не попадают нужные игры и остаются без обложки) -- [X] Избавится от любого вызова yad -- [X] Написать свою реализацию запрета ухода в сон, а не использовать ту что в PortProton (Оставим это [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0)) -- [X] Написать свою реализацию трея, а не использовать ту что в PortProton -- [X] Добавить в поиск экранную клавиатуру (Реализовавывать собственную клавиатуру слишком затратно, лучше положится на встроенную в DE клавиатуру malit в KDE, gjs-osk в GNOME,Squeekboard в phosh, стимовская в SteamOS и так далее) -- [X] Добавить сортировку карточек по различным критериям (сейчас есть: недавние, кол-во наиграного времени, избранное или по алфавиту) +- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено) +- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор) +- [ ] Продумать систему вкладок вместо текущей +- [ ] Добавить сессию Gamescope, аналогичную той, что используется в SteamOS +- [ ] Разработать адаптивный дизайн (за эталон берётся 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] Оптимизировать работу со Steam API для ускорения времени запуска +- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley) +- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода +- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки) +- [X] Избавиться от вызовов yad +- [X] Реализовать собственный механизм запрета ухода в спящий режим вместо использования механизма PortProton (оставлено для [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0)) +- [X] Реализовать собственный системный трей вместо использования трея PortProton +- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.) +- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту) - [X] Добавить индикацию запуска приложения -- [X] Достичь паритета функционала с Ingame -- [ ] Достичь паритета функционала с PortProton -- [X] Добавить возможность изменения названия, описания и обложки через файлы .local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover} -- [X] Добавить встроенное переопределение имени, описания и обложки, например по пути portprotonqt/custom_data [Документация](documentation/metadata_override/) -- [X] Добавить в карточку игры сведения о поддержке геймадов +- [X] Достигнуть паритета функциональности с Ingame +- [ ] Достигнуть паритета функциональности с PortProton +- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}` +- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/) +- [X] Добавить в карточку игры сведения о поддержке геймпада - [X] Добавить в карточки данные с ProtonDB -- [X] Добавить в карточки данные с Are We Anti-Cheat Yet? -- [X] Продублировать бейджы с карточки на страницу с деталями игрыы -- [X] Добавить парсинг ярлыков со Steam -- [X] Добавить парсинг ярлыков с EGS -- [ ] Избавится от бинарника legendary -- [ ] Добавить запуск и скачивание игр с EGS -- [ ] Добавить авторизацию в EGS через WebView, а не вручную -- [X] Брать описания для игр с EGS из их [api](https://store-content.ak.epicgames.com/api) -- [X] Брать slug через Graphql [запрос](https://launcher.store.epicgames.com/graphql) -- [X] Добавить на карточку бейдж того что игра со стима -- [X] Добавить поддержку Flatpak и Snap версии Steam -- [X] Выводить данные о самом недавнем пользователе Steam, а не первом попавшемся -- [X] Исправить склонения в детальном выводе времени, например не 3 часов назад, а 3 часа назад +- [X] Добавить в карточки данные с AreWeAntiCheatYet +- [X] Продублировать бейджи с карточки на страницу с деталями игры +- [X] Добавить парсинг ярлыков из Steam +- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки) +- [ ] Избавиться от бинарника legendary +- [ ] Добавить запуск и скачивание игр из EGS +- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода +- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api) +- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql) +- [X] Добавить на карточку бейдж, указывающий, что игра из Steam +- [X] Добавить поддержку версий Steam для Flatpak и Snap +- [X] Отображать данные о самом последнем пользователе Steam, а не первом попавшемся +- [X] Исправить склонения в детальном выводе времени, например, не «3 часов назад», а «3 часа назад» - [X] Добавить перевод через gettext [Документация](documentation/localization_guide) -- [X] Писать описание игр и прочие данные на языке системы -- [X] Добавить недокументированные параметры конфигурации в GUI (time detail_level, games sort_method, games display_filter) -- [X] Добавить систему избранного к карточкам -- [X] Заменить все print на logging -- [ ] Привести все логи к одному языку -- [X] Стилизовать все элементы без стилей(QMessageBox, QSlider, QDialog) -- [X] Убрать жёсткую привязку путей на стрелочки QComboBox в styles.py +- [X] Отображать описания игр и другие данные на языке системы +- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter) +- [X] Добавить систему избранного для карточек +- [X] Заменить все `print` на `logging` +- [ ] Привести все логи к единому языку +- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog) +- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py` - [X] Исправить частичное применение тем на лету -- [X] Исправить наложение подписей скриншотов при первом перелистывание в полноэкранном режиме -- [ ] Добавить GOG (?) -- [ ] Определится уже наконец с названием (PortProtonQt или PortProtonQT) +- [X] Исправить наложение подписей скриншотов при первом перелистывании в полноэкранном режиме +- [ ] Добавить поддержку GOG (?) +- [ ] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант) +- [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?) +- [ ] Добавить виброотдачу на геймпаде при запуске игры (?) -### Установка (debug) +### Установка (devel) ```sh uv python install 3.10 @@ -71,6 +74,12 @@ source .venv/bin/activate Запуск производится по команде portprotonqt +### Установка (release) + +Выберите подходящий пакет для вашей системы или AppImage. + +Запуск производится по команде portprotonqt или по ярлыку в меню + ### Разработка В проект встроен линтер (ruff), статический анализатор (pyright) и проверка lock файла, если эти проверки не пройдут PR не будет принят, поэтому перед коммитом введите такую команду @@ -90,9 +99,9 @@ pre-commit run --all-files ## Авторы -* [Boria138](https://github.com/Boria138) - Программист +* [Boria138](https://git.linux-gaming.ru/Boria138) - Программист * [BlackSnaker](https://github.com/BlackSnaker) - Дизайнер - программист -* [Mikhail Tergoev(Castro-Fidel)](https://github.com/Castro-Fidel) - Автор оригинального проекта PortProton +* [Mikhail Tergoev(Castro-Fidel)](https://git.linux-gaming.ru/CastroFidel) - Автор оригинального проекта PortProton > [!WARNING] > Проект находится на стадии WIP (work in progress) корректная работоспособность не гарантирована From b965b23a5069a5fc95ad3f54e5db94506826b2b8 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 22:53:16 +0500 Subject: [PATCH 20/47] feat: add toggle favorite actions to context menu Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 33 +++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index a524c05..2918d1e 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -6,7 +6,7 @@ import subprocess from PySide6.QtWidgets import QMessageBox, QDialog, QMenu from PySide6.QtCore import QUrl, QPoint from PySide6.QtGui import QDesktopServices -from portprotonqt.config_utils import parse_desktop_entry +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 @@ -41,6 +41,17 @@ class ContextMenuManager: """ menu = QMenu(self.parent) + + favorites = read_favorites() + is_favorite = game_card.name in favorites + + if is_favorite: + favorite_action = menu.addAction(_("Remove from Favorites")) + favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, False)) + else: + favorite_action = menu.addAction(_("Add to Favorites")) + favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True)) + 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") @@ -80,6 +91,26 @@ class ContextMenuManager: menu.exec(game_card.mapToGlobal(pos)) + def toggle_favorite(self, game_card, add: bool): + """ + Toggle the favorite status of a game and update its icon. + + Args: + game_card: The GameCard instance to toggle. + add: True to add to favorites, False to remove. + """ + favorites = read_favorites() + 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) + 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) + save_favorites(favorites) + game_card.update_favorite_icon() + def _check_portproton(self): """Check if PortProton is available.""" if self.portproton_location is None: From 55c32457d6b6770e83a7a4b93a2d12a5bb85a2b7 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 22:56:19 +0500 Subject: [PATCH 21/47] chore(localization): update Signed-off-by: Boris Yumankulov --- 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 | 16 +++++++++++++++- .../locales/es_ES/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/es_ES/LC_MESSAGES/messages.po | 16 +++++++++++++++- portprotonqt/locales/messages.pot | 16 +++++++++++++++- .../locales/ru_RU/LC_MESSAGES/messages.mo | Bin 12956 -> 13291 bytes .../locales/ru_RU/LC_MESSAGES/messages.po | 18 ++++++++++++++++-- 9 files changed, 67 insertions(+), 11 deletions(-) diff --git a/documentation/localization_guide/README.md b/documentation/localization_guide/README.md index d4b50af..02ac61e 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 153 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 153 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 of 153 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 157 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 157 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 157 of 157 | --- diff --git a/documentation/localization_guide/README.ru.md b/documentation/localization_guide/README.ru.md index 1c4c7e0..8a28b3d 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 из 153 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 153 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 из 153 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 157 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 157 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 157 из 157 | --- diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo index 71e89d71fd38dbc3f0b0bbe95dcf91bd71e501ba..f5b88244345405ea4ae491cf5d3e29fe735cd1d3 100644 GIT binary patch delta 16 XcmX@ie3*H{WL6_1D^t^rGqf22Fp~u` delta 16 XcmX@ie3*H{WL5)9D`UfrGqf22Fqs80 diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po index 4145e5e..16abdbe 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-08 09:31+0500\n" +"POT-Creation-Date: 2025-06-08 22:55+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: de_DE\n" @@ -20,6 +20,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" +msgid "Remove from Favorites" +msgstr "" + +msgid "Add to Favorites" +msgstr "" + msgid "Remove from Desktop" msgstr "" @@ -47,6 +53,14 @@ msgstr "" msgid "Add to Steam" msgstr "" +#, python-brace-format +msgid "Added '{0}' to favorites" +msgstr "" + +#, python-brace-format +msgid "Removed '{0}' from favorites" +msgstr "" + msgid "Error" msgstr "" diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo index 1c9c621316c433ce09621e87918dc6b8898ca4e6..7407abf551b316c6c34a83eed54db35340dd0aef 100644 GIT binary patch delta 16 XcmX@ie3*H{WL6_1D^t^rGqf22Fp~u` delta 16 XcmX@ie3*H{WL5)9D`UfrGqf22Fqs80 diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po index 74cfe8c..482a778 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-08 09:31+0500\n" +"POT-Creation-Date: 2025-06-08 22:55+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es_ES\n" @@ -20,6 +20,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" +msgid "Remove from Favorites" +msgstr "" + +msgid "Add to Favorites" +msgstr "" + msgid "Remove from Desktop" msgstr "" @@ -47,6 +53,14 @@ msgstr "" msgid "Add to Steam" msgstr "" +#, python-brace-format +msgid "Added '{0}' to favorites" +msgstr "" + +#, python-brace-format +msgid "Removed '{0}' from favorites" +msgstr "" + msgid "Error" msgstr "" diff --git a/portprotonqt/locales/messages.pot b/portprotonqt/locales/messages.pot index 14160cf..85236e1 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-08 09:31+0500\n" +"POT-Creation-Date: 2025-06-08 22:55+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,6 +18,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" +msgid "Remove from Favorites" +msgstr "" + +msgid "Add to Favorites" +msgstr "" + msgid "Remove from Desktop" msgstr "" @@ -45,6 +51,14 @@ msgstr "" msgid "Add to Steam" msgstr "" +#, python-brace-format +msgid "Added '{0}' to favorites" +msgstr "" + +#, python-brace-format +msgid "Removed '{0}' from favorites" +msgstr "" + msgid "Error" msgstr "" diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo index 6ce2539ba3f46b89cc5f313d42c068e26e5fbdbb..e5a07da7a080f2ca6188f5067f90240f95cc0bdf 100644 GIT binary patch delta 2849 zcmZYAeN5F=9LMp4JmbxT3-aW{PZ6%7z!e8dLYtY^eOxzaLFX%LGTV@ta}q%En< z22?I}ZVsuoLM}3_RI(LYZRusse=3qKTQ)VDrE4nG`@=nzQ@H$I=lp)>;d{R4{4V>e zFK_bw7#G%V_&LbmWd2;?s{Q|SC)${Fsv{VMX)(r3z_~aP7vXqZi7u?fB&bZkn^dL=5 zCAzT+6>u}E{p&c3`OP~Nw4%@LgR?k^`VeZsKar2Q&1GytdXG)WMRf<2v`kDvFFII) zQ?V8`aSP7C{WuN#CG(pL6w>f-oQ`QklYvW7_iIrd_Tn5oiXQv{Q!(tpVBJAwu-Mk? zP~)}Z6zoH7$$3;puA`4JOa$R+paj(3%|lJN!umX_;~rEdI+5)&$4~+FV?18Q1T?X1 z2TVe3-E7RmY;>>+HU6$x@~=V%H(0#+3bkkFY{Ltv6=^i3IvsUZa!@H>fcaR2T4^I{ zA#bC$(2rXA*QoZtSg#^On?K@w!4-#-4@Ec?H9-X`^-m#-GTTsF@)joIaa2G71mi)IhDMRCl5R zJc(M-8Prw{qb3+ZW!6Pw1(<@W&p?u3d|nFbxWqnKjY`>br~$U4R zKn>i5YTt+DIDk)J68qp_Evo;UsEONJlL5HdfJ$M2Y;YAF^>-PCg)EDX)EAXkCnvdjFSE&_Jd3!5SP0Vee6gGcDN|K4uA*EPNVu z7FtlL_9Gv2mWvmE#R^Q}%Rq;*9-FZRKg2L@wqO^IXMVGYQFh>R+=<_yzDP>h2c3l) zRB9Vhnc9iU&|cJ8IB4(taSio;BzBX?w@)_apfbD8x*LnAe~i9a6s}OvAxvPo6iqtn zP(6w|8>>-o!zNT@_oD*)9XU>BV!AKb z(UTtB+l@Gv2W?o0r*RIBB4e6NHeQ)2#TRisX5t|7rkOjaRL62rW(sfvE<*+Q0qSgA z!X;Rw{O3^Ej*6@sSK=Tp#;LP|@A)&R37SxQ--4@fAL`IuK?O2}3$dDFoQE%qcAWbhmhC@h9W)A65y9(6An{9m`Dy1KxCi)Ux zcn&#_X4pE4$%YdPwxu?A^lb|4?q#YKm1$kzWt1(Lzh=0!1Or~zwH z0qsU@RUbO|Ju0v}sLZ%Yw=!LTUcLWMQqaVEPVf$|D$c0w$xjU>0iPA|&f( zEplGXtN1=1!451V{}jy)jKYlE;9<-`jnjZ*|Nmd3pwroj%EZqYfg`AfVe^9~V<`1( zjKf^(Qgl(Tw9l*2O}z%S(ybVY2T+;lK=nU?6WkQeQc#1R=C^(tT^zma*{1r2n#Ssv zT5m=Sy<0s_W4*KS-)BAUnEaT#nij@qq}0_n{nvJ5Lw(&{t=Gp7j0?}r zeJC%lH8U|SSn64o*c}r4ZlEvFJ-97!G;osNQ-R*rt4RepZIrsX=MQ`q=;i+LK(7rV v;CHCq+owFWqNh3ejN5(ha6N%jt=_DHoWp@*40qpPjCB8S^y%4@l^6aGI-hyg delta 2575 zcmYM#e@sSj@&+Yt?c@vp<|VY`I0lwt9cK&lbC$*Llu)&hvbq@Ao;! ztIwX;;Ee>Q9yR`s@oyIYu1!_z|G)Eovpkx;n1q*b8eYR>yoo887>GR|LR~M#Wmtue z;XYiBqnLh4I|)!wek2F#d*jVjP_{ zaW>{)8D?R?%Ga&6E zyaQ`cCEbk5$N{9Qoj{Fq2EE%k8RSG0jye^-9tO;>W1c zeTfy=k71lZjh{Iq=0aqx_V^6yPnm2J7s9v$wW260^{$4$RqX^YQe`*0iF$#e@$?O3#!SVNU)YlzEqNYOv74KKpT;ty~IJ! zz2R&@y&XqT8F&x**#{gnVHYa(!$^{C1U256$B9z=x9i{|uj&v)ZN?nrmKCA`t3s`$ z7B%2{R4TWi0%~^ceaKoYg6iMyu6Ls{G=O^E`<)X-egie(Kd2S>D6Aq(LJb^5J&=Jb zu@LLiIuV6OW^w3s4vBp&;gC7AoLn=+paO?>aQ1YPJ#M@C8(0TTp@QLG-{jgI8jQDqiTIxH?SAi z;1FsP=1{m|T!2dDCTA;F(LRR-IF8ywVZKKCAeC^?-dTa#3+qvveH(g;EXs*$)`6P1 zAMeBOuoC^ey!>nl2fdz8;|e^058zMEv~a9{1M2=^d=y9VVJzjHeGr>bWC zBF~GZrjZLaUE*nCf9C5rL-G0(J(3lBgp=;t4=@pO{blY+DnU3 zoAn9g1+wkP&)(*slJubZU5>Rq^Kq38Zl$P!YLRl!|xj$|4i~>PTH+W~UdpHxT(Il-~Jo=&QK+lF9|;rIEq3+}K&?^|X#Dk-_xb S&diK(DDq5kZfAFKdHjEGQv?VA diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po index d555b37..ca200aa 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-08 09:31+0500\n" -"PO-Revision-Date: 2025-06-08 09:31+0500\n" +"POT-Creation-Date: 2025-06-08 22:55+0500\n" +"PO-Revision-Date: 2025-06-08 22:55+0500\n" "Last-Translator: \n" "Language: ru_RU\n" "Language-Team: ru_RU \n" @@ -21,6 +21,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.17.0\n" +msgid "Remove from Favorites" +msgstr "Удалить из Избранного" + +msgid "Add to Favorites" +msgstr "Добавить в Избранное" + msgid "Remove from Desktop" msgstr "Удалить с рабочего стола" @@ -48,6 +54,14 @@ msgstr "Удалить из Steam" msgid "Add to Steam" msgstr "Добавить в Steam" +#, python-brace-format +msgid "Added '{0}' to favorites" +msgstr "Добавление '{0}' в избранное" + +#, python-brace-format +msgid "Removed '{0}' from favorites" +msgstr "Удаление '{0}' из избранного" + msgid "Error" msgstr "Ошибка" From 61115411e7eff01b5c6835466a7d2457a03cd3b5 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 22:57:43 +0500 Subject: [PATCH 22/47] chore(changelog): update Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 820b8bf..cd1231a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ - Сохранение и восстановление размера окна при перезапуске - Переключатель полноэкранного режима приложения - Пункт в контекстном меню «Открыть папку игры» -- Пункт в контекстном меню «Добавить в Steam» -- Пункт в контекстном меню «Удалить из Steam» +- Пункты в контекстном меню «Добавить в Steam» и «Удалить из Steam» +- Пункты в контекстном меню «Добавить в Избранное» и «Удалить из Избранного» для переключения статуса избранного через геймпад - Метод сортировки «Сначала избранное» - Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена) - Обработчики для QMenu и QComboBox при управлении геймпадом From 68a52d69801c50a8eb2b0250efac750253bd31cd Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 23:06:14 +0500 Subject: [PATCH 23/47] feat: optimize game grid update for search performance Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 59 ++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index e5d1246..66c1f70 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -102,6 +102,7 @@ class MainWindow(QMainWindow): self.setMinimumSize(800, 600) self.games = [] + self.filtered_games = self.games self.game_processes = [] self.target_exe = None self.current_running_button = None @@ -543,10 +544,10 @@ class MainWindow(QMainWindow): """Filters games based on search text and updates the grid.""" text = self.searchEdit.text().strip().lower() if text == "": - self.updateGameGrid() # Use self.games directly + self.filtered_games = self.games else: - filtered = [game for game in self.games if text in game[0].lower()] - self.updateGameGrid(filtered) + self.filtered_games = [game for game in self.games if text in game[0].lower()] + self.updateGameGrid(self.filtered_games) def createInstalledTab(self): self.gamesLibraryWidget = QWidget() @@ -620,22 +621,37 @@ class MainWindow(QMainWindow): if games_list is None: games_list = self.games if not games_list: - self.clearLayout(self.gamesListLayout) + # Скрываем все карточки, если список пуст + for card in self.game_card_cache.values(): + card.hide() self.game_card_cache.clear() self.pending_images.clear() + self.gamesListWidget.updateGeometry() return - # Create a set of game names for quick lookup + # Создаем словарь текущих игр для быстрого поиска current_games = {game_data[0]: game_data for game_data in games_list} - # Check if the grid is already up-to-date - if set(current_games.keys()) == set(self.game_card_cache.keys()) and self.card_width == getattr(self, '_last_card_width', None): - return # No changes needed, skip update + # Проверяем, изменился ли список игр или размер карточек + current_game_names = set(current_games.keys()) + cached_game_names = set(self.game_card_cache.keys()) + card_width_changed = self.card_width != getattr(self, '_last_card_width', None) - # Track if layout has changed to decide if geometry update is needed + if current_game_names == cached_game_names and not card_width_changed: + # Список игр и размер карточек не изменились, обновляем только видимость + search_text = self.searchEdit.text().strip().lower() + for game_name, card in self.game_card_cache.items(): + card.setVisible(search_text in game_name.lower() or not search_text) + self.loadVisibleImages() + return + + # Обновляем размер карточек, если он изменился + if card_width_changed: + for card in self.game_card_cache.values(): + card.setFixedWidth(self.card_width + 20) # Учитываем extra_margin в GameCard + + # Удаляем карточки, которых больше нет в списке layout_changed = False - - # Remove cards for games no longer in the list for card_key in list(self.game_card_cache.keys()): if card_key not in current_games: card = self.game_card_cache.pop(card_key) @@ -645,11 +661,14 @@ class MainWindow(QMainWindow): del self.pending_images[card_key] layout_changed = True - # Add or update cards for current games + # Добавляем новые карточки и обновляем существующие for game_data in games_list: game_name = game_data[0] + search_text = self.searchEdit.text().strip().lower() + should_be_visible = search_text in game_name.lower() or not search_text + if game_name not in self.game_card_cache: - # Create new card + # Создаем новую карточку card = GameCard( *game_data, select_callback=self.openGameDetailPage, @@ -657,7 +676,7 @@ class MainWindow(QMainWindow): card_width=self.card_width, context_menu_manager=self.context_menu_manager ) - # Connect context menu signals + # Подключаем сигналы контекстного меню card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut) card.deleteGameRequested.connect(self.context_menu_manager.delete_game) card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu) @@ -670,18 +689,18 @@ class MainWindow(QMainWindow): self.game_card_cache[game_name] = card self.gamesListLayout.addWidget(card) layout_changed = True - elif self.card_width != getattr(self, '_last_card_width', None): - # Update size only if card_width has changed + else: + # Обновляем видимость существующей карточки card = self.game_card_cache[game_name] - card.setFixedWidth(self.card_width + 20) # Account for extra_margin in GameCard + card.setVisible(should_be_visible) - # Store the current card_width + # Сохраняем текущий card_width self._last_card_width = self.card_width - # Trigger lazy image loading for visible cards + # Загружаем изображения для видимых карточек self.loadVisibleImages() - # Update layout geometry only if the layout has changed + # Обновляем геометрию только при необходимости if layout_changed: self.gamesListWidget.updateGeometry() From b0ec4487ca25c41cdd906b73ffa394258f2b0a32 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 23:25:48 +0500 Subject: [PATCH 24/47] feat: add styling to QCheckBox and overlay buttons Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 3 ++- portprotonqt/system_overlay.py | 10 ++++----- portprotonqt/themes/standart/styles.py | 29 ++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 66c1f70..2153923 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -972,7 +972,7 @@ class MainWindow(QMainWindow): # 5. Fullscreen setting for application self.fullscreenCheckBox = QCheckBox(_("Launch Application in Fullscreen")) - #self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) + self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) self.fullscreenCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.fullscreenTitle = QLabel(_("Application Fullscreen Mode:")) self.fullscreenTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE) @@ -984,6 +984,7 @@ class MainWindow(QMainWindow): # 6. Automatic fullscreen on gamepad connection self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected")) self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) self.autoFullscreenGamepadTitle = QLabel(_("Auto Fullscreen on Gamepad connected:")) self.autoFullscreenGamepadTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE) self.autoFullscreenGamepadTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) diff --git a/portprotonqt/system_overlay.py b/portprotonqt/system_overlay.py index f62bb10..80b64f9 100644 --- a/portprotonqt/system_overlay.py +++ b/portprotonqt/system_overlay.py @@ -22,35 +22,35 @@ class SystemOverlay(QDialog): # Reboot button reboot_button = QPushButton(_("Reboot")) - #reboot_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + reboot_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) reboot_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) reboot_button.clicked.connect(self.reboot) layout.addWidget(reboot_button) # Shutdown button shutdown_button = QPushButton(_("Shutdown")) - #shutdown_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + shutdown_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) shutdown_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) shutdown_button.clicked.connect(self.shutdown) layout.addWidget(shutdown_button) # Suspend button suspend_button = QPushButton(_("Suspend")) - #suspend_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + suspend_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) suspend_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) suspend_button.clicked.connect(self.suspend) layout.addWidget(suspend_button) # Exit application button exit_button = QPushButton(_("Exit Application")) - #exit_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + exit_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) exit_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) exit_button.clicked.connect(self.exit_application) layout.addWidget(exit_button) # Cancel button cancel_button = QPushButton(_("Cancel")) - #cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE) + cancel_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) cancel_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) cancel_button.clicked.connect(self.reject) layout.addWidget(cancel_button) diff --git a/portprotonqt/themes/standart/styles.py b/portprotonqt/themes/standart/styles.py index f353ff8..1467e6b 100644 --- a/portprotonqt/themes/standart/styles.py +++ b/portprotonqt/themes/standart/styles.py @@ -90,6 +90,13 @@ SEARCH_EDIT_STYLE = """ } """ +SETTINGS_CHECKBOX_STYLE = """ + QCheckBox:focus { + border: 2px solid #409EFF; + background: #404554; + } +""" + # ОТКЛЮЧАЕМ РАМКУ У QScrollArea SCROLL_AREA_STYLE = """ QWidget { @@ -206,6 +213,28 @@ ACTION_BUTTON_STYLE = """ } """ +# СТИЛЬ КНОПОК ОВЕРЛЕЯ +OVERLAY_BUTTON_STYLE = """ + QPushButton { + background: #3f424d; + border: 1px solid rgba(255, 255, 255, 0.20); + border-radius: 10px; + color: #ffffff; + font-size: 16px; + font-family: 'Play'; + } + QPushButton:hover { + background: #282a33; + } + QPushButton:pressed { + background: #282a33; + } + QPushButton:focus { + border: 2px solid #409EFF; + background-color: #404554; + } +""" + # ТЕКСТОВЫЕ СТИЛИ: ЗАГОЛОВКИ И ОСНОВНОЙ КОНТЕНТ TAB_TITLE_STYLE = "font-family: 'Play'; font-size: 24px; color: #ffffff; background-color: none;" CONTENT_STYLE = """ From 6fa145ee130044332ec146d2e53c8ab34eb46525 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Sun, 8 Jun 2025 23:36:01 +0500 Subject: [PATCH 25/47] feat: add styling to Context Menu Signed-off-by: Boris Yumankulov --- portprotonqt/context_menu_manager.py | 1 + portprotonqt/themes/standart/styles.py | 34 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index 2918d1e..ad853c5 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -41,6 +41,7 @@ class ContextMenuManager: """ menu = QMenu(self.parent) + menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE) favorites = read_favorites() is_favorite = game_card.name in favorites diff --git a/portprotonqt/themes/standart/styles.py b/portprotonqt/themes/standart/styles.py index 1467e6b..d66468a 100644 --- a/portprotonqt/themes/standart/styles.py +++ b/portprotonqt/themes/standart/styles.py @@ -8,6 +8,40 @@ current_theme_name = read_theme_from_config() favoriteLabelSize = 48, 48 pixmapsScaledSize = 60, 60 +CONTEXT_MENU_STYLE = """ + QMenu { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 rgba(40, 40, 40, 0.95), + stop:1 rgba(25, 25, 25, 0.95)); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 12px; + color: #ffffff; + font-family: 'Play'; + font-size: 16px; + padding: 5px; + } + QMenu::item { + padding: 8px 20px; + background: transparent; + border-radius: 8px; + color: #ffffff; + } + QMenu::item:selected { + background: #282a33; + color: #09bec8; + } + QMenu::item:hover { + background: #282a33; + color: #09bec8; + } + QMenu::item:focus { + background: #409EFF; + color: #ffffff; + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 8px; + } +""" + # СТИЛЬ ШАПКИ ГЛАВНОГО ОКНА MAIN_WINDOW_HEADER_STYLE = """ QFrame { From 61680ed97f9b7ccb70d5580ccfe702577273c20c Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 9 Jun 2025 09:56:25 +0500 Subject: [PATCH 26/47] chore: update program name to PortProtonQt Signed-off-by: Boris Yumankulov --- README.md | 2 +- dev-scripts/l10n.py | 4 ++-- portprotonqt/cli.py | 2 +- portprotonqt/config_utils.py | 10 +++++----- portprotonqt/context_menu_manager.py | 6 +++--- portprotonqt/downloader.py | 2 +- portprotonqt/egs_api.py | 6 +++--- portprotonqt/image_utils.py | 3 +-- .../locales/de_DE/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/de_DE/LC_MESSAGES/messages.po | 6 +++--- .../locales/es_ES/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/es_ES/LC_MESSAGES/messages.po | 6 +++--- portprotonqt/locales/messages.pot | 8 ++++---- .../locales/ru_RU/LC_MESSAGES/messages.mo | Bin 13291 -> 13316 bytes .../locales/ru_RU/LC_MESSAGES/messages.po | 10 +++++----- portprotonqt/main_window.py | 16 ++++++++-------- portprotonqt/steam_api.py | 2 +- portprotonqt/theme_manager.py | 2 +- .../themes/standart-light/metainfo.ini | 2 +- portprotonqt/themes/standart/metainfo.ini | 2 +- portprotonqt/time_utils.py | 2 +- 21 files changed, 45 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index aa5f634..dacdf9e 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ - [X] Исправить частичное применение тем на лету - [X] Исправить наложение подписей скриншотов при первом перелистывании в полноэкранном режиме - [ ] Добавить поддержку GOG (?) -- [ ] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант) +- [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант) - [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?) - [ ] Добавить виброотдачу на геймпаде при запуске игры (?) diff --git a/dev-scripts/l10n.py b/dev-scripts/l10n.py index 4acbb5b..6a9ff5c 100755 --- a/dev-scripts/l10n.py +++ b/dev-scripts/l10n.py @@ -106,7 +106,7 @@ def compile_locales() -> None: def extract_strings() -> None: input_dir = (Path(__file__).parent.parent / "portprotonqt").resolve() CommandLineInterface().run([ - "pybabel", "extract", "--project=PortProtonQT", + "pybabel", "extract", "--project=PortProtonQt", f"--version={_get_version()}", "--strip-comment-tag", "--no-location", @@ -231,7 +231,7 @@ def main(args) -> int: return 0 if __name__ == "__main__": - parser = argparse.ArgumentParser(prog="l10n", description="Localization utility for PortProtonQT.") + parser = argparse.ArgumentParser(prog="l10n", description="Localization utility for PortProtonQt.") parser.add_argument("--create-new", nargs='+', type=str, default=False, help="Create .po for new locales") parser.add_argument("--update-all", action='store_true', help="Extract/update locales and update README coverage") parser.add_argument("--spellcheck", action='store_true', help="Run spellcheck on POT and PO files") diff --git a/portprotonqt/cli.py b/portprotonqt/cli.py index ed7d096..f781dfc 100644 --- a/portprotonqt/cli.py +++ b/portprotonqt/cli.py @@ -7,7 +7,7 @@ def parse_args(): """ Парсит аргументы командной строки. """ - parser = argparse.ArgumentParser(description="PortProtonQT CLI") + parser = argparse.ArgumentParser(description="PortProtonQt CLI") parser.add_argument( "--fullscreen", action="store_true", diff --git a/portprotonqt/config_utils.py b/portprotonqt/config_utils.py index 3c9ca86..cf40558 100644 --- a/portprotonqt/config_utils.py +++ b/portprotonqt/config_utils.py @@ -10,7 +10,7 @@ _portproton_location = None # Пути к конфигурационным файлам CONFIG_FILE = os.path.join( os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")), - "PortProtonQT.conf" + "PortProtonQt.conf" ) PORTPROTON_CONFIG_FILE = os.path.join( @@ -21,7 +21,7 @@ PORTPROTON_CONFIG_FILE = os.path.join( # Пути к папкам с темами xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) THEMES_DIRS = [ - os.path.join(xdg_data_home, "PortProtonQT", "themes"), + os.path.join(xdg_data_home, "PortProtonQt", "themes"), os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes") ] @@ -472,14 +472,14 @@ def reset_config(): def clear_cache(): """ - Очищает кэш PortProtonQT, удаляя папку кэша. + Очищает кэш PortProtonQt, удаляя папку кэша. """ xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")) - cache_dir = os.path.join(xdg_cache_home, "PortProtonQT") + cache_dir = os.path.join(xdg_cache_home, "PortProtonQt") if os.path.exists(cache_dir): try: shutil.rmtree(cache_dir) - logger.info("Кэш PortProtonQT удалён: %s", cache_dir) + logger.info("Кэш PortProtonQt удалён: %s", cache_dir) except Exception as e: logger.error("Ошибка при удалении кэша: %s", e) diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py index ad853c5..4e5772b 100644 --- a/portprotonqt/context_menu_manager.py +++ b/portprotonqt/context_menu_manager.py @@ -12,7 +12,7 @@ from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_s from portprotonqt.dialogs import AddGameDialog class ContextMenuManager: - """Manages context menu actions for game management in PortProtonQT.""" + """Manages context menu actions for game management in PortProtonQt.""" def __init__(self, parent, portproton_location, theme, load_games_callback, update_game_grid_callback): """ @@ -258,7 +258,7 @@ class ContextMenuManager: "XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share") ) - custom_folder = os.path.join(xdg_data_home, "PortProtonQT", "custom_data", exe_name) + custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name) if os.path.exists(custom_folder): try: shutil.rmtree(custom_folder) @@ -417,7 +417,7 @@ class ContextMenuManager: "XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share") ) - custom_folder = os.path.join(xdg_data_home, "PortProtonQT", "custom_data", exe_name) + custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name) os.makedirs(custom_folder, exist_ok=True) ext = os.path.splitext(new_cover_path)[1].lower() diff --git a/portprotonqt/downloader.py b/portprotonqt/downloader.py index 8118b39..0e8e610 100644 --- a/portprotonqt/downloader.py +++ b/portprotonqt/downloader.py @@ -303,7 +303,7 @@ class Downloader(QObject): local_path = os.path.join( os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), - "PortProtonQT", "legendary_cache", "legendary" + "PortProtonQt", "legendary_cache", "legendary" ) logger.info(f"Downloading legendary binary version {version} from {binary_url} to {local_path}") diff --git a/portprotonqt/egs_api.py b/portprotonqt/egs_api.py index a80a295..8f12c3f 100644 --- a/portprotonqt/egs_api.py +++ b/portprotonqt/egs_api.py @@ -22,7 +22,7 @@ def get_cache_dir() -> Path: "XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache") ) - cache_dir = Path(xdg_cache_home) / "PortProtonQT" + cache_dir = Path(xdg_cache_home) / "PortProtonQt" cache_dir.mkdir(parents=True, exist_ok=True) return cache_dir @@ -36,7 +36,7 @@ def get_egs_game_description_async( Asynchronously fetches the game description from the Epic Games Store API. Prioritizes GraphQL API with namespace for slug and description. Falls back to legacy API if GraphQL provides a slug but no description. - Caches results in ~/.cache/PortProtonQT/egs_app_{app_name}.json. + Caches results in ~/.cache/PortProtonQt/egs_app_{app_name}.json. Handles DNS resolution failures gracefully. """ cache_dir = get_cache_dir() @@ -423,7 +423,7 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu except Exception as e: logger.warning("Error processing metadata for %s: %s", app_name, str(e)) - image_folder = os.path.join(os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "PortProtonQT", "images") + 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): diff --git a/portprotonqt/image_utils.py b/portprotonqt/image_utils.py index 0c2dda5..922f23f 100644 --- a/portprotonqt/image_utils.py +++ b/portprotonqt/image_utils.py @@ -35,10 +35,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q y = (scaled.height() - height) // 2 cropped = scaled.copy(x, y, width, height) callback(cropped) - # Removed: pixmap = None (unnecessary, causes type error) xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")) - image_folder = os.path.join(xdg_cache_home, "PortProtonQT", "images") + image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images") os.makedirs(image_folder, exist_ok=True) if cover and cover.startswith("https://steamcdn-a.akamaihd.net/steam/apps/"): diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo index f5b88244345405ea4ae491cf5d3e29fe735cd1d3..c07c51151cedee2358bfb4d0b5823d9a26dd8127 100644 GIT binary patch delta 18 ZcmX@ie3*H{M0QIB14}DY, 2025. # @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-08 22:55+0500\n" +"POT-Creation-Date: 2025-06-09 09:53+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: de_DE\n" diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo index 7407abf551b316c6c34a83eed54db35340dd0aef..432121931b073d02a47c7f7d1dd1139e8c13e683 100644 GIT binary patch delta 18 ZcmX@ie3*H{M0QIB14}DY, 2025. # @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-08 22:55+0500\n" +"POT-Creation-Date: 2025-06-09 09:53+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es_ES\n" diff --git a/portprotonqt/locales/messages.pot b/portprotonqt/locales/messages.pot index 85236e1..9e57c3a 100644 --- a/portprotonqt/locales/messages.pot +++ b/portprotonqt/locales/messages.pot @@ -1,15 +1,15 @@ -# Translations template for PortProtonQT. +# Translations template for PortProtonQt. # Copyright (C) 2025 boria138 -# This file is distributed under the same license as the PortProtonQT +# This file is distributed under the same license as the PortProtonQt # project. # FIRST AUTHOR , 2025. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: PortProtonQT 0.1.1\n" +"Project-Id-Version: PortProtonQt 0.1.1\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-08 22:55+0500\n" +"POT-Creation-Date: 2025-06-09 09:53+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo index e5a07da7a080f2ca6188f5067f90240f95cc0bdf..e77f989564c594bcedbd8f64b5181f4e516b4f15 100644 GIT binary patch delta 1393 zcmXZbT}YEr7{KvILn~9u&uMBd%gT@0Qp_BtF1J+NoJ}(d%NB~-hoTd+o0gU&NOYAK zeHpZ%f*=%%AT+utX&?~@A%quEh(%W+2o?PweBtH%&N(~pInVo?XFocwbv}=d2rPz) zw3tLPMWifMghyIvEI5F>Fo@|GLLOP7v10gEktQs{G#tf24B}JFU~&v+aT7MN$uOSA ztN1lt0>WD9$PkIbVT{Ea7>#!^2A^O8zBJ}Tc#QF9BzCcG6DhzN+>I9vC-5NS_qYcm z_{z;#fJ{kwWMos&$&VK?r;E2x15F&<}71Nwv|xQL}_GOuOs2<~Luiyj=ua{O+X zy?t$c8|v@(u>pSt=+x1v$Pzh(W2hU?p>D8&X!HxJQC}QEou5Ztm&mDVJ?i_TsOv*W zjIuFXBpGv1->XEu+5qOO&*+rVS-?CrapCMORj8Tiz+N20TwKOPOd`*k>RcMlj2F*f zE9yaSQEMZNyjNi}?#0`vfqg(1=gTr3I};^2Y>Jmq54eeX_d)dHBw8_oC^V4$G{h`E z+=tguukaOW4TX>-$^w!M`HQD8i?7sL8o~pdFSqCrtjwVsSCE**L4I`M1=NFwjPWFD zO5dR#^aZ2wCsL2HYG@+Qsf>$JYsrIJtYfQ2&0N zs2h%;1~h?sRUgrezfl8ABF~zceAHBXQP=mO9{dpX%3q__fvnQeD$it9EEb|(fgSbW zW+eOKN9tAX;bWY^N7zaJ=}K%N1&bA^#aM&7&lqYT4^WHq4QeL-6_Wo*I>|+A3o{H0 zaUJv37>{*^r%+SfYy5s06BrMp9x#sU@dauorcvLY#{^tL{rwM0L0h~3+*EkfbWCfh z*;;BVF3PhOTdh$I9*ehqpv&LY(`|A1`rB>h<4(KR;`Ue^c5{u#=B~{<;i-2&8qigVHR}oQaV|S&NpA#L+gShh(H?&|Z4z z#fKsYwSuxH5`h##_z;9h^b~?nLXl7}L6Aw`!|qFPxu5g9``zE~-19pd>i*vK+_FA8 zyGG<}mPnz96xl?$q@J4-&!Gcjn1^xXk~wZ}{D$qAlOvLgLA-`Be1b_ljNt@s#{DcZ zfJbo<-{nbEm@6&$A}JWaG>o7H2Qd}zpcU_%&*ONC@f##}$tnv9q7e=Jc#d2 zEjtqPn^1qhiLLlCO3*~$*eP-tL#PW+p)T+P^$T2Nr7!lNKA%RN_s7)3?)rWZb$%Q* zv7fjd(~A@Dm7pH&WvsyHU4jDyGgywR$eL2d!E0q&@G73iGMvY)xPn^h3~pMP8oY>& zs0lts?TxQkk9E|)5^ta;_5z#PU*-u8@t|Ng=YkhdH|Rq>`xv(22x`+Uqb8Eija=mz z?!jKvBYc3`Lvf^dGJ_OBzF;SA;w!b6y0M!5AS@O;^#zIFEX3FIAy7>j})p09LTS3=nY11UH?yV2*#ICgPyA`NxPC zb-^CggoaU%>LuFo6KY~BsFkr&Z!L8V>ipBF8{bAf@`o7JiHii<<;l!SLmTQ5l%j52 zhg4n8ApI&s_z1^v6us1+P=4V?bQp{Vx zQS0%PyFG4qN~_n|=I;xH1HoQrgD>K*wHt23<8)U!-T%#N_Jt$P_K>eP+~bP`L$&tF I!tABof5bGQ&;S4c diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po index ca200aa..6e85830 100644 --- a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po +++ b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po @@ -1,6 +1,6 @@ -# Russian (Russia) translations for PortProtonQT. +# Russian (Russia) translations for PortProtonQt. # Copyright (C) 2025 boria138 -# This file is distributed under the same license as the PortProtonQT +# This file is distributed under the same license as the PortProtonQt # project. # FIRST AUTHOR , 2025. # @@ -9,9 +9,9 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-08 22:55+0500\n" -"PO-Revision-Date: 2025-06-08 22:55+0500\n" -"Last-Translator: \n" +"POT-Creation-Date: 2025-06-09 09:53+0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" "Language: ru_RU\n" "Language-Team: ru_RU \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 2153923..9660140 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -44,7 +44,7 @@ from datetime import datetime logger = get_logger(__name__) class MainWindow(QMainWindow): - """Main window of PortProtonQT.""" + """Main window of PortProtonQt.""" settings_saved = Signal() games_loaded = Signal(list) update_progress = Signal(int) # Signal to update progress bar @@ -73,10 +73,10 @@ class MainWindow(QMainWindow): self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed) read_time_config() - # Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQT/legendary + # Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQt/legendary_cache self.legendary_config_path = os.path.join( os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), - "PortProtonQT", "legendary_cache" + "PortProtonQt", "legendary_cache" ) os.makedirs(self.legendary_config_path, exist_ok=True) os.environ["LEGENDARY_CONFIG_PATH"] = self.legendary_config_path @@ -98,7 +98,7 @@ class MainWindow(QMainWindow): if not self.theme: self.theme = default_styles self.card_width = read_card_size() - self.setWindowTitle("PortProtonQT") + self.setWindowTitle("PortProtonQt") self.setMinimumSize(800, 600) self.games = [] @@ -384,7 +384,7 @@ class MainWindow(QMainWindow): builtin_custom_folder = os.path.join(repo_root, "portprotonqt", "custom_data") xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) - user_custom_folder = os.path.join(xdg_data_home, "PortProtonQT", "custom_data") + user_custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data") os.makedirs(user_custom_folder, exist_ok=True) builtin_cover = "" @@ -780,7 +780,7 @@ class MainWindow(QMainWindow): os.path.join(os.path.expanduser("~"), ".local", "share")) custom_folder = os.path.join( xdg_data_home, - "PortProtonQT", + "PortProtonQt", "custom_data", exe_name ) @@ -1228,7 +1228,7 @@ class MainWindow(QMainWindow): self.statusBar().showMessage(_("Theme '{0}' applied successfully").format(selected_theme), 3000) xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) - state_file = os.path.join(xdg_data_home, "PortProtonQT", "state.txt") + state_file = os.path.join(xdg_data_home, "PortProtonQt", "state.txt") os.makedirs(os.path.dirname(state_file), exist_ok=True) with open(state_file, "w", encoding="utf-8") as f: f.write("theme_tab\n") @@ -1251,7 +1251,7 @@ class MainWindow(QMainWindow): def restore_state(self): """Восстанавливает состояние приложения после перезапуска.""" xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")) - state_file = os.path.join(xdg_cache_home, "PortProtonQT", "state.txt") + state_file = os.path.join(xdg_cache_home, "PortProtonQt", "state.txt") if os.path.exists(state_file): with open(state_file, encoding="utf-8") as f: state = f.read().strip() diff --git a/portprotonqt/steam_api.py b/portprotonqt/steam_api.py index 514ba73..fed091e 100644 --- a/portprotonqt/steam_api.py +++ b/portprotonqt/steam_api.py @@ -49,7 +49,7 @@ def decode_text(text: str) -> str: def get_cache_dir(): """Возвращает путь к каталогу кэша, создаёт его при необходимости.""" xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")) - cache_dir = os.path.join(xdg_cache_home, "PortProtonQT") + cache_dir = os.path.join(xdg_cache_home, "PortProtonQt") os.makedirs(cache_dir, exist_ok=True) return cache_dir diff --git a/portprotonqt/theme_manager.py b/portprotonqt/theme_manager.py index 0686e7d..428b069 100644 --- a/portprotonqt/theme_manager.py +++ b/portprotonqt/theme_manager.py @@ -11,7 +11,7 @@ logger = get_logger(__name__) # Папка, где располагаются все дополнительные темы xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) THEMES_DIRS = [ - os.path.join(xdg_data_home, "PortProtonQT", "themes"), + os.path.join(xdg_data_home, "PortProtonQt", "themes"), os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes") ] diff --git a/portprotonqt/themes/standart-light/metainfo.ini b/portprotonqt/themes/standart-light/metainfo.ini index 1b5282d..e899572 100644 --- a/portprotonqt/themes/standart-light/metainfo.ini +++ b/portprotonqt/themes/standart-light/metainfo.ini @@ -1,5 +1,5 @@ [Metainfo] author = BlackSnaker author_link = -description = Стандартная тема PortProtonQT (светлый вариант) +description = Стандартная тема PortProtonQt (светлый вариант) name = Light diff --git a/portprotonqt/themes/standart/metainfo.ini b/portprotonqt/themes/standart/metainfo.ini index 3814979..78b9340 100644 --- a/portprotonqt/themes/standart/metainfo.ini +++ b/portprotonqt/themes/standart/metainfo.ini @@ -1,5 +1,5 @@ [Metainfo] author = Dervart author_link = -description = Стандартная тема PortProtonQT (тёмный вариант) +description = Стандартная тема PortProtonQt (тёмный вариант) name = Clean Dark diff --git a/portprotonqt/time_utils.py b/portprotonqt/time_utils.py index 71fc463..5829d89 100644 --- a/portprotonqt/time_utils.py +++ b/portprotonqt/time_utils.py @@ -10,7 +10,7 @@ logger = get_logger(__name__) def get_cache_file_path(): """Возвращает путь к файлу кеша portproton_last_launch.""" cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")) - return os.path.join(cache_home, "PortProtonQT", "last_launch") + return os.path.join(cache_home, "PortProtonQt", "last_launch") def save_last_launch(exe_name, launch_time): """ From 4a8033a0b7ce34383a71721cf593543418fce18a Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 9 Jun 2025 09:57:50 +0500 Subject: [PATCH 27/47] chore(changelog): update Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd1231a..85ec136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - D-pad больше не переключает вкладки, только RB и LB - Кнопка добавления игры больше не фокусируется - Диалог добавления игры теперь открывается только в библиотеке +- Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt ### Fixed - Обработка несуществующей темы с возвратом к «standard» From fce2ef2d0d8c0d197054048c17670e91576b18d1 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 9 Jun 2025 11:16:44 +0500 Subject: [PATCH 28/47] fix(metainfo): update screenshots url Signed-off-by: Boris Yumankulov --- .../metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml b/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml index 42af19e..a1eedb9 100644 --- a/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml +++ b/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml @@ -7,7 +7,11 @@ Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store Современный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store -

This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.

+

+ This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.

+

+ Это приложение предоставляет стильный и интуитивно понятный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет ваши игровые библиотеки в одном удобном хабе для простой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают целостный игровой опыт, устраняя необходимость в использовании нескольких лаунчеров. Уникальная интеграция с PortProton улучшает игровой процесс на Linux, позволяя с лёгкостью запускать Windows-игры с минимальными настройками. +

ru.linux_gaming.PortProtonQt.desktop @@ -41,7 +45,7 @@ Детали игры - https://git.linux-gaming.ru/Boria138/PortProtonQt/src/commit/9c4ad0b7bacac08849aff9036561de7b88a9bad2/portprotonqt/themes/standart/images/screenshots/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8.png + https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8.png Settings Настройки From 61f655c08f13ff2ca6d923bba9914d4477e2a3ed Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 9 Jun 2025 15:04:34 +0500 Subject: [PATCH 29/47] chore(readme): added all known issues Signed-off-by: Boris Yumankulov --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index dacdf9e..b4685d6 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,9 @@ - [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант) - [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?) - [ ] Добавить виброотдачу на геймпаде при запуске игры (?) +- [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63) +- [ ] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)) +- [ ] Скопировать логику управления с D-pad на стрелки с клавиатуры ### Установка (devel) From 9373aa1329c6333bbc7654ae51514ea75ce36829 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 9 Jun 2025 19:53:41 +0500 Subject: [PATCH 30/47] feat: added "Return to Desktop" button to overlay Signed-off-by: Boris Yumankulov --- portprotonqt/system_overlay.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/portprotonqt/system_overlay.py b/portprotonqt/system_overlay.py index 80b64f9..6359dce 100644 --- a/portprotonqt/system_overlay.py +++ b/portprotonqt/system_overlay.py @@ -3,6 +3,7 @@ from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QMessageBox from PySide6.QtWidgets import QApplication from PySide6.QtCore import Qt from portprotonqt.logger import get_logger +import os from portprotonqt.localization import _ logger = get_logger(__name__) @@ -48,6 +49,18 @@ class SystemOverlay(QDialog): exit_button.clicked.connect(self.exit_application) layout.addWidget(exit_button) + # Return to Desktop button + desktop_button = QPushButton(_("Return to Desktop")) + desktop_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) + desktop_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + desktop_button.clicked.connect(self.return_to_desktop) + script_path = "/usr/bin/portprotonqt-session-select" + script_exists = os.path.isfile(script_path) + desktop_button.setEnabled(script_exists) + if not script_exists: + desktop_button.setToolTip(_("portprotonqt-session-select file not found at /usr/bin/")) + layout.addWidget(desktop_button) + # Cancel button cancel_button = QPushButton(_("Cancel")) cancel_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) @@ -82,6 +95,15 @@ class SystemOverlay(QDialog): QMessageBox.warning(self, _("Error"), _("Failed to suspend the system")) self.accept() + def return_to_desktop(self): + try: + script_path = os.path.join(os.path.dirname(__file__), "portprotonqt-session-select") + subprocess.run([script_path, "desktop"], check=True) + except subprocess.CalledProcessError as e: + logger.error(f"Failed to return to desktop: {e}") + QMessageBox.warning(self, _("Error"), _("Failed to return to desktop")) + self.accept() + def exit_application(self): QApplication.quit() self.accept() From 149e80fa3382c0fc2e2bbe0f4cb018445bc60c6d Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Mon, 9 Jun 2025 21:31:52 +0500 Subject: [PATCH 31/47] feat: added Gamescope session Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 1 + README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ec136..b56ea83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Обработчики для QMenu и QComboBox при управлении геймпадом - Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме - Оверлей на кнопку Xbox/PS для закрытия приложения, выключения, перезагрузки и перехода в спящий режим +- [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) ### Changed - Обновлены все иконки diff --git a/README.md b/README.md index b4685d6..1355fb2 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено) - [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор) - [ ] Продумать систему вкладок вместо текущей -- [ ] Добавить сессию Gamescope, аналогичную той, что используется в SteamOS +- [X] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) - [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800) - [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) - [X] Получать описания и названия игр из базы данных Steam From b025e0bbcf692f0d42026fa0e9b204a37187ee75 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Tue, 10 Jun 2025 00:22:54 +0500 Subject: [PATCH 32/47] feat: rework QMessageBox handle and add focus style to it Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 20 +++++++++++++++++--- portprotonqt/themes/standart/styles.py | 4 ++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 1b4fa7a..51da736 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -3,7 +3,7 @@ import threading from typing import Protocol, cast from evdev import InputDevice, ecodes, list_devices import pyudev -from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView +from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer from PySide6.QtGui import QKeyEvent from portprotonqt.logger import get_logger @@ -284,8 +284,22 @@ class InputManager(QObject): self.dpad_timer.stop() # Stop timer when D-pad is released return - # Handle SystemOverlay or AddGameDialog navigation with D-pad - if isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: + # Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad + if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0: + if isinstance(active, QMessageBox): # Specific handling for QMessageBox + if not focused or not active.focusWidget(): + # If no widget is focused, focus the first focusable widget + focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively) + focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus] + if focusables: + focusables[0].setFocus(Qt.FocusReason.OtherFocusReason) + return + if value > 0: # Right + active.focusNextChild() + elif value < 0: # Left + active.focusPreviousChild() + return + elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: # Keep up/down for other dialogs if not focused or not active.focusWidget(): # If no widget is focused, focus the first focusable widget focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively) diff --git a/portprotonqt/themes/standart/styles.py b/portprotonqt/themes/standart/styles.py index d66468a..2ba0c9e 100644 --- a/portprotonqt/themes/standart/styles.py +++ b/portprotonqt/themes/standart/styles.py @@ -519,6 +519,10 @@ MESSAGE_BOX_STYLE = """ background: #09bec8; border-color: rgba(255, 255, 255, 0.3); } + QMessageBox QPushButton:focus { + border: 2px solid #409EFF; + background: #404554; + } """ # СТИЛИ ДЛЯ ВКЛАДКИ НАСТРОЕК PORTPROTON From cc8c22e9725bf7c864c95a429a8440d87900efde Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Tue, 10 Jun 2025 10:25:42 +0500 Subject: [PATCH 33/47] chore(localization): update Signed-off-by: Boris Yumankulov --- 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 | 11 ++++++++++- .../locales/es_ES/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/es_ES/LC_MESSAGES/messages.po | 11 ++++++++++- portprotonqt/locales/messages.pot | 11 ++++++++++- .../locales/ru_RU/LC_MESSAGES/messages.mo | Bin 13316 -> 13603 bytes .../locales/ru_RU/LC_MESSAGES/messages.po | 15 ++++++++++++--- 9 files changed, 48 insertions(+), 12 deletions(-) diff --git a/documentation/localization_guide/README.md b/documentation/localization_guide/README.md index 02ac61e..f4eea9e 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 157 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 157 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 157 of 157 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 160 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 160 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 160 of 160 | --- diff --git a/documentation/localization_guide/README.ru.md b/documentation/localization_guide/README.ru.md index 8a28b3d..eabcaa1 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 из 157 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 157 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 157 из 157 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 160 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 160 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 160 из 160 | --- diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo index c07c51151cedee2358bfb4d0b5823d9a26dd8127..99bd71e23efe2b7b8f78ec83ad24c916681b9a98 100644 GIT binary patch delta 19 acmX@ie3*H{1P((31w#WXBh!sDv>5?DAO)=e delta 19 acmX@ie3*H{1P%jB1p`YfQ{#;@v>5?Dz6HJj diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po index b4d17ec..c9e17da 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-09 09:53+0500\n" +"POT-Creation-Date: 2025-06-10 10:25+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: de_DE\n" @@ -503,6 +503,12 @@ msgstr "" msgid "Exit Application" msgstr "" +msgid "Return to Desktop" +msgstr "" + +msgid "portprotonqt-session-select file not found at /usr/bin/" +msgstr "" + msgid "Cancel" msgstr "" @@ -515,6 +521,9 @@ msgstr "" msgid "Failed to suspend the system" msgstr "" +msgid "Failed to return to desktop" +msgstr "" + msgid "just now" msgstr "" diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo index 432121931b073d02a47c7f7d1dd1139e8c13e683..23cb19531997d3eb614aa540c54d1360dc6df01f 100644 GIT binary patch delta 19 acmX@ie3*H{1P((31w#WXBh!sDv>5?DAO)=e delta 19 acmX@ie3*H{1P%jB1p`YfQ{#;@v>5?Dz6HJj diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po index 6f0217d..f327b4e 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-09 09:53+0500\n" +"POT-Creation-Date: 2025-06-10 10:25+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es_ES\n" @@ -503,6 +503,12 @@ msgstr "" msgid "Exit Application" msgstr "" +msgid "Return to Desktop" +msgstr "" + +msgid "portprotonqt-session-select file not found at /usr/bin/" +msgstr "" + msgid "Cancel" msgstr "" @@ -515,6 +521,9 @@ msgstr "" msgid "Failed to suspend the system" msgstr "" +msgid "Failed to return to desktop" +msgstr "" + msgid "just now" msgstr "" diff --git a/portprotonqt/locales/messages.pot b/portprotonqt/locales/messages.pot index 9e57c3a..836a96d 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-09 09:53+0500\n" +"POT-Creation-Date: 2025-06-10 10:25+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -501,6 +501,12 @@ msgstr "" msgid "Exit Application" msgstr "" +msgid "Return to Desktop" +msgstr "" + +msgid "portprotonqt-session-select file not found at /usr/bin/" +msgstr "" + msgid "Cancel" msgstr "" @@ -513,6 +519,9 @@ msgstr "" msgid "Failed to suspend the system" msgstr "" +msgid "Failed to return to desktop" +msgstr "" + msgid "just now" msgstr "" diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo index e77f989564c594bcedbd8f64b5181f4e516b4f15..9754ccf66dbf04c56325edd90418f01edc521f5e 100644 GIT binary patch delta 2924 zcmaLYe@xVM9LMpG0{%b{kWleQd{OWho*W1w%%~2_Uo=iIHFM2F&qCRqa))#N*cs&1 z>2xOchuRNIm&-JY$f4P+r8c#8*B_>BW>)JDom$$mwWU2@+-LPqi=E%c=hyf1d4JxY z&z@Yb@(cLNf48~*n5Zy5jbl2rTmZ+x;b`BYul2kWpuF2r5AjJr|u z9ku=EaT@h+Z~^A<(oAC_W(|ejG@Qmf{1Ua`9n^$_c_d;!_QG-2BJ4|jGNz)(wtLY{ zeI7D}*^HUkh6?x)YJ3-tVSRI!f_C(a?RbC#si!hZ3+5qBrU1JaL_Wr2>t0mS7T^$E zgbuF7hjAZj;S)FlFJKPdmaK2mSbZcGVlLLY)6}9S^r6mhJ!;`stRJCXyok!kUF0~8$%qHm z4EKk*t^u-qP9Spo0adomHcbU?u9#tVN~1 z6(`{q)J{*KHu3}NDDR_op2co8exkKJGtwQrd6Wk2cs44+WvB(-Kpo8?WV7Z1>S(Uu zAWUSt3TPP8WJaOhtFU@d-q5_SqwjCRgESc?gz<%4_flA${sEMwk zc619B$UW4~?xO-vAm5re6*WE!XJa1L;VN|S9P)m|+@PR^@1R~xBCon!sW=*kpd$BR z0(x!#9MoC*uqQr`3TzoFkPs@c^|rso_HRVpt?k|Yod2Hgf-$e70yu!0=v^$sPSiv< z@k|c_MP1geY-9MC%RF3o7j-vs`4Ld+r=$A)=*9@n!FJT;OyaSc^-V5?cW@7G#V2@M z5yr3uuV5=K;v9I_{Dit24^Syi=2u1;%SL5t6zXo2*!Jl-kNVR{?B)ngz|$B}ihrXZ zhq0|ka4L?$7SyFYj0`a+P?zjm)Fr!t`gZ(<3M|)&ALSHOKs8u~3$YyAkfG))%)?s_ z=U+o1lP_&0F1Egjdf^(Xe<;73UR;i|@MH8~A6D16S*Z6nqBih3>b;(i%`+9Oq6NID zJK)D!452RH8RTQ`@nBD81W^}ZWI6?%sw+J_xIHw67qyce z_!J&PVm3*nN#iD<7WUYB2$j)IsC8b$6xKIKC=93Jxb+fdQ~w=xsnWTsy0rzUpUf&` zEwd0AVz!{h9kcbXQGqCJd~wWJ)O=5(0$PMRs-39&XF4hHF&B9#!d{G0iiV?7UWIyb zF%JdMhC2HrsBza(KVa$Pvp2fE#$MRFBtEXcbtERxJ{c9*6l)EVa#L^ngIJkKLx_SF*o3|D zZB!=SLrri7HSRJhlMw36>+!S!aL1&3Sv^*SK(b1N+DJlJ*)~U8N zJFBv1*@|FOctuk%91N@syF&g@s391@M*rflv$Ubn?*xJrg3W;?jxX#KHiw!D>l*@v zyXFpRm{#O=irf{&r3LO%w>#NeXv+hQ-qcI@)y=XBKMolVYo`9He`7XF+# e>Hp%7o{V-nJldmO(GQuT#l&8Wc1Dj+Fnp5N0AT^yu#EV6mNhcuZgJSd(qoSQfc# z1g*rEO&HbIY*JCp!ntJ@n^DcJxs0}&IZfNl*etQWKkwOM*XMK2^PHFe`JeMV9^d>z zL-42Auy*6`2>%}BpD$ds|NricGfSa5f{~aUWfqOI@qT;+$74DAuoe^XDb#p--Sb1Z zlzI=Y!9TFnENG?lj^IW+&cws03D2Mgyn~Za1!-Pr~z*wKl_Kvy$R`kZ$d7rd#I$P;}rCxhh;bo z8&MOt;tcG>BWZ$lK4@n4*d$wc!I7NhPrqBa5H`rF;R-$Azes?nEu5 z1GR-GQ7iun)&4ix-qke^-Q;>Xc^1Zpq`br|>KF6_d0aVj@k@f?n4eyd@WW_$vl$6t`PwuybvS!hP3 z_5dnVou~}GgE|X+?*0I-rv3vGyZQP0WMK&^v(Gw@;zH_QVsI9PaF(e2{j zA+A#X=TLYH71`HVj#n{&3(`XG`EJw%ucG$83#;)Y`Z1g+6v%uo#B6mq7hgne;U}mw zG=L=0E+WaW;0T5F6lSnY9i}FH7~7Fx?L1cCFcOm$ksh_%f||I=)lVXCsGUVkbP0X< zJ8~ZFwlje=Po_Q(IZHvSqM*~d8TDywM%vmTdeM5++T{Z zSms=hO7%APd@sgPZ$_=O10(PwR3<)0^}m48aTKmlP=h-;z3EZi;l0-;>SeCJ-s+a Mn+87|{4~q{13B0l%K!iX diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po index 6e85830..1523e86 100644 --- a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po +++ b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po @@ -9,9 +9,9 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-06-09 09:53+0500\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" +"POT-Creation-Date: 2025-06-10 10:25+0500\n" +"PO-Revision-Date: 2025-06-10 10:24+0500\n" +"Last-Translator: \n" "Language: ru_RU\n" "Language-Team: ru_RU \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " @@ -512,6 +512,12 @@ msgstr "Перейти в ждущий режим" msgid "Exit Application" msgstr "Выйти из приложения" +msgid "Return to Desktop" +msgstr "Вернуться на рабочий стол" + +msgid "portprotonqt-session-select file not found at /usr/bin/" +msgstr "portprotonqt-session-select не найдет" + msgid "Cancel" msgstr "Отмена" @@ -524,6 +530,9 @@ msgstr "Не удалось завершить работу системы" msgid "Failed to suspend the system" msgstr "Не удалось перейти в ждущий режим" +msgid "Failed to return to desktop" +msgstr "Не удалось вернуться на рабочий стол" + msgid "just now" msgstr "только что" From b35a1b8dfeb012d1f1e37370224df8da3f360782 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 07:20:24 +0500 Subject: [PATCH 34/47] fix: prevent game card overlap in all\ display filter Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 9660140..cb67f3c 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -274,9 +274,10 @@ class MainWindow(QMainWindow): seen = set() games = [] for game in portproton_games + steam_games: - name = game[0] - if name not in seen: - seen.add(name) + # Уникальный ключ: имя + exec_line + key = (game[0], game[4]) + if key not in seen: + seen.add(key) games.append(game) self.games_loaded.emit(games) self._load_portproton_games_async( @@ -629,18 +630,19 @@ class MainWindow(QMainWindow): self.gamesListWidget.updateGeometry() return - # Создаем словарь текущих игр для быстрого поиска - current_games = {game_data[0]: game_data for game_data in games_list} + # Создаем словарь текущих игр с уникальным ключом (name + exec_line) + current_games = {(game_data[0], game_data[4]): game_data for game_data in games_list} # Проверяем, изменился ли список игр или размер карточек - current_game_names = set(current_games.keys()) - cached_game_names = set(self.game_card_cache.keys()) + current_game_keys = set(current_games.keys()) + cached_game_keys = set(self.game_card_cache.keys()) card_width_changed = self.card_width != getattr(self, '_last_card_width', None) - if current_game_names == cached_game_names and not card_width_changed: + if current_game_keys == cached_game_keys and not card_width_changed: # Список игр и размер карточек не изменились, обновляем только видимость search_text = self.searchEdit.text().strip().lower() - for game_name, card in self.game_card_cache.items(): + for game_key, card in self.game_card_cache.items(): + game_name = game_key[0] card.setVisible(search_text in game_name.lower() or not search_text) self.loadVisibleImages() return @@ -664,10 +666,11 @@ class MainWindow(QMainWindow): # Добавляем новые карточки и обновляем существующие for game_data in games_list: game_name = game_data[0] + game_key = (game_name, game_data[4]) search_text = self.searchEdit.text().strip().lower() should_be_visible = search_text in game_name.lower() or not search_text - if game_name not in self.game_card_cache: + if game_key not in self.game_card_cache: # Создаем новую карточку card = GameCard( *game_data, @@ -686,24 +689,26 @@ class MainWindow(QMainWindow): card.addToSteamRequested.connect(self.context_menu_manager.add_to_steam) card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam) card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder) - self.game_card_cache[game_name] = card + self.game_card_cache[game_key] = card self.gamesListLayout.addWidget(card) layout_changed = True else: # Обновляем видимость существующей карточки - card = self.game_card_cache[game_name] + card = self.game_card_cache[game_key] card.setVisible(should_be_visible) # Сохраняем текущий card_width self._last_card_width = self.card_width + # Принудительно обновляем макет + if layout_changed: + self.gamesListLayout.update() + self.gamesListWidget.updateGeometry() + self.gamesListWidget.update() + # Загружаем изображения для видимых карточек self.loadVisibleImages() - # Обновляем геометрию только при необходимости - if layout_changed: - self.gamesListWidget.updateGeometry() - def clearLayout(self, layout): """Удаляет все виджеты из layout.""" while layout.count(): From 4e057c204c5b9dc455055ddfe96c67149185b457 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 07:21:23 +0500 Subject: [PATCH 35/47] chore(readme): update todo Signed-off-by: Boris Yumankulov --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1355fb2..d954823 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ - [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?) - [ ] Добавить виброотдачу на геймпаде при запуске игры (?) - [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63) -- [ ] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)) +- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)) - [ ] Скопировать логику управления с D-pad на стрелки с клавиатуры ### Установка (devel) From 7e9a0be150eaf1fd04a9261800e6c44f7fb97bfd Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 07:35:40 +0500 Subject: [PATCH 36/47] fix: restore theme tab after theme change Signed-off-by: Boris Yumankulov --- portprotonqt/main_window.py | 38 +++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index cb67f3c..bc0dd50 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -1235,9 +1235,13 @@ class MainWindow(QMainWindow): os.path.join(os.path.expanduser("~"), ".local", "share")) state_file = os.path.join(xdg_data_home, "PortProtonQt", "state.txt") os.makedirs(os.path.dirname(state_file), exist_ok=True) - with open(state_file, "w", encoding="utf-8") as f: - f.write("theme_tab\n") - QTimer.singleShot(500, lambda: self.restart_application()) + try: + with open(state_file, "w", encoding="utf-8") as f: + f.write("theme_tab\n") + logger.info(f"State saved to {state_file}") + QTimer.singleShot(500, lambda: self.restart_application()) + except Exception as e: + logger.error(f"Failed to save state to {state_file}: {e}") else: self.statusBar().showMessage(_("Error applying theme '{0}'").format(selected_theme), 3000) @@ -1255,14 +1259,28 @@ class MainWindow(QMainWindow): def restore_state(self): """Восстанавливает состояние приложения после перезапуска.""" - xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")) - state_file = os.path.join(xdg_cache_home, "PortProtonQt", "state.txt") + xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) + state_file = os.path.join(xdg_data_home, "PortProtonQt", "state.txt") + logger.info(f"Checking for state file: {state_file}") if os.path.exists(state_file): - with open(state_file, encoding="utf-8") as f: - state = f.read().strip() - if state == "theme_tab": - self.switchTab(5) - os.remove(state_file) + try: + with open(state_file, encoding="utf-8") as f: + state = f.read().strip() + logger.info(f"State file contents: '{state}'") + if state == "theme_tab": + logger.info("Restoring to theme tab (index 5)") + if self.stackedWidget.count() > 5: + self.switchTab(5) + else: + logger.warning("Theme tab (index 5) not available yet") + else: + logger.warning(f"Unexpected state value: '{state}'") + os.remove(state_file) + logger.info(f"State file {state_file} removed") + except Exception as e: + logger.error(f"Failed to read or process state file {state_file}: {e}") + else: + logger.info(f"State file {state_file} does not exist") # ЛОГИКА ДЕТАЛЬНОЙ СТРАНИЦЫ ИГРЫ def getColorPalette_async(self, cover_path, num_colors=5, sample_step=10, callback=None): From b9d7fc23263a4ff00e5f9273eb67db04cbedc152 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 17:53:08 +0500 Subject: [PATCH 37/47] feat(input_manager): add dualshock 4 and dualsence mapping Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 51da736..9a55387 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -34,11 +34,13 @@ class MainWindowProtocol(Protocol): current_exec_line: str | None current_add_game_dialog: QDialog | None -# Mapping of actions to evdev button codes, includes PlayStation, Xbox, and Switch controllers +# Mapping of actions to evdev button codes, includes PlayStation, Xbox controllers +# https://github.com/torvalds/linux/blob/master/drivers/hid/hid-playstation.c +# https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c BUTTONS = { - 'confirm': {ecodes.BTN_A}, - 'back': {ecodes.BTN_B}, - 'add_game': {ecodes.BTN_Y}, + 'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, + 'back': {ecodes.BTN_B, ecodes.BTN_EAST}, + 'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, 'prev_tab': {ecodes.BTN_TL}, 'next_tab': {ecodes.BTN_TR}, 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR}, From 58c7541fa37ff73ffcb846fc48644287fe9c4b8c Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 18:16:08 +0500 Subject: [PATCH 38/47] feat(input_manager): rework gamepad buttons maping Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 36 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 9a55387..ce2def4 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -34,19 +34,18 @@ class MainWindowProtocol(Protocol): current_exec_line: str | None current_add_game_dialog: QDialog | None -# Mapping of actions to evdev button codes, includes PlayStation, Xbox controllers +# Mapping of actions to evdev button codes, includes Xbox and Playstation controllers # https://github.com/torvalds/linux/blob/master/drivers/hid/hid-playstation.c # https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c BUTTONS = { - 'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, - 'back': {ecodes.BTN_B, ecodes.BTN_EAST}, - 'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, - 'prev_tab': {ecodes.BTN_TL}, - 'next_tab': {ecodes.BTN_TR}, - 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR}, - 'context_menu': {ecodes.BTN_START}, - 'menu': {ecodes.BTN_SELECT}, - 'guide': {ecodes.BTN_MODE, ecodes.KEY_HOMEPAGE}, + 'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, # A / Cross + 'back': {ecodes.BTN_B, ecodes.BTN_EAST}, # B / Circle + 'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, # Y / Triangle + 'prev_tab': {ecodes.BTN_TL}, # LB / L1 + 'next_tab': {ecodes.BTN_TR}, # RB / R1 + 'context_menu': {ecodes.BTN_START}, # Start / Options + 'menu': {ecodes.BTN_SELECT}, # Select / Share + 'guide': {ecodes.BTN_MODE}, # Xbox / PS Home } class InputManager(QObject): @@ -149,19 +148,19 @@ class InputManager(QObject): # Handle QMenu (context menu) if isinstance(popup, QMenu): - if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: + if button_code in BUTTONS['confirm']: if popup.activeAction(): popup.activeAction().trigger() popup.close() return - elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']: + elif button_code in BUTTONS['back']: popup.close() return return # Handle QComboBox if isinstance(focused, QComboBox): - if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: + if button_code in BUTTONS['confirm']: focused.showPopup() return @@ -175,7 +174,7 @@ class InputManager(QObject): break parent = parent.parentWidget() - if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: + if button_code in BUTTONS['confirm']: idx = focused.currentIndex() if idx.isValid(): if combo: @@ -221,18 +220,17 @@ class InputManager(QObject): return # Game launch on detail page - if (button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None: + if (button_code in BUTTONS['confirm']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None: if self._parent.current_exec_line: self._parent.toggleGame(self._parent.current_exec_line, None) return # Standard navigation - if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']: + if button_code in BUTTONS['confirm']: self._parent.activateFocusedWidget() - elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']: + elif button_code in BUTTONS['back']: self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None)) elif button_code in BUTTONS['add_game']: - # Only open AddGameDialog if in library tab (index 0) if self._parent.stackedWidget.currentIndex() == 0: self._parent.openAddGameDialog() elif button_code in BUTTONS['prev_tab']: @@ -791,9 +789,7 @@ class InputManager(QObject): continue now = time.time() if event.type == ecodes.EV_KEY and event.value == 1: - # Обработка кнопки Select для переключения полноэкранного режима if event.code in BUTTONS['menu']: - # Переключаем полноэкранный режим self.toggle_fullscreen.emit(not self._is_fullscreen) else: self.button_pressed.emit(event.code) From 0587cf58ed4752fda01c14caa06ecc1084805f0d Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 18:20:48 +0500 Subject: [PATCH 39/47] feat(input_manager): open system overlay by Insert button Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index ce2def4..e76f158 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -491,6 +491,12 @@ class InputManager(QObject): focused = QApplication.focusWidget() popup = QApplication.activePopupWidget() + # Open system overlay with Insert + if key == Qt.Key.Key_Insert: + if not popup and not isinstance(QApplication.activeWindow(), QDialog): + self._parent.openSystemOverlay() + return True + # Close application with Ctrl+Q if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier: app.quit() From 2d7369d46c42c30e46082aef4a7cc951ead0b522 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 18:28:27 +0500 Subject: [PATCH 40/47] chore(changelog): update Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b56ea83..c8ccc34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,8 +24,9 @@ - Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена) - Обработчики для QMenu и QComboBox при управлении геймпадом - Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме -- Оверлей на кнопку Xbox/PS для закрытия приложения, выключения, перезагрузки и перехода в спящий режим +- Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или между сессиями - [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) +- Мапинги управления для Dualshock 4 и DualSense ### Changed - Обновлены все иконки From 30a4fc6ed783e6a1e839caf51288bfbd7f0e9735 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 19:08:58 +0500 Subject: [PATCH 41/47] feat(input-manager): add haptic feedback for game launch with gamepad Signed-off-by: Boris Yumankulov --- portprotonqt/input_manager.py | 61 ++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index e76f158..3640bfe 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -1,8 +1,8 @@ import time import threading from typing import Protocol, cast -from evdev import InputDevice, ecodes, list_devices -import pyudev +from evdev import InputDevice, InputEvent, ecodes, list_devices, ff +from pyudev import Context, Monitor, MonitorObserver, Device from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer from PySide6.QtGui import QKeyEvent @@ -52,8 +52,7 @@ class InputManager(QObject): """ Manages input from gamepads and keyboards for navigating the application interface. Supports gamepad hotplugging, button and axis events, and keyboard event filtering - for seamless UI interaction. Enables fullscreen mode when a gamepad is connected - and restores normal mode when disconnected. + for seamless UI interaction. """ # Signals for gamepad events button_pressed = Signal(int) # Signal for button presses @@ -83,6 +82,7 @@ class InputManager(QObject): self.gamepad_thread: threading.Thread | None = None self.running = True self._is_fullscreen = read_fullscreen_config() + self.rumble_effect_id: int | None = None # Store the rumble effect ID # Add variables for continuous D-pad movement self.dpad_timer = QTimer(self) @@ -126,6 +126,46 @@ class InputManager(QObject): except Exception as e: logger.error(f"Error in handle_fullscreen_slot: {e}", exc_info=True) + def trigger_rumble(self, duration_ms: int = 200, strong_magnitude: int = 0x8000, weak_magnitude: int = 0x8000) -> None: + """Trigger a rumble effect on the gamepad if supported.""" + if not self.gamepad: + return + try: + # Check if the gamepad supports force feedback + caps = self.gamepad.capabilities() + if ecodes.EV_FF not in caps or ecodes.FF_RUMBLE not in caps.get(ecodes.EV_FF, []): + logger.debug("Gamepad does not support force feedback or rumble") + return + + # Create a rumble effect + rumble = ff.Rumble(strong_magnitude=strong_magnitude, weak_magnitude=weak_magnitude) + effect = ff.Effect( + id=-1, # Let evdev assign an ID + type=ecodes.FF_RUMBLE, + direction=0, # Direction (not used for rumble) + replay=ff.Replay(length=duration_ms, delay=0), + u=ff.EffectType(ff_rumble_effect=rumble) + ) + + # Upload the effect + self.rumble_effect_id = self.gamepad.upload_effect(effect) + # Play the effect + event = InputEvent(0, 0, ecodes.EV_FF, self.rumble_effect_id, 1) + self.gamepad.write_event(event) + # Schedule effect erasure after duration + QTimer.singleShot(duration_ms, self.stop_rumble) + except Exception as e: + logger.error(f"Error triggering rumble: {e}", exc_info=True) + + def stop_rumble(self) -> None: + """Stop the rumble effect and clean up.""" + if self.gamepad and self.rumble_effect_id is not None: + try: + self.gamepad.erase_effect(self.rumble_effect_id) + self.rumble_effect_id = None + except Exception as e: + logger.error(f"Error stopping rumble: {e}", exc_info=True) + @Slot(int) def handle_button_slot(self, button_code: int) -> None: try: @@ -222,6 +262,7 @@ class InputManager(QObject): # Game launch on detail page if (button_code in BUTTONS['confirm']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None: if self._parent.current_exec_line: + self.trigger_rumble() self._parent.toggleGame(self._parent.current_exec_line, None) return @@ -728,17 +769,17 @@ class InputManager(QObject): def run_udev_monitor(self) -> None: try: - context = pyudev.Context() - monitor = pyudev.Monitor.from_netlink(context) + context = Context() + monitor = Monitor.from_netlink(context) monitor.filter_by(subsystem='input') - observer = pyudev.MonitorObserver(monitor, self.handle_udev_event) + observer = MonitorObserver(monitor, self.handle_udev_event) observer.start() while self.running: time.sleep(1) except Exception as e: logger.error(f"Error in udev monitor: {e}", exc_info=True) - def handle_udev_event(self, action: str, device: pyudev.Device) -> None: + def handle_udev_event(self, action: str, device: Device) -> None: try: if action == 'add': time.sleep(0.1) @@ -746,6 +787,7 @@ class InputManager(QObject): elif action == 'remove' and self.gamepad: if not any(self.gamepad.path == path for path in list_devices()): logger.info("Gamepad disconnected") + self.stop_rumble() self.gamepad = None if self.gamepad_thread: self.gamepad_thread.join() @@ -759,6 +801,7 @@ 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.stop_rumble() self.gamepad = new_gamepad if self.gamepad_thread: self.gamepad_thread.join() @@ -811,6 +854,7 @@ class InputManager(QObject): finally: if self.gamepad: try: + self.stop_rumble() self.gamepad.close() except Exception: pass @@ -820,6 +864,7 @@ class InputManager(QObject): try: self.running = False self.dpad_timer.stop() + self.stop_rumble() if self.gamepad_thread: self.gamepad_thread.join() if self.gamepad: From 24ca66a1afe55e5db627a19112d80c83840cddeb Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 19:09:56 +0500 Subject: [PATCH 42/47] chore(readme): update todo Signed-off-by: Boris Yumankulov --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d954823..b9e5c25 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ - [ ] Добавить поддержку GOG (?) - [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант) - [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?) -- [ ] Добавить виброотдачу на геймпаде при запуске игры (?) +- [X] Добавить виброотдачу на геймпаде при запуске игры - [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63) - [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)) - [ ] Скопировать логику управления с D-pad на стрелки с клавиатуры From 953e4fa71549eb519a342e2e3b0998ec4380b501 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 19:11:56 +0500 Subject: [PATCH 43/47] chore(changelog): update Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ccc34..dd9f3fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или между сессиями - [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) - Мапинги управления для Dualshock 4 и DualSense +- Виброотдача на геймпаде при запуске игры ### Changed - Обновлены все иконки From c1b8eac127a74ea622871e5d06cca789dec92b60 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 23:05:27 +0500 Subject: [PATCH 44/47] feat: add gamepad haptic feedback setting Signed-off-by: Boris Yumankulov --- portprotonqt/config_utils.py | 38 ++++++++++++++++++++++++++++++++--- portprotonqt/input_manager.py | 4 +++- portprotonqt/main_window.py | 17 +++++++++++++++- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/portprotonqt/config_utils.py b/portprotonqt/config_utils.py index cf40558..2066d2c 100644 --- a/portprotonqt/config_utils.py +++ b/portprotonqt/config_utils.py @@ -322,6 +322,41 @@ def save_favorites(favorites): with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: cp.write(configfile) +def read_rumble_config(): + """ + Читает настройку виброотдачи геймпада из секции [Gamepad]. + Если параметр отсутствует, сохраняет и возвращает False по умолчанию. + """ + cp = configparser.ConfigParser() + if os.path.exists(CONFIG_FILE): + try: + cp.read(CONFIG_FILE, encoding="utf-8") + except Exception as e: + logger.error("Ошибка чтения конфигурационного файла: %s", e) + save_rumble_config(False) + return False + if not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "rumble_enabled"): + save_rumble_config(False) + return False + return cp.getboolean("Gamepad", "rumble_enabled", fallback=False) + return False + +def save_rumble_config(rumble_enabled): + """ + Сохраняет настройку виброотдачи геймпада в секцию [Gamepad]. + """ + cp = configparser.ConfigParser() + if os.path.exists(CONFIG_FILE): + try: + cp.read(CONFIG_FILE, encoding="utf-8") + except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e: + logger.error("Ошибка чтения конфигурационного файла: %s", e) + if "Gamepad" not in cp: + cp["Gamepad"] = {} + cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled) + with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: + cp.write(configfile) + def ensure_default_proxy_config(): """ Проверяет наличие секции [Proxy] в конфигурационном файле. @@ -342,7 +377,6 @@ def ensure_default_proxy_config(): with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: cp.write(configfile) - def read_proxy_config(): """ Читает настройки прокси из секции [Proxy] конфигурационного файла. @@ -421,8 +455,6 @@ def save_fullscreen_config(fullscreen): with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: cp.write(configfile) - - def read_window_geometry() -> tuple[int, int]: """ Читает ширину и высоту окна из секции [MainWindow] конфигурационного файла. diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py index 3640bfe..9713485 100644 --- a/portprotonqt/input_manager.py +++ b/portprotonqt/input_manager.py @@ -10,7 +10,7 @@ from portprotonqt.logger import get_logger from portprotonqt.image_utils import FullscreenDialog from portprotonqt.custom_widgets import NavLabel from portprotonqt.game_card import GameCard -from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad +from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config logger = get_logger(__name__) @@ -128,6 +128,8 @@ class InputManager(QObject): def trigger_rumble(self, duration_ms: int = 200, strong_magnitude: int = 0x8000, weak_magnitude: int = 0x8000) -> None: """Trigger a rumble effect on the gamepad if supported.""" + if not read_rumble_config(): + return if not self.gamepad: return try: diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index bc0dd50..46e0414 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -26,7 +26,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 + clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config ) from portprotonqt.localization import _ from portprotonqt.logger import get_logger @@ -997,6 +997,17 @@ class MainWindow(QMainWindow): self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen) formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox) + # 7. Gamepad haptic feedback config + self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback")) + self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) + self.gamepadRumbleTitle = QLabel(_("Gamepad haptic feedback:")) + self.gamepadRumbleTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE) + self.gamepadRumbleTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus) + current_rumble_state = read_rumble_config() + self.gamepadRumbleCheckBox.setChecked(current_rumble_state) + formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox) + layout.addLayout(formLayout) # Кнопки @@ -1117,6 +1128,10 @@ class MainWindow(QMainWindow): auto_fullscreen_gamepad = self.autoFullscreenGamepadCheckBox.isChecked() save_auto_fullscreen_gamepad(auto_fullscreen_gamepad) + # Сохранение настройки виброотдачи геймпада + rumble_enabled = self.gamepadRumbleCheckBox.isChecked() + save_rumble_config(rumble_enabled) + for card in self.game_card_cache.values(): card.update_badge_visibility(filter_key) From 9fe5a8315a94c143774ad526b3c9b56465746499 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 23:13:50 +0500 Subject: [PATCH 45/47] chore(localization): update Signed-off-by: Boris Yumankulov --- 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 | 8 +++++++- .../locales/es_ES/LC_MESSAGES/messages.mo | Bin 451 -> 451 bytes .../locales/es_ES/LC_MESSAGES/messages.po | 8 +++++++- portprotonqt/locales/messages.pot | 8 +++++++- .../locales/ru_RU/LC_MESSAGES/messages.mo | Bin 13603 -> 13827 bytes .../locales/ru_RU/LC_MESSAGES/messages.po | 10 ++++++++-- 9 files changed, 35 insertions(+), 11 deletions(-) diff --git a/documentation/localization_guide/README.md b/documentation/localization_guide/README.md index f4eea9e..08dd168 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 160 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 160 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 160 of 160 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 162 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 162 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 of 162 | --- diff --git a/documentation/localization_guide/README.ru.md b/documentation/localization_guide/README.ru.md index eabcaa1..a8efde8 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 из 160 | -| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 160 | -| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 160 из 160 | +| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 162 | +| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 162 | +| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 из 162 | --- diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo index 99bd71e23efe2b7b8f78ec83ad24c916681b9a98..af709464842873b037a5e514e9c58c4958782eed 100644 GIT binary patch delta 17 YcmX@ie3*H{L^eYOBV#MWjnj1)0W@0$N&o-= delta 17 YcmX@ie3*H{L^cBjLjx)UMgRZ+ diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po index c9e17da..14a21f6 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-10 10:25+0500\n" +"POT-Creation-Date: 2025-06-11 23:15+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: de_DE\n" @@ -376,6 +376,12 @@ msgstr "" msgid "Auto Fullscreen on Gamepad connected:" msgstr "" +msgid "Gamepad haptic feedback" +msgstr "" + +msgid "Gamepad haptic feedback:" +msgstr "" + msgid "Save Settings" msgstr "" diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo index 23cb19531997d3eb614aa540c54d1360dc6df01f..e423857d91e5d9befc79d7b3a117c35c1bcbaab3 100644 GIT binary patch delta 17 YcmX@ie3*H{L^eYOBV#MWjnj1)0W@0$N&o-= delta 17 YcmX@ie3*H{L^cBjLjx)UMgRZ+ diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po index f327b4e..e139416 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-10 10:25+0500\n" +"POT-Creation-Date: 2025-06-11 23:15+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: es_ES\n" @@ -376,6 +376,12 @@ msgstr "" msgid "Auto Fullscreen on Gamepad connected:" msgstr "" +msgid "Gamepad haptic feedback" +msgstr "" + +msgid "Gamepad haptic feedback:" +msgstr "" + msgid "Save Settings" msgstr "" diff --git a/portprotonqt/locales/messages.pot b/portprotonqt/locales/messages.pot index 836a96d..10519af 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-10 10:25+0500\n" +"POT-Creation-Date: 2025-06-11 23:15+0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -374,6 +374,12 @@ msgstr "" msgid "Auto Fullscreen on Gamepad connected:" msgstr "" +msgid "Gamepad haptic feedback" +msgstr "" + +msgid "Gamepad haptic feedback:" +msgstr "" + msgid "Save Settings" msgstr "" diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo index 9754ccf66dbf04c56325edd90418f01edc521f5e..e07321607e18623fac7927157ced22dd782bcf40 100644 GIT binary patch delta 2885 zcmbW%dra0<9LMo5VxTA}cnJjK7pRDM0TELqub9mnDyT)q0=%T4A=>C-J&L+E(W!G} zqPEg>*w&_^s0q1Mvx{ZFXBOR7x;F9;tx?WgORM+C=WMO5zhV#1>)d|le9!ls-*a%q z#>&9=ah>apzk~cI@jvPw)&BXL8D%z*>MZPnYcK}aVNa~XZnzJ-V(l zM*S8R;{+a>ZWgcx3XwFl;{f~#wO|~xX~Hplb;hX}fzzFHu`BgQFdEBTdlhCWjlr3+5w3YzE$45cyb{t5>0t_8j)Z_2}VV zyboJY3xA6#cpa0m2a6`NzKx)eiUoK-uE9ZA@7h~X4_v_vv>s+@FccH9$knS*8GOan zTU~!UYThuS=#QzWqnU&OrKW@eb6YiP!VRc1+>ctg+1ZAA@HbROdc~S?oaUhd%fQ~a z5bwoG)I6K85AMe}Y(gFB`B?IwL*W7q9){7Y1yh`pk!)ER>MX0!!*!^g?nfQTyQn+# z3F;_5$Ipz4#!sFNpPon}miwfj@RA3if|E0VAoPQeyU9QV+z%^$FDuA1)iSFP7 z7{NJdqVae>j6hMBHj0-&0f*o)%tzggYEsuICQ@3*gU(aAQzKPec z4qxVJ1(-tKYjHSk#AC=Dmcy@%?#3KciXTH|Yy~P)HK@C>*|qP%h13sYfKAz-6ozA8 zwxJYHb*{t*sn_FRyo9=Rw~{BWv4y)NA-G zuEs4m1aIR*m=++f(4$LGK|3&u{&d9DQ%Ms=n}P~pE$S|8Lyh|w^%its42F*k9Yq`_vA(5I;213*x#+eE8*v97!m-(* z1ukP3>ODt=E@=YlOc$X7S&h21J5d?=3?uL=_Qs!`;W?pS&Uk!0j*cV>as(>nlih#< zyn8gL1(snXzJ$uecGPo+QRB{_QvNAw+#gNpQE8Dwt4bGpPnK4#U0&`jU9x0xS!sEN zO)Fiws8~o$J?f$9YR!=RS-{hb4&-g9=Ii5b@pEUpfP80L5d2*Y+NSht~ EH;6rVhX4Qo delta 2665 zcmYM#eN5F=9LMpm@^BG(lt>=q@{EE8NbxYVx>SPFK(I7p4KZs&S98q9Q1@#vrcOs= zVt=STP_|fv(U>u^HETgltlId(OS~?0&D^ z`*~UDapP~0e`);72~+L=zoJO9T&k-u0-G@gTQClH;Zod>QFsWG@HlF|Gp_w2uBZMZ zZo>=)Z7}n!i-M0AlbD0wp%%P@nlOn+DCS}~u5^~*66zHgjRE(*5sRsBL8h=nn1BPQ zfQL~1C$NC^?HdZ((I2j19`C0fO)o8&gS@grTv!nKSisdAQAyi|DcFjB+=ma~Db&K} zFbgkX2HuveZ?UYNjSpfbHe(*{b?;B12As!2`~~wdD%LC&%TV=3R0ca;{Y}?CiJJF1 zreg@v=x8$0Q)<>xU`}g7O}HI(h6hj!zv288HERYn`m4RkdzpbeCc6$_*`d;LCYydUkX;f;@xb_Lx{w?ZK&Y&i`hD!AuD!?UF zNTQ{nj;aVXe;q2bTV4B$s6f3=*YFyWCF^q?2HpEHRO-G&O>_;lqg$vz?xJ>f4;6R_ z`PRhIsQ!t#33IR+UqwG&K*oDEOF;|YK@AKeuew~(n2#x_$O9OHjjnw&>MXb8Vtfe| zSUW0^4pd+VTzj``??v6MzJ+$q|HMMU>}^y4r%@Aqgynb^HPKC+T123z%Q}&4#>cMk zScP+_yOGI{fKp$JYTt>)=;3A@MP1G?9-XXjnG`<66LX`z;4u~97ej> zIn*Wl33bV4QQwY#QGsRpgGX773aA0gu>~t}0O@MqV-DW(bN&q!68O@t!yV3dQ3I}{ z+Ee-6Y{Xr-5kJQOMzFg2ZA6XlMQz|~)VRg0qW4Xxe*IX37hOFz*9%Tmmm3^-6cynG zCL!^&5BfA5eiPZG3TT z1!}%0Q3180j;bGZ|LiOUJ~qum5r)%CDN0AByaqLJ2M+}>fI9mTRKFXjAFw#`8H3rV zqbNlMupT)jdjYx6)`vqlitn?&JzE-FU>+l=7q1Ck%1YEk`%!`Pqi**nsEpjkaP*Z0 z`^7l3F@*OOsKBb74M@tZ&9%RRYZG|UK|u=~L?3>D%EZU038ql}uAowW6V)$utS&O& jH#Qg_?h7yWmlRi(9vex@@&$`!W7A1Zv18)}_0j(Ug7E$x diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po index 1523e86..a26b290 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-10 10:25+0500\n" -"PO-Revision-Date: 2025-06-10 10:24+0500\n" +"POT-Creation-Date: 2025-06-11 23:15+0500\n" +"PO-Revision-Date: 2025-06-11 23:15+0500\n" "Last-Translator: \n" "Language: ru_RU\n" "Language-Team: ru_RU \n" @@ -383,6 +383,12 @@ msgstr "Режим полноэкранного отображения прил msgid "Auto Fullscreen on Gamepad connected:" msgstr "Режим полноэкранного отображения приложения при подключении геймпада:" +msgid "Gamepad haptic feedback" +msgstr "Тактильная обратная связь на геймпаде" + +msgid "Gamepad haptic feedback:" +msgstr "Тактильная обратная связь на геймпаде:" + msgid "Save Settings" msgstr "Сохранить настройки" From 84708ed2600f245e737813073d18ac114f7a345f Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Wed, 11 Jun 2025 23:19:31 +0500 Subject: [PATCH 46/47] chore(changelog): update Signed-off-by: Boris Yumankulov --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd9f3fb..1de1388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ - Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или между сессиями - [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) - Мапинги управления для Dualshock 4 и DualSense -- Виброотдача на геймпаде при запуске игры +- Настройка тактильной обратной связи на геймпаде при запуске игры (по умолчанию отключена) ### Changed - Обновлены все иконки From dbf1340f8859f0cbdbf2b8cd404fdb5ccc282ac3 Mon Sep 17 00:00:00 2001 From: Boris Yumankulov Date: Thu, 12 Jun 2025 14:37:03 +0500 Subject: [PATCH 47/47] feat: added colors to AreWeAntiCheatYet badges Signed-off-by: Boris Yumankulov --- portprotonqt/game_card.py | 2 +- portprotonqt/main_window.py | 2 +- portprotonqt/themes/standart-light/styles.py | 20 +++++++++++++++++++ portprotonqt/themes/standart/styles.py | 21 ++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/portprotonqt/game_card.py b/portprotonqt/game_card.py index 42b558a..8ad4a20 100644 --- a/portprotonqt/game_card.py +++ b/portprotonqt/game_card.py @@ -199,7 +199,7 @@ class GameCard(QFrame): icon_size=16, icon_space=3, ) - self.anticheatLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE) + self.anticheatLabel.setStyleSheet(self.theme.get_anticheat_badge_style(anticheat_status)) self.anticheatLabel.setFixedWidth(int(card_width * 2/3)) anticheat_visible = True else: diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py index 46e0414..23af778 100644 --- a/portprotonqt/main_window.py +++ b/portprotonqt/main_window.py @@ -1492,7 +1492,7 @@ class MainWindow(QMainWindow): icon_size=16, icon_space=3, ) - anticheatLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE) + anticheatLabel.setStyleSheet(self.theme.get_anticheat_badge_style(anticheat_status)) anticheatLabel.setFixedWidth(badge_width) anticheatLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://areweanticheatyet.com/game/{name.lower().replace(' ', '-')}"))) anticheat_visible = True diff --git a/portprotonqt/themes/standart-light/styles.py b/portprotonqt/themes/standart-light/styles.py index 84d5da4..83f2435 100644 --- a/portprotonqt/themes/standart-light/styles.py +++ b/portprotonqt/themes/standart-light/styles.py @@ -416,6 +416,26 @@ def get_protondb_badge_style(tier): font-weight: bold; """ +def get_anticheat_badge_style(status): + status = status.lower() + status_colors = { + "supported": {"background": "rgba(102, 168, 15, 0.7)", "color": "black"}, + "running": {"background": "rgba(25, 113, 194, 0.7)", "color": "black"}, + "planned": {"background": "rgba(156, 54, 181, 0.7)", "color": "black"}, + "broken": {"background": "rgba(232, 89, 12, 0.7)", "color": "black"}, + "denied": {"background": "rgba(224, 49, 49, 0.7)", "color": "black"} + } + colors = status_colors.get(status, {"background": "rgba(0, 0, 0, 0.5)", "color": "white"}) + return f""" + qproperty-alignment: AlignCenter; + background-color: {colors["background"]}; + color: {colors["color"]}; + font-size: 14px; + border-radius: 5px; + font-family: 'Poppins'; + font-weight: bold; + """ + # СТИЛИ БЕЙДЖА STEAM STEAM_BADGE_STYLE= """ qproperty-alignment: AlignCenter; diff --git a/portprotonqt/themes/standart/styles.py b/portprotonqt/themes/standart/styles.py index 2ba0c9e..bcc6a27 100644 --- a/portprotonqt/themes/standart/styles.py +++ b/portprotonqt/themes/standart/styles.py @@ -478,6 +478,27 @@ def get_protondb_badge_style(tier): font-weight: bold; """ +# СТИЛИ БЕЙДЖА WEANTICHEATYET +def get_anticheat_badge_style(status): + status = status.lower() + status_colors = { + "supported": {"background": "rgba(102, 168, 15, 0.7)", "color": "black"}, + "running": {"background": "rgba(25, 113, 194, 0.7)", "color": "black"}, + "planned": {"background": "rgba(156, 54, 181, 0.7)", "color": "black"}, + "broken": {"background": "rgba(232, 89, 12, 0.7)", "color": "black"}, + "denied": {"background": "rgba(224, 49, 49, 0.7)", "color": "black"} + } + colors = status_colors.get(status, {"background": "rgba(0, 0, 0, 0.5)", "color": "white"}) + return f""" + qproperty-alignment: AlignCenter; + background-color: {colors["background"]}; + color: {colors["color"]}; + font-size: 16px; + border-radius: 5px; + font-family: 'Play'; + font-weight: bold; + """ + # СТИЛИ БЕЙДЖА STEAM STEAM_BADGE_STYLE= """ qproperty-alignment: AlignCenter;