forked from Boria138/PortProtonQt
Compare commits
16 Commits
67e56e33ab
...
647394ca92
Author | SHA1 | Date | |
---|---|---|---|
647394ca92
|
|||
14dc44d4f7
|
|||
34e70d05f3
|
|||
a21705da15
|
|||
1ea5fd710c
|
|||
4de4bdb99d
|
|||
bcf319c024
|
|||
83455bc33f
|
|||
2377426b27
|
|||
a5977f0f59
|
|||
3e49357152
|
|||
9c4ad0b7ba
|
|||
0f59c46d36
|
|||
364e1dd02a
|
|||
c037af4314
|
|||
2ae3831662
|
@@ -7,8 +7,6 @@
|
||||
|
||||
### Added
|
||||
- Кнопки сброса настроек и очистки кэша
|
||||
- Начальная интеграция с EGS с помощью [Legendary](https://github.com/derrod/legendary)
|
||||
- Бейдж EGS
|
||||
- Бейдж PortProton
|
||||
- Зависимость на `xdg-utils`
|
||||
- Интеграция статуса WeAntiCheatYet в карточку
|
||||
@@ -24,6 +22,7 @@
|
||||
- Пункт в контекстное меню "Удалить из Steam”
|
||||
- Метод сортировки сначала избранное
|
||||
- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
|
||||
- Обработчики для QMenu и QComboBox на геймпаде
|
||||
|
||||
### Changed
|
||||
- Обновлены все иконки
|
||||
@@ -37,8 +36,12 @@
|
||||
- Установка ширины бейджа в две трети ширины карточки
|
||||
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
|
||||
- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку
|
||||
- Теперь D-pad можно зажимать для переключения карточек
|
||||
- D-pad больше не переключает вкладки только RB и LB
|
||||
- Кнопка добавления игры больше не фокусируется
|
||||
- Диалог добавления игры теперь открывается только в библиотеке
|
||||
- Аргумент --fullscreen для открытия приложения в режиме полноэкранного отображения
|
||||
- Оверлей на кнопку Xbox / PS для закрытия приложения, выключения, перезагрузки и ухода в сон
|
||||
|
||||
### Fixed
|
||||
- Обработка несуществующей темы с возвратом к “standart”
|
||||
|
@@ -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 что бы ускорить время запуска
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<name>PortProtonQt</name>
|
||||
<id>ru.linux_gaming.PortProtonQt</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0-or-later</project_license>
|
||||
<summary>Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store</summary>
|
||||
<summary xml:lang="ru">Современный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store</summary>
|
||||
<description>
|
||||
<p>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.</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">ru.linux_gaming.PortProtonQt.desktop</launchable>
|
||||
<developer id="ru.linux_gaming">
|
||||
<name>Boria138</name>
|
||||
</developer>
|
||||
<recommends>
|
||||
<control>keyboard</control>
|
||||
<control>pointing</control>
|
||||
<control>touch</control>
|
||||
<control>gamepad</control>
|
||||
</recommends>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#007AFF</color>
|
||||
<color type="primary" scheme_preference="dark">#09BEC8</color>
|
||||
</branding>
|
||||
<categories>
|
||||
<category>Game</category>
|
||||
<category>Utility</category>
|
||||
</categories>
|
||||
<url type="homepage">https://git.linux-gaming.ru/Boria138/PortProtonQt</url>
|
||||
<url type="bugtracker">https://git.linux-gaming.ru/Boria138/PortProtonQt/issues</url>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>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</image>
|
||||
<caption>Library</caption>
|
||||
<caption xml:lang="ru">Библиотека</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>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</image>
|
||||
<caption>Card detail page</caption>
|
||||
<caption xml:lang="ru">Детали игры</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>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</image>
|
||||
<caption>Settings</caption>
|
||||
<caption xml:lang="ru">Настройки</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<keywords>
|
||||
<keyword translate="no">wine</keyword>
|
||||
<keyword translate="no">proton</keyword>
|
||||
<keyword translate="no">steam</keyword>
|
||||
<keyword translate="no">windows</keyword>
|
||||
<keyword translate="no">epic games store</keyword>
|
||||
<keyword translate="no">egs</keyword>
|
||||
<keyword translate="no">qt</keyword>
|
||||
<keyword translate="no">portproton</keyword>
|
||||
<keyword>games</keyword>
|
||||
</keywords>
|
||||
<content_rating type="oars-1.1" />
|
||||
</component>
|
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -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__':
|
||||
|
16
portprotonqt/cli.py
Normal file
16
portprotonqt/cli.py
Normal file
@@ -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()
|
@@ -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
|
||||
|
@@ -261,41 +261,40 @@ 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)
|
||||
|
||||
# Reposition badges
|
||||
# Подготавливаем список всех бейджей с их текущей видимостью
|
||||
badges = [
|
||||
(self.steam_visible, self.steamLabel),
|
||||
(self.egs_visible, self.egsLabel),
|
||||
(self.portproton_visible, self.portprotonLabel),
|
||||
(protondb_visible, self.protondbLabel),
|
||||
(anticheat_visible, self.anticheatLabel),
|
||||
]
|
||||
|
||||
# Пересчитываем позиции бейджей
|
||||
right_margin = 8
|
||||
badge_spacing = 5
|
||||
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)
|
||||
|
||||
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_()
|
||||
|
@@ -3,8 +3,8 @@ 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.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView
|
||||
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
|
||||
@@ -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
|
||||
@@ -37,11 +39,12 @@ 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},
|
||||
'guide': {ecodes.BTN_MODE},
|
||||
}
|
||||
|
||||
class InputManager(QObject):
|
||||
@@ -69,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
|
||||
@@ -81,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)
|
||||
@@ -129,10 +137,66 @@ class InputManager(QObject):
|
||||
return
|
||||
active = QApplication.activeWindow()
|
||||
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']:
|
||||
if popup.activeAction():
|
||||
popup.activeAction().trigger()
|
||||
popup.close()
|
||||
return
|
||||
elif button_code in BUTTONS['back'] or button_code in BUTTONS['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
|
||||
@@ -149,7 +213,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
|
||||
@@ -164,6 +230,8 @@ 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']:
|
||||
# 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)
|
||||
@@ -176,6 +244,14 @@ 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:
|
||||
@@ -188,6 +264,71 @@ class InputManager(QObject):
|
||||
if not app:
|
||||
return
|
||||
active = QApplication.activeWindow()
|
||||
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():
|
||||
# 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:
|
||||
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
|
||||
|
||||
# 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:
|
||||
@@ -197,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()
|
||||
@@ -280,7 +408,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
|
||||
@@ -390,6 +517,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)
|
||||
@@ -520,6 +664,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):
|
||||
@@ -540,6 +687,8 @@ class InputManager(QObject):
|
||||
elif key == Qt.Key.Key_E:
|
||||
if isinstance(focused, QLineEdit):
|
||||
return False
|
||||
# Only open AddGameDialog if in library tab (index 0)
|
||||
if self._parent.stackedWidget.currentIndex() == 0:
|
||||
self._parent.openAddGameDialog()
|
||||
return True
|
||||
|
||||
@@ -652,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:
|
||||
|
Binary file not shown.
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-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 <EMAIL@ADDRESS>\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 ""
|
||||
|
||||
|
Binary file not shown.
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-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 <EMAIL@ADDRESS>\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 ""
|
||||
|
||||
|
@@ -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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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 ""
|
||||
|
||||
|
Binary file not shown.
@@ -9,8 +9,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
|
||||
"PO-Revision-Date: 2025-06-06 20:01+0500\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:31+0500\n"
|
||||
"PO-Revision-Date: 2025-06-08 09:31+0500\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: ru_RU\n"
|
||||
"Language-Team: ru_RU <LL@li.org>\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 "только что"
|
||||
|
||||
|
@@ -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
|
||||
@@ -259,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)
|
||||
@@ -285,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 []
|
||||
@@ -500,6 +489,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)
|
||||
@@ -539,6 +533,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 +579,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 +604,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()
|
||||
@@ -742,6 +724,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
|
||||
@@ -920,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)
|
||||
@@ -989,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)
|
||||
|
||||
# Кнопки
|
||||
@@ -1070,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(
|
||||
|
87
portprotonqt/system_overlay.py
Normal file
87
portprotonqt/system_overlay.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import subprocess
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, 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)
|
||||
|
||||
# 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 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()
|
Reference in New Issue
Block a user