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
 | 
					### Added
 | 
				
			||||||
- Кнопки сброса настроек и очистки кэша
 | 
					- Кнопки сброса настроек и очистки кэша
 | 
				
			||||||
- Начальная интеграция с EGS с помощью [Legendary](https://github.com/derrod/legendary)
 | 
					 | 
				
			||||||
- Бейдж EGS
 | 
					 | 
				
			||||||
- Бейдж PortProton
 | 
					- Бейдж PortProton
 | 
				
			||||||
- Зависимость на `xdg-utils`
 | 
					- Зависимость на `xdg-utils`
 | 
				
			||||||
- Интеграция статуса WeAntiCheatYet в карточку
 | 
					- Интеграция статуса WeAntiCheatYet в карточку
 | 
				
			||||||
@@ -24,6 +22,7 @@
 | 
				
			|||||||
- Пункт в контекстное меню "Удалить из Steam”
 | 
					- Пункт в контекстное меню "Удалить из Steam”
 | 
				
			||||||
- Метод сортировки сначала избранное
 | 
					- Метод сортировки сначала избранное
 | 
				
			||||||
- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
 | 
					- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
 | 
				
			||||||
 | 
					- Обработчики для QMenu и QComboBox на геймпаде
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Changed
 | 
					### Changed
 | 
				
			||||||
- Обновлены все иконки
 | 
					- Обновлены все иконки
 | 
				
			||||||
@@ -37,8 +36,12 @@
 | 
				
			|||||||
- Установка ширины бейджа в две трети ширины карточки
 | 
					- Установка ширины бейджа в две трети ширины карточки
 | 
				
			||||||
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
 | 
					- Бейджи источников (`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
 | 
					### Fixed
 | 
				
			||||||
- Обработка несуществующей темы с возвратом к “standart”
 | 
					- Обработка несуществующей темы с возвратом к “standart”
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@
 | 
				
			|||||||
## В планах
 | 
					## В планах
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
 | 
					- [X] Адаптировать структуру проекта для поддержки инструментов сборки
 | 
				
			||||||
- [ ] Добавить возможность управление с геймпада
 | 
					- [X] Добавить возможность управление с геймпада
 | 
				
			||||||
- [ ] Добавить возможность управление с тачскрина
 | 
					- [ ] Добавить возможность управление с тачскрина
 | 
				
			||||||
- [X] Добавить возможность управление с мыши и клавиатуры
 | 
					- [X] Добавить возможность управление с мыши и клавиатуры
 | 
				
			||||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
 | 
					- [X] Добавить систему тем [Документация](documentation/theme_guide)
 | 
				
			||||||
@@ -16,6 +16,7 @@
 | 
				
			|||||||
- [ ] Продумать систему вкладок вместо той что есть сейчас
 | 
					- [ ] Продумать систему вкладок вместо той что есть сейчас
 | 
				
			||||||
- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
 | 
					- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
 | 
				
			||||||
- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
 | 
					- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
 | 
				
			||||||
 | 
					- [ ] Переделать скриншоты для соответсвия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
 | 
				
			||||||
- [X] Брать описание и названия игр с базы данных Steam
 | 
					- [X] Брать описание и названия игр с базы данных Steam
 | 
				
			||||||
- [X] Брать обложки для игр со SteamGridDB или CDN Steam
 | 
					- [X] Брать обложки для игр со SteamGridDB или CDN Steam
 | 
				
			||||||
- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска
 | 
					- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,7 +45,7 @@ Requires:       perl-Image-ExifTool
 | 
				
			|||||||
Requires:       xdg-utils
 | 
					Requires:       xdg-utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%description -n python3-%{pypi_name}-git
 | 
					%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
 | 
					%prep
 | 
				
			||||||
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
 | 
					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}
 | 
					%files -n python3-%{pypi_name}-git -f %{pyproject_files}
 | 
				
			||||||
%{_bindir}/%{pypi_name}
 | 
					%{_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
 | 
					%changelog
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,7 +42,7 @@ Requires:       perl-Image-ExifTool
 | 
				
			|||||||
Requires:       xdg-utils
 | 
					Requires:       xdg-utils
 | 
				
			||||||
 | 
					
 | 
				
			||||||
%description -n python3-%{pypi_name}
 | 
					%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
 | 
					%prep
 | 
				
			||||||
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt
 | 
					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}
 | 
					%files -n python3-%{pypi_name} -f %{pyproject_files}
 | 
				
			||||||
%{_bindir}/%{pypi_name}
 | 
					%{_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
 | 
					%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 |
 | 
					| Locale | Progress | Translated |
 | 
				
			||||||
| :----- | -------: | ---------: |
 | 
					| :----- | -------: | ---------: |
 | 
				
			||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 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 154 |
 | 
					| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 153 |
 | 
				
			||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 of 154 |
 | 
					| [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 |
 | 
					| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 153 |
 | 
				
			||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 154 |
 | 
					| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 153 |
 | 
				
			||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 из 154 |
 | 
					| [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 PySide6.QtGui import QIcon
 | 
				
			||||||
from portprotonqt.main_window import MainWindow
 | 
					from portprotonqt.main_window import MainWindow
 | 
				
			||||||
from portprotonqt.tray import SystemTray
 | 
					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.logger import get_logger
 | 
				
			||||||
 | 
					from portprotonqt.cli import parse_args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = get_logger(__name__)
 | 
					logger = get_logger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,7 +29,17 @@ def main():
 | 
				
			|||||||
    else:
 | 
					    else:
 | 
				
			||||||
        logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}")
 | 
					        logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Парсинг аргументов командной строки
 | 
				
			||||||
 | 
					    args = parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window = MainWindow()
 | 
					    window = MainWindow()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Обработка флага --fullscreen
 | 
				
			||||||
 | 
					    if args.fullscreen:
 | 
				
			||||||
 | 
					        logger.info("Запуск в полноэкранном режиме по флагу --fullscreen")
 | 
				
			||||||
 | 
					        save_fullscreen_config(True)
 | 
				
			||||||
 | 
					        window.showFullScreen()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    current_theme_name = read_theme_from_config()
 | 
					    current_theme_name = read_theme_from_config()
 | 
				
			||||||
    tray = SystemTray(app, current_theme_name)
 | 
					    tray = SystemTray(app, current_theme_name)
 | 
				
			||||||
    tray.show_action.triggered.connect(window.show)
 | 
					    tray.show_action.triggered.connect(window.show)
 | 
				
			||||||
@@ -43,7 +54,9 @@ def main():
 | 
				
			|||||||
        tray.hide_action.triggered.connect(window.hide)
 | 
					        tray.hide_action.triggered.connect(window.hide)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window.settings_saved.connect(recreate_tray)
 | 
					    window.settings_saved.connect(recreate_tray)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window.show()
 | 
					    window.show()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sys.exit(app.exec())
 | 
					    sys.exit(app.exec())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					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.config_utils import parse_desktop_entry
 | 
				
			||||||
from portprotonqt.localization import _
 | 
					from portprotonqt.localization import _
 | 
				
			||||||
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
 | 
					from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
 | 
				
			||||||
 | 
					from portprotonqt.dialogs import AddGameDialog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ContextMenuManager:
 | 
					class ContextMenuManager:
 | 
				
			||||||
    """Manages context menu actions for game management in PortProtonQT."""
 | 
					    """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):
 | 
					    def edit_game_shortcut(self, game_name, exec_line, cover_path):
 | 
				
			||||||
        """Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
 | 
					        """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():
 | 
					        if not self._check_portproton():
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -261,46 +261,45 @@ class GameCard(QFrame):
 | 
				
			|||||||
        self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
 | 
					        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.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"))
 | 
					        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.steamLabel.setVisible(self.steam_visible)
 | 
				
			||||||
        self.egsLabel.setVisible(self.egs_visible)
 | 
					        self.egsLabel.setVisible(self.egs_visible)
 | 
				
			||||||
        self.portprotonLabel.setVisible(self.portproton_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
 | 
					        right_margin = 8
 | 
				
			||||||
        badge_spacing = 5
 | 
					        badge_spacing = 5
 | 
				
			||||||
        top_y = 10
 | 
					        top_y = 10
 | 
				
			||||||
        badge_y_positions = []
 | 
					        badge_y_positions = []
 | 
				
			||||||
        badge_width = int(self.coverLabel.width() * 2/3)
 | 
					        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_()
 | 
					        for is_visible, badge in badges:
 | 
				
			||||||
            self.protondbLabel.raise_()
 | 
					            if is_visible:
 | 
				
			||||||
            self.portprotonLabel.raise_()
 | 
					                badge_x = self.coverLabel.width() - badge_width - right_margin
 | 
				
			||||||
            self.egsLabel.raise_()
 | 
					                badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
 | 
				
			||||||
            self.steamLabel.raise_()
 | 
					                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):
 | 
					    def _show_context_menu(self, pos):
 | 
				
			||||||
        """Delegate context menu display to ContextMenuManager."""
 | 
					        """Delegate context menu display to ContextMenuManager."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,8 +3,8 @@ import threading
 | 
				
			|||||||
from typing import Protocol, cast
 | 
					from typing import Protocol, cast
 | 
				
			||||||
from evdev import InputDevice, ecodes, list_devices
 | 
					from evdev import InputDevice, ecodes, list_devices
 | 
				
			||||||
import pyudev
 | 
					import pyudev
 | 
				
			||||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog
 | 
					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 PySide6.QtGui import QKeyEvent
 | 
				
			||||||
from portprotonqt.logger import get_logger
 | 
					from portprotonqt.logger import get_logger
 | 
				
			||||||
from portprotonqt.image_utils import FullscreenDialog
 | 
					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 toggleGame(self, exec_line: str | None, button: QWidget | None = None) -> None:
 | 
				
			||||||
        ...
 | 
					        ...
 | 
				
			||||||
 | 
					    def openSystemOverlay(self) -> None:
 | 
				
			||||||
 | 
					            ...
 | 
				
			||||||
    stackedWidget: QStackedWidget
 | 
					    stackedWidget: QStackedWidget
 | 
				
			||||||
    tabButtons: dict[int, QWidget]
 | 
					    tabButtons: dict[int, QWidget]
 | 
				
			||||||
    gamesListWidget: QWidget
 | 
					    gamesListWidget: QWidget
 | 
				
			||||||
@@ -37,11 +39,12 @@ BUTTONS = {
 | 
				
			|||||||
    'confirm':   {ecodes.BTN_A},
 | 
					    'confirm':   {ecodes.BTN_A},
 | 
				
			||||||
    'back':      {ecodes.BTN_B},
 | 
					    'back':      {ecodes.BTN_B},
 | 
				
			||||||
    'add_game':  {ecodes.BTN_Y},
 | 
					    'add_game':  {ecodes.BTN_Y},
 | 
				
			||||||
    'prev_tab':  {ecodes.BTN_TL, ecodes.BTN_TRIGGER_HAPPY7},
 | 
					    'prev_tab':  {ecodes.BTN_TL},
 | 
				
			||||||
    'next_tab':  {ecodes.BTN_TR, ecodes.BTN_TRIGGER_HAPPY5},
 | 
					    'next_tab':  {ecodes.BTN_TR},
 | 
				
			||||||
    'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
 | 
					    'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
 | 
				
			||||||
    'context_menu': {ecodes.BTN_START},
 | 
					    'context_menu': {ecodes.BTN_START},
 | 
				
			||||||
    'menu':      {ecodes.BTN_SELECT},
 | 
					    'menu':      {ecodes.BTN_SELECT},
 | 
				
			||||||
 | 
					    'guide':     {ecodes.BTN_MODE},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InputManager(QObject):
 | 
					class InputManager(QObject):
 | 
				
			||||||
@@ -69,7 +72,6 @@ class InputManager(QObject):
 | 
				
			|||||||
        self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
 | 
					        self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
 | 
				
			||||||
        self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', 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._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.axis_deadzone = axis_deadzone
 | 
					        self.axis_deadzone = axis_deadzone
 | 
				
			||||||
        self.initial_axis_move_delay = initial_axis_move_delay
 | 
					        self.initial_axis_move_delay = initial_axis_move_delay
 | 
				
			||||||
        self.repeat_axis_move_delay = repeat_axis_move_delay
 | 
					        self.repeat_axis_move_delay = repeat_axis_move_delay
 | 
				
			||||||
@@ -81,6 +83,12 @@ class InputManager(QObject):
 | 
				
			|||||||
        self.running = True
 | 
					        self.running = True
 | 
				
			||||||
        self._is_fullscreen = read_fullscreen_config()
 | 
					        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
 | 
					        # Connect signals to slots
 | 
				
			||||||
        self.button_pressed.connect(self.handle_button_slot)
 | 
					        self.button_pressed.connect(self.handle_button_slot)
 | 
				
			||||||
        self.dpad_moved.connect(self.handle_dpad_slot)
 | 
					        self.dpad_moved.connect(self.handle_dpad_slot)
 | 
				
			||||||
@@ -129,10 +137,66 @@ class InputManager(QObject):
 | 
				
			|||||||
                return
 | 
					                return
 | 
				
			||||||
            active = QApplication.activeWindow()
 | 
					            active = QApplication.activeWindow()
 | 
				
			||||||
            focused = QApplication.focusWidget()
 | 
					            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
 | 
					            # Закрытие AddGameDialog на кнопку B
 | 
				
			||||||
            if button_code in BUTTONS['back'] and isinstance(active, QDialog):
 | 
					            if button_code in BUTTONS['back'] and isinstance(active, QDialog):
 | 
				
			||||||
                active.reject()  # Закрываем диалог
 | 
					                active.reject()
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # FullscreenDialog
 | 
					            # FullscreenDialog
 | 
				
			||||||
@@ -149,7 +213,9 @@ class InputManager(QObject):
 | 
				
			|||||||
            if isinstance(focused, GameCard):
 | 
					            if isinstance(focused, GameCard):
 | 
				
			||||||
                if button_code in BUTTONS['context_menu']:
 | 
					                if button_code in BUTTONS['context_menu']:
 | 
				
			||||||
                    pos = QPoint(focused.width() // 2, focused.height() // 2)
 | 
					                    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
 | 
					                    return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Game launch on detail page
 | 
					            # Game launch on detail page
 | 
				
			||||||
@@ -164,7 +230,9 @@ class InputManager(QObject):
 | 
				
			|||||||
            elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
 | 
					            elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
 | 
				
			||||||
                self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
 | 
					                self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
 | 
				
			||||||
            elif button_code in BUTTONS['add_game']:
 | 
					            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']:
 | 
					            elif button_code in BUTTONS['prev_tab']:
 | 
				
			||||||
                idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
 | 
					                idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
 | 
				
			||||||
                self._parent.switchTab(idx)
 | 
					                self._parent.switchTab(idx)
 | 
				
			||||||
@@ -176,6 +244,14 @@ class InputManager(QObject):
 | 
				
			|||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
 | 
					            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)
 | 
					    @Slot(int, int, float)
 | 
				
			||||||
    def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
 | 
					    def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
 | 
				
			||||||
@@ -188,6 +264,71 @@ class InputManager(QObject):
 | 
				
			|||||||
            if not app:
 | 
					            if not app:
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
            active = QApplication.activeWindow()
 | 
					            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
 | 
					            # Fullscreen horizontal navigation
 | 
				
			||||||
            if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
 | 
					            if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
 | 
				
			||||||
@@ -197,19 +338,6 @@ class InputManager(QObject):
 | 
				
			|||||||
                    active.show_next()
 | 
					                    active.show_next()
 | 
				
			||||||
                return
 | 
					                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)
 | 
					            # Library tab navigation (index 0)
 | 
				
			||||||
            if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
 | 
					            if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
 | 
				
			||||||
                focused = QApplication.focusWidget()
 | 
					                focused = QApplication.focusWidget()
 | 
				
			||||||
@@ -280,7 +408,6 @@ class InputManager(QObject):
 | 
				
			|||||||
                                    next_card.setFocus()
 | 
					                                    next_card.setFocus()
 | 
				
			||||||
                                    if scroll_area:
 | 
					                                    if scroll_area:
 | 
				
			||||||
                                        scroll_area.ensureWidgetVisible(next_card, 50, 50)
 | 
					                                        scroll_area.ensureWidgetVisible(next_card, 50, 50)
 | 
				
			||||||
 | 
					 | 
				
			||||||
                elif code == ecodes.ABS_HAT0Y and value != 0:  # Up/Down
 | 
					                elif code == ecodes.ABS_HAT0Y and value != 0:  # Up/Down
 | 
				
			||||||
                    if value > 0:  # Down
 | 
					                    if value > 0:  # Down
 | 
				
			||||||
                        next_row_idx = current_row_idx + 1
 | 
					                        next_row_idx = current_row_idx + 1
 | 
				
			||||||
@@ -390,6 +517,23 @@ class InputManager(QObject):
 | 
				
			|||||||
                focused._show_context_menu(pos)
 | 
					                focused._show_context_menu(pos)
 | 
				
			||||||
                return True
 | 
					                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)
 | 
					        # Tab switching with Left/Right keys (non-GameCard focus or no focus)
 | 
				
			||||||
        idx = self._parent.stackedWidget.currentIndex()
 | 
					        idx = self._parent.stackedWidget.currentIndex()
 | 
				
			||||||
        total = len(self._parent.tabButtons)
 | 
					        total = len(self._parent.tabButtons)
 | 
				
			||||||
@@ -520,6 +664,9 @@ class InputManager(QObject):
 | 
				
			|||||||
                if focusables:
 | 
					                if focusables:
 | 
				
			||||||
                    focusables[0].setFocus()
 | 
					                    focusables[0].setFocus()
 | 
				
			||||||
                    return True
 | 
					                    return True
 | 
				
			||||||
 | 
					            elif focused:
 | 
				
			||||||
 | 
					                focused.focusNextChild()
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
        # Navigate up through tab content
 | 
					        # Navigate up through tab content
 | 
				
			||||||
        if key == Qt.Key.Key_Up:
 | 
					        if key == Qt.Key.Key_Up:
 | 
				
			||||||
            if isinstance(focused, NavLabel):
 | 
					            if isinstance(focused, NavLabel):
 | 
				
			||||||
@@ -540,8 +687,10 @@ class InputManager(QObject):
 | 
				
			|||||||
        elif key == Qt.Key.Key_E:
 | 
					        elif key == Qt.Key.Key_E:
 | 
				
			||||||
            if isinstance(focused, QLineEdit):
 | 
					            if isinstance(focused, QLineEdit):
 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
            self._parent.openAddGameDialog()
 | 
					            # Only open AddGameDialog if in library tab (index 0)
 | 
				
			||||||
            return True
 | 
					            if self._parent.stackedWidget.currentIndex() == 0:
 | 
				
			||||||
 | 
					                self._parent.openAddGameDialog()
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Toggle fullscreen with F11
 | 
					        # Toggle fullscreen with F11
 | 
				
			||||||
        if key == Qt.Key.Key_F11:
 | 
					        if key == Qt.Key.Key_F11:
 | 
				
			||||||
@@ -652,6 +801,7 @@ class InputManager(QObject):
 | 
				
			|||||||
    def cleanup(self) -> None:
 | 
					    def cleanup(self) -> None:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.running = False
 | 
					            self.running = False
 | 
				
			||||||
 | 
					            self.dpad_timer.stop()
 | 
				
			||||||
            if self.gamepad_thread:
 | 
					            if self.gamepad_thread:
 | 
				
			||||||
                self.gamepad_thread.join()
 | 
					                self.gamepad_thread.join()
 | 
				
			||||||
            if self.gamepad:
 | 
					            if self.gamepad:
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							@@ -9,7 +9,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PROJECT VERSION\n"
 | 
					"Project-Id-Version: PROJECT VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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"
 | 
					"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
				
			||||||
"Language: de_DE\n"
 | 
					"Language: de_DE\n"
 | 
				
			||||||
@@ -362,21 +362,6 @@ msgstr ""
 | 
				
			|||||||
msgid "Auto Fullscreen on Gamepad connected:"
 | 
					msgid "Auto Fullscreen on Gamepad connected:"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "Save Settings"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -392,22 +377,6 @@ msgstr ""
 | 
				
			|||||||
msgid "Failed to open Legendary login page"
 | 
					msgid "Failed to open Legendary login page"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "Confirm Reset"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -505,6 +474,33 @@ msgstr ""
 | 
				
			|||||||
msgid "Launching"
 | 
					msgid "Launching"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "just now"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							@@ -9,7 +9,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PROJECT VERSION\n"
 | 
					"Project-Id-Version: PROJECT VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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"
 | 
					"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
				
			||||||
"Language: es_ES\n"
 | 
					"Language: es_ES\n"
 | 
				
			||||||
@@ -362,21 +362,6 @@ msgstr ""
 | 
				
			|||||||
msgid "Auto Fullscreen on Gamepad connected:"
 | 
					msgid "Auto Fullscreen on Gamepad connected:"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "Save Settings"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -392,22 +377,6 @@ msgstr ""
 | 
				
			|||||||
msgid "Failed to open Legendary login page"
 | 
					msgid "Failed to open Legendary login page"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "Confirm Reset"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -505,6 +474,33 @@ msgstr ""
 | 
				
			|||||||
msgid "Launching"
 | 
					msgid "Launching"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "just now"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PortProtonQT 0.1.1\n"
 | 
					"Project-Id-Version: PortProtonQT 0.1.1\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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"
 | 
					"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
				
			||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
					"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
				
			||||||
@@ -360,21 +360,6 @@ msgstr ""
 | 
				
			|||||||
msgid "Auto Fullscreen on Gamepad connected:"
 | 
					msgid "Auto Fullscreen on Gamepad connected:"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "Save Settings"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -390,22 +375,6 @@ msgstr ""
 | 
				
			|||||||
msgid "Failed to open Legendary login page"
 | 
					msgid "Failed to open Legendary login page"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "Confirm Reset"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -503,6 +472,33 @@ msgstr ""
 | 
				
			|||||||
msgid "Launching"
 | 
					msgid "Launching"
 | 
				
			||||||
msgstr ""
 | 
					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"
 | 
					msgid "just now"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							@@ -9,8 +9,8 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PROJECT VERSION\n"
 | 
					"Project-Id-Version: PROJECT VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\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: 2025-06-06 20:01+0500\n"
 | 
					"PO-Revision-Date: 2025-06-08 09:31+0500\n"
 | 
				
			||||||
"Last-Translator: \n"
 | 
					"Last-Translator: \n"
 | 
				
			||||||
"Language: ru_RU\n"
 | 
					"Language: ru_RU\n"
 | 
				
			||||||
"Language-Team: ru_RU <LL@li.org>\n"
 | 
					"Language-Team: ru_RU <LL@li.org>\n"
 | 
				
			||||||
@@ -369,21 +369,6 @@ msgstr "Режим полноэкранного отображения прил
 | 
				
			|||||||
msgid "Auto Fullscreen on Gamepad connected:"
 | 
					msgid "Auto Fullscreen on Gamepad connected:"
 | 
				
			||||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
 | 
					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"
 | 
					msgid "Save Settings"
 | 
				
			||||||
msgstr "Сохранить настройки"
 | 
					msgstr "Сохранить настройки"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -399,22 +384,6 @@ msgstr "Открытие страницы входа в Legendary в брауз
 | 
				
			|||||||
msgid "Failed to open Legendary login page"
 | 
					msgid "Failed to open Legendary login page"
 | 
				
			||||||
msgstr "Не удалось открыть страницу входа в Legendary"
 | 
					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"
 | 
					msgid "Confirm Reset"
 | 
				
			||||||
msgstr "Подтвердите удаление"
 | 
					msgstr "Подтвердите удаление"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -514,6 +483,33 @@ msgstr "Невозможно запустить игру пока запущен
 | 
				
			|||||||
msgid "Launching"
 | 
					msgid "Launching"
 | 
				
			||||||
msgstr "Идёт запуск"
 | 
					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"
 | 
					msgid "just now"
 | 
				
			||||||
msgstr "только что"
 | 
					msgstr "только что"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ from portprotonqt.game_card import GameCard
 | 
				
			|||||||
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
 | 
					from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
 | 
				
			||||||
from portprotonqt.input_manager import InputManager
 | 
					from portprotonqt.input_manager import InputManager
 | 
				
			||||||
from portprotonqt.context_menu_manager import ContextMenuManager
 | 
					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.image_utils import load_pixmap_async, round_corners, ImageCarousel
 | 
				
			||||||
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
 | 
					from portprotonqt.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
 | 
					                self.update_status_message.emit
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        elif display_filter == "favorites":
 | 
					        elif display_filter == "favorites":
 | 
				
			||||||
            def on_all_games(portproton_games, steam_games, epic_games):
 | 
					            def on_all_games(portproton_games, steam_games):
 | 
				
			||||||
                games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
 | 
					                games = [game for game in portproton_games + steam_games if game[0] in favorites]
 | 
				
			||||||
                self.games_loaded.emit(games)
 | 
					                self.games_loaded.emit(games)
 | 
				
			||||||
            self._load_portproton_games_async(
 | 
					            self._load_portproton_games_async(
 | 
				
			||||||
                lambda pg: self._load_steam_games_async(
 | 
					                lambda pg: self._load_steam_games_async(
 | 
				
			||||||
                    lambda sg: load_egs_games_async(
 | 
					                    lambda sg: on_all_games(pg, sg)
 | 
				
			||||||
                        self.legendary_path,
 | 
					 | 
				
			||||||
                        lambda eg: on_all_games(pg, sg, eg),
 | 
					 | 
				
			||||||
                        self.downloader,
 | 
					 | 
				
			||||||
                        self.update_progress.emit,
 | 
					 | 
				
			||||||
                        self.update_status_message.emit
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            def on_all_games(portproton_games, steam_games, epic_games):
 | 
					            def on_all_games(portproton_games, steam_games):
 | 
				
			||||||
                seen = set()
 | 
					                seen = set()
 | 
				
			||||||
                games = []
 | 
					                games = []
 | 
				
			||||||
                for game in portproton_games + steam_games + epic_games:
 | 
					                for game in portproton_games + steam_games:
 | 
				
			||||||
                    name = game[0]
 | 
					                    name = game[0]
 | 
				
			||||||
                    if name not in seen:
 | 
					                    if name not in seen:
 | 
				
			||||||
                        seen.add(name)
 | 
					                        seen.add(name)
 | 
				
			||||||
@@ -285,13 +280,7 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
                self.games_loaded.emit(games)
 | 
					                self.games_loaded.emit(games)
 | 
				
			||||||
            self._load_portproton_games_async(
 | 
					            self._load_portproton_games_async(
 | 
				
			||||||
                lambda pg: self._load_steam_games_async(
 | 
					                lambda pg: self._load_steam_games_async(
 | 
				
			||||||
                    lambda sg: load_egs_games_async(
 | 
					                    lambda sg: on_all_games(pg, sg)
 | 
				
			||||||
                        self.legendary_path,
 | 
					 | 
				
			||||||
                        lambda eg: on_all_games(pg, sg, eg),
 | 
					 | 
				
			||||||
                        self.downloader,
 | 
					 | 
				
			||||||
                        self.update_progress.emit,
 | 
					 | 
				
			||||||
                        self.update_status_message.emit
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        return []
 | 
					        return []
 | 
				
			||||||
@@ -500,6 +489,11 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
            btn.setChecked(i == index)
 | 
					            btn.setChecked(i == index)
 | 
				
			||||||
        self.stackedWidget.setCurrentIndex(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]:
 | 
					    def createSearchWidget(self) -> tuple[QWidget, QLineEdit]:
 | 
				
			||||||
        self.container = QWidget()
 | 
					        self.container = QWidget()
 | 
				
			||||||
        self.container.setStyleSheet(self.theme.CONTAINER_STYLE)
 | 
					        self.container.setStyleSheet(self.theme.CONTAINER_STYLE)
 | 
				
			||||||
@@ -539,6 +533,12 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
    def startSearchDebounce(self, text):
 | 
					    def startSearchDebounce(self, text):
 | 
				
			||||||
        self.searchDebounceTimer.start()
 | 
					        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):
 | 
					    def filterGamesDelayed(self):
 | 
				
			||||||
        """Filters games based on search text and updates the grid."""
 | 
					        """Filters games based on search text and updates the grid."""
 | 
				
			||||||
        text = self.searchEdit.text().strip().lower()
 | 
					        text = self.searchEdit.text().strip().lower()
 | 
				
			||||||
@@ -579,33 +579,16 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
        self.sizeSlider.setFixedWidth(150)
 | 
					        self.sizeSlider.setFixedWidth(150)
 | 
				
			||||||
        self.sizeSlider.setToolTip(f"{self.card_width} px")
 | 
					        self.sizeSlider.setToolTip(f"{self.card_width} px")
 | 
				
			||||||
        self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
 | 
					        self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
 | 
				
			||||||
 | 
					        self.sizeSlider.valueChanged.connect(self.on_slider_value_changed)
 | 
				
			||||||
        sliderLayout.addWidget(self.sizeSlider)
 | 
					        sliderLayout.addWidget(self.sizeSlider)
 | 
				
			||||||
        layout.addLayout(sliderLayout)
 | 
					        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():
 | 
					        def calculate_card_width():
 | 
				
			||||||
            available_width = scrollArea.width() - 20
 | 
					            available_width = scrollArea.width() - 20
 | 
				
			||||||
            spacing = self.gamesListLayout._spacing
 | 
					            spacing = self.gamesListLayout._spacing
 | 
				
			||||||
            target_cards_per_row = 8
 | 
					            target_cards_per_row = 8
 | 
				
			||||||
            calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
 | 
					            calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
 | 
				
			||||||
            calculated_width = max(200, min(calculated_width, 250))
 | 
					            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)
 | 
					        QTimer.singleShot(0, calculate_card_width)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -621,7 +604,6 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
            self._last_width = self.width()
 | 
					            self._last_width = self.width()
 | 
				
			||||||
        if abs(self.width() - self._last_width) > 10:
 | 
					        if abs(self.width() - self._last_width) > 10:
 | 
				
			||||||
            self._last_width = self.width()
 | 
					            self._last_width = self.width()
 | 
				
			||||||
            self.sliderDebounceTimer.start()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def loadVisibleImages(self):
 | 
					    def loadVisibleImages(self):
 | 
				
			||||||
        visible_region = self.gamesListWidget.visibleRegion()
 | 
					        visible_region = self.gamesListWidget.visibleRegion()
 | 
				
			||||||
@@ -742,6 +724,7 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialog = AddGameDialog(self, self.theme)
 | 
					        dialog = AddGameDialog(self, self.theme)
 | 
				
			||||||
 | 
					        dialog.setFocus(Qt.FocusReason.OtherFocusReason)
 | 
				
			||||||
        self.current_add_game_dialog = dialog  # Сохраняем ссылку на диалог
 | 
					        self.current_add_game_dialog = dialog  # Сохраняем ссылку на диалог
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Предзаполняем путь к .exe при drag-and-drop
 | 
					        # Предзаполняем путь к .exe при drag-and-drop
 | 
				
			||||||
@@ -920,7 +903,7 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # 3. Games display_filter
 | 
					        # 3. Games display_filter
 | 
				
			||||||
        self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
 | 
					        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 = QComboBox()
 | 
				
			||||||
        self.gamesDisplayCombo.addItems(self.filter_labels)
 | 
					        self.gamesDisplayCombo.addItems(self.filter_labels)
 | 
				
			||||||
        self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
 | 
					        self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
 | 
				
			||||||
@@ -989,37 +972,6 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
        self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
 | 
					        self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
 | 
				
			||||||
        formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
 | 
					        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)
 | 
					        layout.addLayout(formLayout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Кнопки
 | 
					        # Кнопки
 | 
				
			||||||
@@ -1070,37 +1022,6 @@ class MainWindow(QMainWindow):
 | 
				
			|||||||
            logger.error(f"Failed to open Legendary login page: {e}")
 | 
					            logger.error(f"Failed to open Legendary login page: {e}")
 | 
				
			||||||
            self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
 | 
					            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):
 | 
					    def resetSettings(self):
 | 
				
			||||||
        """Сбрасывает настройки и перезапускает приложение."""
 | 
					        """Сбрасывает настройки и перезапускает приложение."""
 | 
				
			||||||
        reply = QMessageBox.question(
 | 
					        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