diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6472f4..1de1388 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,50 +7,57 @@
### Added
- Кнопки сброса настроек и очистки кэша
-- Начальная интеграция с EGS с помощью [Legendary](https://github.com/derrod/legendary)
-- Бейдж EGS
- Бейдж PortProton
-- Зависимость на `xdg-utils`
+- Зависимость от `xdg-utils`
- Интеграция статуса WeAntiCheatYet в карточку
-- Стили в AddGameDialog
-- Переключение полноэкранного режима через F11 или Select на геймпаде
-- Выбор QCheckBox через Enter или кнопку A геймпада
-- Закрытие диалога добавления игры через ESC или кнопку B геймпада
+- Стили в AddGameDialog
+- Переключение полноэкранного режима через F11 или кнопку Select на геймпаде
+- Выбор QCheckBox через Enter или кнопку A на геймпаде
+- Закрытие диалога добавления игры через ESC или кнопку B на геймпаде
- Закрытие окна приложения по комбинации клавиш Ctrl+Q
-- Сохранение и восстановление размера при рестарте
+- Сохранение и восстановление размера окна при перезапуске
- Переключатель полноэкранного режима приложения
-- Пункт в контекстное меню “Открыть папку игры”
-- Пункт в контекстное меню “Добавить в Steam”
-- Пункт в контекстное меню "Удалить из Steam”
-- Метод сортировки сначала избранное
-- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
+- Пункт в контекстном меню «Открыть папку игры»
+- Пункты в контекстном меню «Добавить в Steam» и «Удалить из Steam»
+- Пункты в контекстном меню «Добавить в Избранное» и «Удалить из Избранного» для переключения статуса избранного через геймпад
+- Метод сортировки «Сначала избранное»
+- Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена)
+- Обработчики для QMenu и QComboBox при управлении геймпадом
+- Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме
+- Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или между сессиями
+- [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
+- Мапинги управления для Dualshock 4 и DualSense
+- Настройка тактильной обратной связи на геймпаде при запуске игры (по умолчанию отключена)
### Changed
- Обновлены все иконки
-- Переименован `_get_steam_home` → `get_steam_home`
-- Переименован `steam_game` → `game_source`
-- Догика контекстного меню вынесена в `ContextMenuManager`
+- Переименована функция `_get_steam_home` в `get_steam_home`
+- Переименован `steam_game` в `game_source`
+- Логика контекстного меню вынесена в `ContextMenuManager`
- Бейдж Steam теперь открывает Steam Community
- Изменена лицензия с MIT на GPL-3.0 для совместимости с кодом от legendary
-- Оптимизирована генерация карточек для предотвращения лагов при поиске и изменения размера окна
-- Бейджи с карточек так же теперь дублируются и на странице с деталями, а не только в библиотеке
-- Установка ширины бейджа в две трети ширины карточки
+- Оптимизирована генерация карточек для предотвращения задержек при поиске и изменении размера окна
+- Бейджи с карточек теперь отображаются также на странице с деталями, а не только в библиотеке
+- Установлена ширина бейджа в две трети ширины карточки
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
-- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку
-- D-pad больше не переключает вкладки только RB и LB
+- Карточки теперь фокусируются в направлении движения стрелок или D-pad: например, при нажатии D-pad вниз фокус переходит на карточку в следующей колонке, а не по порядку
+- Теперь D-pad можно зажимать для переключения карточек
+- D-pad больше не переключает вкладки, только RB и LB
- Кнопка добавления игры больше не фокусируется
+- Диалог добавления игры теперь открывается только в библиотеке
+- Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt
### Fixed
-- Обработка несуществующей темы с возвратом к “standart”
+- Обработка несуществующей темы с возвратом к «standard»
- Открытие контекстного меню
- Запуск при отсутствии exiftool
- Переводы пунктов настроек
-- Бесконечное обращение к get_portproton_location
+- Бесконечное обращение к `get_portproton_location`
- Ссылки на документацию в README
-- traceback при загрузке placeholder при отсутствии обложек
+- Traceback при загрузке placeholder при отсутствии обложек
- Утечки памяти при загрузке обложек
- Ошибки при подключении геймпада из-за работы в разных потоках
-- Множественное открытие диалога добавления игры на геймпаде
+- Многократное открытие диалога добавления игры при использовании геймпада
- Перехват событий геймпада во время работы игры
---
@@ -64,16 +71,15 @@
- Сборка AppImage
### Changed
-- Удалён жёстко заданный ресайз окна
-- Использован icoextract как python модуль
+- Удалён жёстко заданный размер окна
+- Использован `icoextract` как Python-модуль
### Fixed
- Скрытие статус-бара
- Чтение списка Steam-игр
-- Подвисание GUI
-- Краш при повреждённом Steam
+- Зависание GUI
+- Сбой при повреждённом Steam
---
-
> См. подробности по каждому коммиту в истории репозитория.
diff --git a/README.md b/README.md
index 87e3dcb..b9e5c25 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,73 @@
-

+
PortProtonQt
-
Современный, удобный графический интерфейс, написанный с использованием PySide6(Qt6) и предназначенный для упрощения управления и запуска игр на различных платформах, включая PortProton, Steam и Epic Games Store.
+
Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.
+
## В планах
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
-- [ ] Добавить возможность управление с геймпада
-- [ ] Добавить возможность управление с тачскрина
-- [X] Добавить возможность управление с мыши и клавиатуры
+- [X] Добавить возможность управления с геймпада
+- [ ] Добавить возможность управления с тачскрина
+- [X] Добавить возможность управления с мыши и клавиатуры
- [X] Добавить систему тем [Документация](documentation/theme_guide)
-- [X] Вынести все константы такие как уровень закругления карточек в темы (Частично вынесено)
-- [X] Добавить метадату для тем (скришоты, описание, домащняя страница и автор)
-- [ ] Продумать систему вкладок вместо той что есть сейчас
-- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
-- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
-- [X] Брать описание и названия игр с базы данных Steam
-- [X] Брать обложки для игр со SteamGridDB или CDN Steam
-- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска
-- [X] Улучшить функцию поиска SteamApi что бы исправить некорректное определение ID (Graven определается как ENGRAVEN или GRAVENFALL, Spore определается как SporeBound или Spore Valley)
-- [ ] Убрать логи со SteamApi в релизной версии потому что логи замедляют код
-- [X] Что-то придумать с ограничением SteamApi в 50 тысяч игр за один запрос (иногда туда не попадают нужные игры и остаются без обложки)
-- [X] Избавится от любого вызова yad
-- [X] Написать свою реализацию запрета ухода в сон, а не использовать ту что в PortProton (Оставим это [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
-- [X] Написать свою реализацию трея, а не использовать ту что в PortProton
-- [X] Добавить в поиск экранную клавиатуру (Реализовавывать собственную клавиатуру слишком затратно, лучше положится на встроенную в DE клавиатуру malit в KDE, gjs-osk в GNOME,Squeekboard в phosh, стимовская в SteamOS и так далее)
-- [X] Добавить сортировку карточек по различным критериям (сейчас есть: недавние, кол-во наиграного времени, избранное или по алфавиту)
+- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
+- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор)
+- [ ] Продумать систему вкладок вместо текущей
+- [X] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
+- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
+- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
+- [X] Получать описания и названия игр из базы данных Steam
+- [X] Получать обложки для игр из SteamGridDB или CDN Steam
+- [X] Оптимизировать работу со Steam API для ускорения времени запуска
+- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
+- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
+- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
+- [X] Избавиться от вызовов yad
+- [X] Реализовать собственный механизм запрета ухода в спящий режим вместо использования механизма PortProton (оставлено для [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
+- [X] Реализовать собственный системный трей вместо использования трея PortProton
+- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
+- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
- [X] Добавить индикацию запуска приложения
-- [X] Достичь паритета функционала с Ingame
-- [ ] Достичь паритета функционала с PortProton
-- [X] Добавить возможность изменения названия, описания и обложки через файлы .local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}
-- [X] Добавить встроенное переопределение имени, описания и обложки, например по пути portprotonqt/custom_data [Документация](documentation/metadata_override/)
-- [X] Добавить в карточку игры сведения о поддержке геймадов
+- [X] Достигнуть паритета функциональности с Ingame
+- [ ] Достигнуть паритета функциональности с PortProton
+- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
+- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
+- [X] Добавить в карточку игры сведения о поддержке геймпада
- [X] Добавить в карточки данные с ProtonDB
-- [X] Добавить в карточки данные с Are We Anti-Cheat Yet?
-- [X] Продублировать бейджы с карточки на страницу с деталями игрыы
-- [X] Добавить парсинг ярлыков со Steam
-- [X] Добавить парсинг ярлыков с EGS
-- [ ] Избавится от бинарника legendary
-- [ ] Добавить запуск и скачивание игр с EGS
-- [ ] Добавить авторизацию в EGS через WebView, а не вручную
-- [X] Брать описания для игр с EGS из их [api](https://store-content.ak.epicgames.com/api)
-- [X] Брать slug через Graphql [запрос](https://launcher.store.epicgames.com/graphql)
-- [X] Добавить на карточку бейдж того что игра со стима
-- [X] Добавить поддержку Flatpak и Snap версии Steam
-- [X] Выводить данные о самом недавнем пользователе Steam, а не первом попавшемся
-- [X] Исправить склонения в детальном выводе времени, например не 3 часов назад, а 3 часа назад
+- [X] Добавить в карточки данные с AreWeAntiCheatYet
+- [X] Продублировать бейджи с карточки на страницу с деталями игры
+- [X] Добавить парсинг ярлыков из Steam
+- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки)
+- [ ] Избавиться от бинарника legendary
+- [ ] Добавить запуск и скачивание игр из EGS
+- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода
+- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api)
+- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
+- [X] Добавить на карточку бейдж, указывающий, что игра из Steam
+- [X] Добавить поддержку версий Steam для Flatpak и Snap
+- [X] Отображать данные о самом последнем пользователе Steam, а не первом попавшемся
+- [X] Исправить склонения в детальном выводе времени, например, не «3 часов назад», а «3 часа назад»
- [X] Добавить перевод через gettext [Документация](documentation/localization_guide)
-- [X] Писать описание игр и прочие данные на языке системы
-- [X] Добавить недокументированные параметры конфигурации в GUI (time detail_level, games sort_method, games display_filter)
-- [X] Добавить систему избранного к карточкам
-- [X] Заменить все print на logging
-- [ ] Привести все логи к одному языку
-- [X] Стилизовать все элементы без стилей(QMessageBox, QSlider, QDialog)
-- [X] Убрать жёсткую привязку путей на стрелочки QComboBox в styles.py
+- [X] Отображать описания игр и другие данные на языке системы
+- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
+- [X] Добавить систему избранного для карточек
+- [X] Заменить все `print` на `logging`
+- [ ] Привести все логи к единому языку
+- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
+- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
- [X] Исправить частичное применение тем на лету
-- [X] Исправить наложение подписей скриншотов при первом перелистывание в полноэкранном режиме
-- [ ] Добавить GOG (?)
-- [ ] Определится уже наконец с названием (PortProtonQt или PortProtonQT)
+- [X] Исправить наложение подписей скриншотов при первом перелистывании в полноэкранном режиме
+- [ ] Добавить поддержку GOG (?)
+- [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант)
+- [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?)
+- [X] Добавить виброотдачу на геймпаде при запуске игры
+- [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
+- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
+- [ ] Скопировать логику управления с D-pad на стрелки с клавиатуры
-### Установка (debug)
+### Установка (devel)
```sh
uv python install 3.10
@@ -70,6 +77,12 @@ source .venv/bin/activate
Запуск производится по команде portprotonqt
+### Установка (release)
+
+Выберите подходящий пакет для вашей системы или AppImage.
+
+Запуск производится по команде portprotonqt или по ярлыку в меню
+
### Разработка
В проект встроен линтер (ruff), статический анализатор (pyright) и проверка lock файла, если эти проверки не пройдут PR не будет принят, поэтому перед коммитом введите такую команду
@@ -89,9 +102,9 @@ pre-commit run --all-files
## Авторы
-* [Boria138](https://github.com/Boria138) - Программист
+* [Boria138](https://git.linux-gaming.ru/Boria138) - Программист
* [BlackSnaker](https://github.com/BlackSnaker) - Дизайнер - программист
-* [Mikhail Tergoev(Castro-Fidel)](https://github.com/Castro-Fidel) - Автор оригинального проекта PortProton
+* [Mikhail Tergoev(Castro-Fidel)](https://git.linux-gaming.ru/CastroFidel) - Автор оригинального проекта PortProton
> [!WARNING]
> Проект находится на стадии WIP (work in progress) корректная работоспособность не гарантирована
diff --git a/build-aux/fedora-git.spec b/build-aux/fedora-git.spec
index 3eace5a..feb0d77 100644
--- a/build-aux/fedora-git.spec
+++ b/build-aux/fedora-git.spec
@@ -45,7 +45,7 @@ Requires: perl-Image-ExifTool
Requires: xdg-utils
%description -n python3-%{pypi_name}-git
-PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store.
+This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
%prep
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
@@ -62,6 +62,8 @@ cp -r build-aux/share %{buildroot}/usr/
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
%{_bindir}/%{pypi_name}
-%{_datadir}/*
+%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
+%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
+%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%changelog
diff --git a/build-aux/fedora.spec b/build-aux/fedora.spec
index 24c5f1c..ba26d25 100644
--- a/build-aux/fedora.spec
+++ b/build-aux/fedora.spec
@@ -42,7 +42,7 @@ Requires: perl-Image-ExifTool
Requires: xdg-utils
%description -n python3-%{pypi_name}
-PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store.
+This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
%prep
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt
@@ -61,6 +61,8 @@ cp -r build-aux/share %{buildroot}/usr/
%files -n python3-%{pypi_name} -f %{pyproject_files}
%{_bindir}/%{pypi_name}
-%{_datadir}/*
+%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
+%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
+%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%changelog
diff --git a/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml b/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml
new file mode 100644
index 0000000..a1eedb9
--- /dev/null
+++ b/build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml
@@ -0,0 +1,65 @@
+
+
+ PortProtonQt
+ ru.linux_gaming.PortProtonQt
+ CC0-1.0
+ GPL-3.0-or-later
+ Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store
+ Современный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store
+
+
+ This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
+
+ Это приложение предоставляет стильный и интуитивно понятный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет ваши игровые библиотеки в одном удобном хабе для простой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают целостный игровой опыт, устраняя необходимость в использовании нескольких лаунчеров. Уникальная интеграция с PortProton улучшает игровой процесс на Linux, позволяя с лёгкостью запускать Windows-игры с минимальными настройками.
+
+
+ ru.linux_gaming.PortProtonQt.desktop
+
+ Boria138
+
+
+ keyboard
+ pointing
+ touch
+ gamepad
+
+
+ #007AFF
+ #09BEC8
+
+
+ Game
+ Utility
+
+ https://git.linux-gaming.ru/Boria138/PortProtonQt
+ https://git.linux-gaming.ru/Boria138/PortProtonQt/issues
+
+
+ https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%91%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B0.png
+ Library
+ Библиотека
+
+
+ https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9A%D0%B0%D1%80%D1%82%D0%BE%D1%87%D0%BA%D0%B0.png
+ Card detail page
+ Детали игры
+
+
+ https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8.png
+ Settings
+ Настройки
+
+
+
+ wine
+ proton
+ steam
+ windows
+ epic games store
+ egs
+ qt
+ portproton
+ games
+
+
+
diff --git a/dev-scripts/l10n.py b/dev-scripts/l10n.py
index 4acbb5b..6a9ff5c 100755
--- a/dev-scripts/l10n.py
+++ b/dev-scripts/l10n.py
@@ -106,7 +106,7 @@ def compile_locales() -> None:
def extract_strings() -> None:
input_dir = (Path(__file__).parent.parent / "portprotonqt").resolve()
CommandLineInterface().run([
- "pybabel", "extract", "--project=PortProtonQT",
+ "pybabel", "extract", "--project=PortProtonQt",
f"--version={_get_version()}",
"--strip-comment-tag",
"--no-location",
@@ -231,7 +231,7 @@ def main(args) -> int:
return 0
if __name__ == "__main__":
- parser = argparse.ArgumentParser(prog="l10n", description="Localization utility for PortProtonQT.")
+ parser = argparse.ArgumentParser(prog="l10n", description="Localization utility for PortProtonQt.")
parser.add_argument("--create-new", nargs='+', type=str, default=False, help="Create .po for new locales")
parser.add_argument("--update-all", action='store_true', help="Extract/update locales and update README coverage")
parser.add_argument("--spellcheck", action='store_true', help="Run spellcheck on POT and PO files")
diff --git a/documentation/localization_guide/README.md b/documentation/localization_guide/README.md
index 84fdcc5..08dd168 100644
--- a/documentation/localization_guide/README.md
+++ b/documentation/localization_guide/README.md
@@ -20,9 +20,9 @@ Current translation status:
| Locale | Progress | Translated |
| :----- | -------: | ---------: |
-| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 154 |
-| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 154 |
-| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 of 154 |
+| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 162 |
+| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 162 |
+| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 of 162 |
---
diff --git a/documentation/localization_guide/README.ru.md b/documentation/localization_guide/README.ru.md
index eb4ccca..a8efde8 100644
--- a/documentation/localization_guide/README.ru.md
+++ b/documentation/localization_guide/README.ru.md
@@ -20,9 +20,9 @@
| Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: |
-| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 154 |
-| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 154 |
-| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 из 154 |
+| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
+| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
+| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 из 162 |
---
diff --git a/portprotonqt/app.py b/portprotonqt/app.py
index fdd2bbf..d215643 100644
--- a/portprotonqt/app.py
+++ b/portprotonqt/app.py
@@ -4,8 +4,9 @@ from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon
from portprotonqt.main_window import MainWindow
from portprotonqt.tray import SystemTray
-from portprotonqt.config_utils import read_theme_from_config
+from portprotonqt.config_utils import read_theme_from_config, save_fullscreen_config
from portprotonqt.logger import get_logger
+from portprotonqt.cli import parse_args
logger = get_logger(__name__)
@@ -28,7 +29,17 @@ def main():
else:
logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}")
+ # Парсинг аргументов командной строки
+ args = parse_args()
+
window = MainWindow()
+
+ # Обработка флага --fullscreen
+ if args.fullscreen:
+ logger.info("Запуск в полноэкранном режиме по флагу --fullscreen")
+ save_fullscreen_config(True)
+ window.showFullScreen()
+
current_theme_name = read_theme_from_config()
tray = SystemTray(app, current_theme_name)
tray.show_action.triggered.connect(window.show)
@@ -43,7 +54,9 @@ def main():
tray.hide_action.triggered.connect(window.hide)
window.settings_saved.connect(recreate_tray)
+
window.show()
+
sys.exit(app.exec())
if __name__ == '__main__':
diff --git a/portprotonqt/cli.py b/portprotonqt/cli.py
new file mode 100644
index 0000000..f781dfc
--- /dev/null
+++ b/portprotonqt/cli.py
@@ -0,0 +1,16 @@
+import argparse
+from portprotonqt.logger import get_logger
+
+logger = get_logger(__name__)
+
+def parse_args():
+ """
+ Парсит аргументы командной строки.
+ """
+ parser = argparse.ArgumentParser(description="PortProtonQt CLI")
+ parser.add_argument(
+ "--fullscreen",
+ action="store_true",
+ help="Запустить приложение в полноэкранном режиме и сохранить эту настройку"
+ )
+ return parser.parse_args()
diff --git a/portprotonqt/config_utils.py b/portprotonqt/config_utils.py
index 3c9ca86..2066d2c 100644
--- a/portprotonqt/config_utils.py
+++ b/portprotonqt/config_utils.py
@@ -10,7 +10,7 @@ _portproton_location = None
# Пути к конфигурационным файлам
CONFIG_FILE = os.path.join(
os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")),
- "PortProtonQT.conf"
+ "PortProtonQt.conf"
)
PORTPROTON_CONFIG_FILE = os.path.join(
@@ -21,7 +21,7 @@ PORTPROTON_CONFIG_FILE = os.path.join(
# Пути к папкам с темами
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
THEMES_DIRS = [
- os.path.join(xdg_data_home, "PortProtonQT", "themes"),
+ os.path.join(xdg_data_home, "PortProtonQt", "themes"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
]
@@ -322,6 +322,41 @@ def save_favorites(favorites):
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
+def read_rumble_config():
+ """
+ Читает настройку виброотдачи геймпада из секции [Gamepad].
+ Если параметр отсутствует, сохраняет и возвращает False по умолчанию.
+ """
+ cp = configparser.ConfigParser()
+ if os.path.exists(CONFIG_FILE):
+ try:
+ cp.read(CONFIG_FILE, encoding="utf-8")
+ except Exception as e:
+ logger.error("Ошибка чтения конфигурационного файла: %s", e)
+ save_rumble_config(False)
+ return False
+ if not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "rumble_enabled"):
+ save_rumble_config(False)
+ return False
+ return cp.getboolean("Gamepad", "rumble_enabled", fallback=False)
+ return False
+
+def save_rumble_config(rumble_enabled):
+ """
+ Сохраняет настройку виброотдачи геймпада в секцию [Gamepad].
+ """
+ cp = configparser.ConfigParser()
+ if os.path.exists(CONFIG_FILE):
+ try:
+ cp.read(CONFIG_FILE, encoding="utf-8")
+ except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
+ logger.error("Ошибка чтения конфигурационного файла: %s", e)
+ if "Gamepad" not in cp:
+ cp["Gamepad"] = {}
+ cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
+ with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
+ cp.write(configfile)
+
def ensure_default_proxy_config():
"""
Проверяет наличие секции [Proxy] в конфигурационном файле.
@@ -342,7 +377,6 @@ def ensure_default_proxy_config():
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
-
def read_proxy_config():
"""
Читает настройки прокси из секции [Proxy] конфигурационного файла.
@@ -421,8 +455,6 @@ def save_fullscreen_config(fullscreen):
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
-
-
def read_window_geometry() -> tuple[int, int]:
"""
Читает ширину и высоту окна из секции [MainWindow] конфигурационного файла.
@@ -472,14 +504,14 @@ def reset_config():
def clear_cache():
"""
- Очищает кэш PortProtonQT, удаляя папку кэша.
+ Очищает кэш PortProtonQt, удаляя папку кэша.
"""
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
- cache_dir = os.path.join(xdg_cache_home, "PortProtonQT")
+ cache_dir = os.path.join(xdg_cache_home, "PortProtonQt")
if os.path.exists(cache_dir):
try:
shutil.rmtree(cache_dir)
- logger.info("Кэш PortProtonQT удалён: %s", cache_dir)
+ logger.info("Кэш PortProtonQt удалён: %s", cache_dir)
except Exception as e:
logger.error("Ошибка при удалении кэша: %s", e)
diff --git a/portprotonqt/context_menu_manager.py b/portprotonqt/context_menu_manager.py
index 6d9eca2..4e5772b 100644
--- a/portprotonqt/context_menu_manager.py
+++ b/portprotonqt/context_menu_manager.py
@@ -6,12 +6,13 @@ import subprocess
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu
from PySide6.QtCore import QUrl, QPoint
from PySide6.QtGui import QDesktopServices
-from portprotonqt.config_utils import parse_desktop_entry
+from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
from portprotonqt.localization import _
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
+from portprotonqt.dialogs import AddGameDialog
class ContextMenuManager:
- """Manages context menu actions for game management in PortProtonQT."""
+ """Manages context menu actions for game management in PortProtonQt."""
def __init__(self, parent, portproton_location, theme, load_games_callback, update_game_grid_callback):
"""
@@ -40,6 +41,18 @@ class ContextMenuManager:
"""
menu = QMenu(self.parent)
+ menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
+
+ favorites = read_favorites()
+ is_favorite = game_card.name in favorites
+
+ if is_favorite:
+ favorite_action = menu.addAction(_("Remove from Favorites"))
+ favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, False))
+ else:
+ favorite_action = menu.addAction(_("Add to Favorites"))
+ favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True))
+
if game_card.game_source not in ("steam", "epic"):
desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip()
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
@@ -79,6 +92,26 @@ class ContextMenuManager:
menu.exec(game_card.mapToGlobal(pos))
+ def toggle_favorite(self, game_card, add: bool):
+ """
+ Toggle the favorite status of a game and update its icon.
+
+ Args:
+ game_card: The GameCard instance to toggle.
+ add: True to add to favorites, False to remove.
+ """
+ favorites = read_favorites()
+ if add and game_card.name not in favorites:
+ favorites.append(game_card.name)
+ game_card.is_favorite = True
+ self.parent.statusBar().showMessage(_("Added '{0}' to favorites").format(game_card.name), 3000)
+ elif not add and game_card.name in favorites:
+ favorites.remove(game_card.name)
+ game_card.is_favorite = False
+ self.parent.statusBar().showMessage(_("Removed '{0}' from favorites").format(game_card.name), 3000)
+ save_favorites(favorites)
+ game_card.update_favorite_icon()
+
def _check_portproton(self):
"""Check if PortProton is available."""
if self.portproton_location is None:
@@ -225,7 +258,7 @@ class ContextMenuManager:
"XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share")
)
- custom_folder = os.path.join(xdg_data_home, "PortProtonQT", "custom_data", exe_name)
+ custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name)
if os.path.exists(custom_folder):
try:
shutil.rmtree(custom_folder)
@@ -321,7 +354,6 @@ class ContextMenuManager:
def edit_game_shortcut(self, game_name, exec_line, cover_path):
"""Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
- from portprotonqt.dialogs import AddGameDialog # Local import to avoid circular dependency
if not self._check_portproton():
return
@@ -385,7 +417,7 @@ class ContextMenuManager:
"XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share")
)
- custom_folder = os.path.join(xdg_data_home, "PortProtonQT", "custom_data", exe_name)
+ custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name)
os.makedirs(custom_folder, exist_ok=True)
ext = os.path.splitext(new_cover_path)[1].lower()
diff --git a/portprotonqt/downloader.py b/portprotonqt/downloader.py
index 8118b39..0e8e610 100644
--- a/portprotonqt/downloader.py
+++ b/portprotonqt/downloader.py
@@ -303,7 +303,7 @@ class Downloader(QObject):
local_path = os.path.join(
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
- "PortProtonQT", "legendary_cache", "legendary"
+ "PortProtonQt", "legendary_cache", "legendary"
)
logger.info(f"Downloading legendary binary version {version} from {binary_url} to {local_path}")
diff --git a/portprotonqt/egs_api.py b/portprotonqt/egs_api.py
index a80a295..8f12c3f 100644
--- a/portprotonqt/egs_api.py
+++ b/portprotonqt/egs_api.py
@@ -22,7 +22,7 @@ def get_cache_dir() -> Path:
"XDG_CACHE_HOME",
os.path.join(os.path.expanduser("~"), ".cache")
)
- cache_dir = Path(xdg_cache_home) / "PortProtonQT"
+ cache_dir = Path(xdg_cache_home) / "PortProtonQt"
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir
@@ -36,7 +36,7 @@ def get_egs_game_description_async(
Asynchronously fetches the game description from the Epic Games Store API.
Prioritizes GraphQL API with namespace for slug and description.
Falls back to legacy API if GraphQL provides a slug but no description.
- Caches results in ~/.cache/PortProtonQT/egs_app_{app_name}.json.
+ Caches results in ~/.cache/PortProtonQt/egs_app_{app_name}.json.
Handles DNS resolution failures gracefully.
"""
cache_dir = get_cache_dir()
@@ -423,7 +423,7 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
except Exception as e:
logger.warning("Error processing metadata for %s: %s", app_name, str(e))
- image_folder = os.path.join(os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "PortProtonQT", "images")
+ image_folder = os.path.join(os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), "PortProtonQt", "images")
local_path = os.path.join(image_folder, f"{app_name}.jpg") if cover_url else ""
def on_description_fetched(api_description: str):
diff --git a/portprotonqt/game_card.py b/portprotonqt/game_card.py
index e24e3be..8ad4a20 100644
--- a/portprotonqt/game_card.py
+++ b/portprotonqt/game_card.py
@@ -199,7 +199,7 @@ class GameCard(QFrame):
icon_size=16,
icon_space=3,
)
- self.anticheatLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
+ self.anticheatLabel.setStyleSheet(self.theme.get_anticheat_badge_style(anticheat_status))
self.anticheatLabel.setFixedWidth(int(card_width * 2/3))
anticheat_visible = True
else:
@@ -261,46 +261,45 @@ class GameCard(QFrame):
self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
self.egs_visible = (str(self.game_source).lower() == "epic" and display_filter in ("all", "favorites"))
self.portproton_visible = (str(self.game_source).lower() == "portproton" and display_filter in ("all", "favorites"))
+ protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
+ anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
+ # Обновляем видимость бейджей
self.steamLabel.setVisible(self.steam_visible)
self.egsLabel.setVisible(self.egs_visible)
self.portprotonLabel.setVisible(self.portproton_visible)
+ self.protondbLabel.setVisible(protondb_visible)
+ self.anticheatLabel.setVisible(anticheat_visible)
- # Reposition badges
+ # Подготавливаем список всех бейджей с их текущей видимостью
+ badges = [
+ (self.steam_visible, self.steamLabel),
+ (self.egs_visible, self.egsLabel),
+ (self.portproton_visible, self.portprotonLabel),
+ (protondb_visible, self.protondbLabel),
+ (anticheat_visible, self.anticheatLabel),
+ ]
+
+ # Пересчитываем позиции бейджей
right_margin = 8
badge_spacing = 5
top_y = 10
badge_y_positions = []
badge_width = int(self.coverLabel.width() * 2/3)
- if self.steam_visible:
- steam_x = self.coverLabel.width() - badge_width - right_margin
- self.steamLabel.move(steam_x, top_y)
- badge_y_positions.append(top_y + self.steamLabel.height())
- if self.egs_visible:
- egs_x = self.coverLabel.width() - badge_width - right_margin
- egs_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
- self.egsLabel.move(egs_x, egs_y)
- badge_y_positions.append(egs_y + self.egsLabel.height())
- if self.portproton_visible:
- portproton_x = self.coverLabel.width() - badge_width - right_margin
- portproton_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
- self.portprotonLabel.move(portproton_x, portproton_y)
- badge_y_positions.append(portproton_y + self.portprotonLabel.height())
- if self.protondbLabel.isVisible():
- protondb_x = self.coverLabel.width() - badge_width - right_margin
- protondb_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
- self.protondbLabel.move(protondb_x, protondb_y)
- badge_y_positions.append(protondb_y + self.protondbLabel.height())
- if self.anticheatLabel.isVisible():
- anticheat_x = self.coverLabel.width() - badge_width - right_margin
- anticheat_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
- self.anticheatLabel.move(anticheat_x, anticheat_y)
- self.anticheatLabel.raise_()
- self.protondbLabel.raise_()
- self.portprotonLabel.raise_()
- self.egsLabel.raise_()
- self.steamLabel.raise_()
+ for is_visible, badge in badges:
+ if is_visible:
+ badge_x = self.coverLabel.width() - badge_width - right_margin
+ badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
+ badge.move(badge_x, badge_y)
+ badge_y_positions.append(badge_y + badge.height())
+
+ # Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
+ self.anticheatLabel.raise_()
+ self.protondbLabel.raise_()
+ self.portprotonLabel.raise_()
+ self.egsLabel.raise_()
+ self.steamLabel.raise_()
def _show_context_menu(self, pos):
"""Delegate context menu display to ContextMenuManager."""
diff --git a/portprotonqt/image_utils.py b/portprotonqt/image_utils.py
index 0c2dda5..922f23f 100644
--- a/portprotonqt/image_utils.py
+++ b/portprotonqt/image_utils.py
@@ -35,10 +35,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
y = (scaled.height() - height) // 2
cropped = scaled.copy(x, y, width, height)
callback(cropped)
- # Removed: pixmap = None (unnecessary, causes type error)
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
- image_folder = os.path.join(xdg_cache_home, "PortProtonQT", "images")
+ image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
os.makedirs(image_folder, exist_ok=True)
if cover and cover.startswith("https://steamcdn-a.akamaihd.net/steam/apps/"):
diff --git a/portprotonqt/input_manager.py b/portprotonqt/input_manager.py
index daaafbc..9713485 100644
--- a/portprotonqt/input_manager.py
+++ b/portprotonqt/input_manager.py
@@ -1,16 +1,16 @@
import time
import threading
from typing import Protocol, cast
-from evdev import InputDevice, ecodes, list_devices
-import pyudev
-from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog
-from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot
+from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
+from pyudev import Context, Monitor, MonitorObserver, Device
+from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox
+from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
from PySide6.QtGui import QKeyEvent
from portprotonqt.logger import get_logger
from portprotonqt.image_utils import FullscreenDialog
from portprotonqt.custom_widgets import NavLabel
from portprotonqt.game_card import GameCard
-from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad
+from portprotonqt.config_utils import read_fullscreen_config, read_window_geometry, save_window_geometry, read_auto_fullscreen_gamepad, read_rumble_config
logger = get_logger(__name__)
@@ -25,6 +25,8 @@ class MainWindowProtocol(Protocol):
...
def toggleGame(self, exec_line: str | None, button: QWidget | None = None) -> None:
...
+ def openSystemOverlay(self) -> None:
+ ...
stackedWidget: QStackedWidget
tabButtons: dict[int, QWidget]
gamesListWidget: QWidget
@@ -32,24 +34,25 @@ class MainWindowProtocol(Protocol):
current_exec_line: str | None
current_add_game_dialog: QDialog | None
-# Mapping of actions to evdev button codes, includes PlayStation, Xbox, and Switch controllers
+# Mapping of actions to evdev button codes, includes Xbox and Playstation controllers
+# https://github.com/torvalds/linux/blob/master/drivers/hid/hid-playstation.c
+# https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c
BUTTONS = {
- 'confirm': {ecodes.BTN_A},
- 'back': {ecodes.BTN_B},
- 'add_game': {ecodes.BTN_Y},
- 'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TRIGGER_HAPPY7},
- 'next_tab': {ecodes.BTN_TR, ecodes.BTN_TRIGGER_HAPPY5},
- 'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
- 'context_menu': {ecodes.BTN_START},
- 'menu': {ecodes.BTN_SELECT},
+ 'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, # A / Cross
+ 'back': {ecodes.BTN_B, ecodes.BTN_EAST}, # B / Circle
+ 'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, # Y / Triangle
+ 'prev_tab': {ecodes.BTN_TL}, # LB / L1
+ 'next_tab': {ecodes.BTN_TR}, # RB / R1
+ 'context_menu': {ecodes.BTN_START}, # Start / Options
+ 'menu': {ecodes.BTN_SELECT}, # Select / Share
+ 'guide': {ecodes.BTN_MODE}, # Xbox / PS Home
}
class InputManager(QObject):
"""
Manages input from gamepads and keyboards for navigating the application interface.
Supports gamepad hotplugging, button and axis events, and keyboard event filtering
- for seamless UI interaction. Enables fullscreen mode when a gamepad is connected
- and restores normal mode when disconnected.
+ for seamless UI interaction.
"""
# Signals for gamepad events
button_pressed = Signal(int) # Signal for button presses
@@ -69,7 +72,6 @@ class InputManager(QObject):
self._parent.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None)
self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
-
self.axis_deadzone = axis_deadzone
self.initial_axis_move_delay = initial_axis_move_delay
self.repeat_axis_move_delay = repeat_axis_move_delay
@@ -80,6 +82,13 @@ class InputManager(QObject):
self.gamepad_thread: threading.Thread | None = None
self.running = True
self._is_fullscreen = read_fullscreen_config()
+ self.rumble_effect_id: int | None = None # Store the rumble effect ID
+
+ # Add variables for continuous D-pad movement
+ self.dpad_timer = QTimer(self)
+ self.dpad_timer.timeout.connect(self.handle_dpad_repeat)
+ self.current_dpad_code = None # Tracks the current D-pad axis (e.g., ABS_HAT0X, ABS_HAT0Y)
+ self.current_dpad_value = 0 # Tracks the current D-pad direction value (e.g., -1, 1)
# Connect signals to slots
self.button_pressed.connect(self.handle_button_slot)
@@ -117,6 +126,48 @@ class InputManager(QObject):
except Exception as e:
logger.error(f"Error in handle_fullscreen_slot: {e}", exc_info=True)
+ def trigger_rumble(self, duration_ms: int = 200, strong_magnitude: int = 0x8000, weak_magnitude: int = 0x8000) -> None:
+ """Trigger a rumble effect on the gamepad if supported."""
+ if not read_rumble_config():
+ return
+ if not self.gamepad:
+ return
+ try:
+ # Check if the gamepad supports force feedback
+ caps = self.gamepad.capabilities()
+ if ecodes.EV_FF not in caps or ecodes.FF_RUMBLE not in caps.get(ecodes.EV_FF, []):
+ logger.debug("Gamepad does not support force feedback or rumble")
+ return
+
+ # Create a rumble effect
+ rumble = ff.Rumble(strong_magnitude=strong_magnitude, weak_magnitude=weak_magnitude)
+ effect = ff.Effect(
+ id=-1, # Let evdev assign an ID
+ type=ecodes.FF_RUMBLE,
+ direction=0, # Direction (not used for rumble)
+ replay=ff.Replay(length=duration_ms, delay=0),
+ u=ff.EffectType(ff_rumble_effect=rumble)
+ )
+
+ # Upload the effect
+ self.rumble_effect_id = self.gamepad.upload_effect(effect)
+ # Play the effect
+ event = InputEvent(0, 0, ecodes.EV_FF, self.rumble_effect_id, 1)
+ self.gamepad.write_event(event)
+ # Schedule effect erasure after duration
+ QTimer.singleShot(duration_ms, self.stop_rumble)
+ except Exception as e:
+ logger.error(f"Error triggering rumble: {e}", exc_info=True)
+
+ def stop_rumble(self) -> None:
+ """Stop the rumble effect and clean up."""
+ if self.gamepad and self.rumble_effect_id is not None:
+ try:
+ self.gamepad.erase_effect(self.rumble_effect_id)
+ self.rumble_effect_id = None
+ except Exception as e:
+ logger.error(f"Error stopping rumble: {e}", exc_info=True)
+
@Slot(int)
def handle_button_slot(self, button_code: int) -> None:
try:
@@ -129,10 +180,66 @@ class InputManager(QObject):
return
active = QApplication.activeWindow()
focused = QApplication.focusWidget()
+ popup = QApplication.activePopupWidget()
+
+ # Handle Guide button to open system overlay
+ if button_code in BUTTONS['guide']:
+ if not popup and not isinstance(active, QDialog):
+ self._parent.openSystemOverlay()
+ return
+
+ # Handle QMenu (context menu)
+ if isinstance(popup, QMenu):
+ if button_code in BUTTONS['confirm']:
+ if popup.activeAction():
+ popup.activeAction().trigger()
+ popup.close()
+ return
+ elif button_code in BUTTONS['back']:
+ popup.close()
+ return
+ return
+
+ # Handle QComboBox
+ if isinstance(focused, QComboBox):
+ if button_code in BUTTONS['confirm']:
+ 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']:
+ idx = focused.currentIndex()
+ if idx.isValid():
+ if combo:
+ combo.setCurrentIndex(idx.row())
+ combo.hidePopup()
+ combo.setFocus(Qt.FocusReason.OtherFocusReason)
+ else:
+ focused.activated.emit(idx)
+ focused.clicked.emit(idx)
+ focused.hide()
+ return
+
+ if button_code in BUTTONS['back']:
+ if combo:
+ combo.hidePopup()
+ combo.setFocus(Qt.FocusReason.OtherFocusReason)
+ else:
+ focused.clearSelection()
+ focused.hide()
# Закрытие AddGameDialog на кнопку B
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
- active.reject() # Закрываем диалог
+ active.reject()
return
# FullscreenDialog
@@ -149,22 +256,26 @@ class InputManager(QObject):
if isinstance(focused, GameCard):
if button_code in BUTTONS['context_menu']:
pos = QPoint(focused.width() // 2, focused.height() // 2)
- focused._show_context_menu(pos)
+ menu = focused._show_context_menu(pos)
+ if menu:
+ menu.setFocus(Qt.FocusReason.OtherFocusReason)
return
# Game launch on detail page
- if (button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None:
+ if (button_code in BUTTONS['confirm']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None:
if self._parent.current_exec_line:
+ self.trigger_rumble()
self._parent.toggleGame(self._parent.current_exec_line, None)
return
# Standard navigation
- if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
+ if button_code in BUTTONS['confirm']:
self._parent.activateFocusedWidget()
- elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
+ elif button_code in BUTTONS['back']:
self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
elif button_code in BUTTONS['add_game']:
- self._parent.openAddGameDialog()
+ if self._parent.stackedWidget.currentIndex() == 0:
+ self._parent.openAddGameDialog()
elif button_code in BUTTONS['prev_tab']:
idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
self._parent.switchTab(idx)
@@ -176,6 +287,14 @@ class InputManager(QObject):
except Exception as e:
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
+ def handle_dpad_repeat(self) -> None:
+ """Handle repeated D-pad input while the D-pad is held."""
+ if self.current_dpad_code is not None and self.current_dpad_value != 0:
+ now = time.time()
+ if (now - self.last_move_time) >= self.current_axis_delay:
+ self.handle_dpad_slot(self.current_dpad_code, self.current_dpad_value, now)
+ self.last_move_time = now
+ self.current_axis_delay = self.repeat_axis_move_delay
@Slot(int, int, float)
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
@@ -188,6 +307,85 @@ class InputManager(QObject):
if not app:
return
active = QApplication.activeWindow()
+ focused = QApplication.focusWidget()
+ popup = QApplication.activePopupWidget()
+
+ # Update D-pad state
+ if value != 0:
+ self.current_dpad_code = code
+ self.current_dpad_value = value
+ if not self.axis_moving:
+ self.axis_moving = True
+ self.last_move_time = current_time
+ self.current_axis_delay = self.initial_axis_move_delay
+ self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds)
+ else:
+ self.current_dpad_code = None
+ self.current_dpad_value = 0
+ self.axis_moving = False
+ self.current_axis_delay = self.initial_axis_move_delay
+ self.dpad_timer.stop() # Stop timer when D-pad is released
+ return
+
+ # Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad
+ if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0:
+ if isinstance(active, QMessageBox): # Specific handling for QMessageBox
+ if not focused or not active.focusWidget():
+ # If no widget is focused, focus the first focusable widget
+ focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
+ focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
+ if focusables:
+ focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
+ return
+ if value > 0: # Right
+ active.focusNextChild()
+ elif value < 0: # Left
+ active.focusPreviousChild()
+ return
+ elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: # Keep up/down for other dialogs
+ if not focused or not active.focusWidget():
+ # If no widget is focused, focus the first focusable widget
+ focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
+ focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
+ if focusables:
+ focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
+ return
+ if value > 0: # Down
+ active.focusNextChild()
+ elif value < 0: # Up
+ active.focusPreviousChild()
+ return
+
+ # Handle QMenu navigation with D-pad
+ if isinstance(popup, QMenu):
+ if code == ecodes.ABS_HAT0Y and value != 0:
+ actions = popup.actions()
+ if actions:
+ current_idx = actions.index(popup.activeAction()) if popup.activeAction() in actions else 0
+ if value < 0: # Up
+ next_idx = (current_idx - 1) % len(actions)
+ popup.setActiveAction(actions[next_idx])
+ elif value > 0: # Down
+ next_idx = (current_idx + 1) % len(actions)
+ popup.setActiveAction(actions[next_idx])
+ return
+ return
+
+ # Handle QListView navigation with D-pad
+ if isinstance(focused, QListView) and code == ecodes.ABS_HAT0Y and value != 0:
+ model = focused.model()
+ current_index = focused.currentIndex()
+ if model and current_index.isValid():
+ row_count = model.rowCount()
+ current_row = current_index.row()
+ if value > 0: # Down
+ next_row = min(current_row + 1, row_count - 1)
+ focused.setCurrentIndex(model.index(next_row, current_index.column()))
+ elif value < 0: # Up
+ prev_row = max(current_row - 1, 0)
+ focused.setCurrentIndex(model.index(prev_row, current_index.column()))
+ focused.scrollTo(focused.currentIndex(), QListView.ScrollHint.PositionAtCenter)
+ return
# Fullscreen horizontal navigation
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
@@ -197,19 +395,6 @@ class InputManager(QObject):
active.show_next()
return
- # Handle repeated D-pad movement
- if value != 0:
- if not self.axis_moving:
- self.axis_moving = True
- elif (current_time - self.last_move_time) < self.current_axis_delay:
- return
- self.last_move_time = current_time
- self.current_axis_delay = self.repeat_axis_move_delay
- else:
- self.axis_moving = False
- self.current_axis_delay = self.initial_axis_move_delay
- return
-
# Library tab navigation (index 0)
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
focused = QApplication.focusWidget()
@@ -280,7 +465,6 @@ class InputManager(QObject):
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
-
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
if value > 0: # Down
next_row_idx = current_row_idx + 1
@@ -350,6 +534,12 @@ class InputManager(QObject):
focused = QApplication.focusWidget()
popup = QApplication.activePopupWidget()
+ # Open system overlay with Insert
+ if key == Qt.Key.Key_Insert:
+ if not popup and not isinstance(QApplication.activeWindow(), QDialog):
+ self._parent.openSystemOverlay()
+ return True
+
# Close application with Ctrl+Q
if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
app.quit()
@@ -390,6 +580,23 @@ class InputManager(QObject):
focused._show_context_menu(pos)
return True
+ # Handle Up/Down keys for non-GameCard tabs
+ if key in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not isinstance(focused, GameCard):
+ page = self._parent.stackedWidget.currentWidget()
+ if key == Qt.Key.Key_Down:
+ if isinstance(focused, NavLabel):
+ focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
+ focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
+ if focusables:
+ focusables[0].setFocus()
+ return True
+ elif focused:
+ focused.focusNextChild()
+ return True
+ elif key == Qt.Key.Key_Up and focused:
+ focused.focusPreviousChild()
+ return True
+
# Tab switching with Left/Right keys (non-GameCard focus or no focus)
idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons)
@@ -520,6 +727,9 @@ class InputManager(QObject):
if focusables:
focusables[0].setFocus()
return True
+ elif focused:
+ focused.focusNextChild()
+ return True
# Navigate up through tab content
if key == Qt.Key.Key_Up:
if isinstance(focused, NavLabel):
@@ -540,8 +750,10 @@ class InputManager(QObject):
elif key == Qt.Key.Key_E:
if isinstance(focused, QLineEdit):
return False
- self._parent.openAddGameDialog()
- return True
+ # Only open AddGameDialog if in library tab (index 0)
+ if self._parent.stackedWidget.currentIndex() == 0:
+ self._parent.openAddGameDialog()
+ return True
# Toggle fullscreen with F11
if key == Qt.Key.Key_F11:
@@ -559,17 +771,17 @@ class InputManager(QObject):
def run_udev_monitor(self) -> None:
try:
- context = pyudev.Context()
- monitor = pyudev.Monitor.from_netlink(context)
+ context = Context()
+ monitor = Monitor.from_netlink(context)
monitor.filter_by(subsystem='input')
- observer = pyudev.MonitorObserver(monitor, self.handle_udev_event)
+ observer = MonitorObserver(monitor, self.handle_udev_event)
observer.start()
while self.running:
time.sleep(1)
except Exception as e:
logger.error(f"Error in udev monitor: {e}", exc_info=True)
- def handle_udev_event(self, action: str, device: pyudev.Device) -> None:
+ def handle_udev_event(self, action: str, device: Device) -> None:
try:
if action == 'add':
time.sleep(0.1)
@@ -577,6 +789,7 @@ class InputManager(QObject):
elif action == 'remove' and self.gamepad:
if not any(self.gamepad.path == path for path in list_devices()):
logger.info("Gamepad disconnected")
+ self.stop_rumble()
self.gamepad = None
if self.gamepad_thread:
self.gamepad_thread.join()
@@ -590,6 +803,7 @@ class InputManager(QObject):
new_gamepad = self.find_gamepad()
if new_gamepad and new_gamepad != self.gamepad:
logger.info(f"Gamepad connected: {new_gamepad.name}")
+ self.stop_rumble()
self.gamepad = new_gamepad
if self.gamepad_thread:
self.gamepad_thread.join()
@@ -626,9 +840,7 @@ class InputManager(QObject):
continue
now = time.time()
if event.type == ecodes.EV_KEY and event.value == 1:
- # Обработка кнопки Select для переключения полноэкранного режима
if event.code in BUTTONS['menu']:
- # Переключаем полноэкранный режим
self.toggle_fullscreen.emit(not self._is_fullscreen)
else:
self.button_pressed.emit(event.code)
@@ -644,6 +856,7 @@ class InputManager(QObject):
finally:
if self.gamepad:
try:
+ self.stop_rumble()
self.gamepad.close()
except Exception:
pass
@@ -652,6 +865,8 @@ class InputManager(QObject):
def cleanup(self) -> None:
try:
self.running = False
+ self.dpad_timer.stop()
+ self.stop_rumble()
if self.gamepad_thread:
self.gamepad_thread.join()
if self.gamepad:
diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo
index 0f3b271..af70946 100644
Binary files a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo and b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.mo differ
diff --git a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po
index 882d392..14a21f6 100644
--- a/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po
+++ b/portprotonqt/locales/de_DE/LC_MESSAGES/messages.po
@@ -1,6 +1,6 @@
-# German (Germany) translations for PortProtonQT.
+# German (Germany) translations for PortProtonQt.
# Copyright (C) 2025 boria138
-# This file is distributed under the same license as the PortProtonQT
+# This file is distributed under the same license as the PortProtonQt
# project.
# FIRST AUTHOR , 2025.
#
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-06-06 20:01+0500\n"
+"POT-Creation-Date: 2025-06-11 23:15+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language: de_DE\n"
@@ -20,6 +20,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
+msgid "Remove from Favorites"
+msgstr ""
+
+msgid "Add to Favorites"
+msgstr ""
+
msgid "Remove from Desktop"
msgstr ""
@@ -47,6 +53,14 @@ msgstr ""
msgid "Add to Steam"
msgstr ""
+#, python-brace-format
+msgid "Added '{0}' to favorites"
+msgstr ""
+
+#, python-brace-format
+msgid "Removed '{0}' from favorites"
+msgstr ""
+
msgid "Error"
msgstr ""
@@ -362,19 +376,10 @@ msgstr ""
msgid "Auto Fullscreen on Gamepad connected:"
msgstr ""
-msgid "Open Legendary Login"
+msgid "Gamepad haptic feedback"
msgstr ""
-msgid "Legendary Authentication:"
-msgstr ""
-
-msgid "Enter Legendary Authorization Code"
-msgstr ""
-
-msgid "Authorization Code:"
-msgstr ""
-
-msgid "Submit Code"
+msgid "Gamepad haptic feedback:"
msgstr ""
msgid "Save Settings"
@@ -392,22 +397,6 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
-msgid "Please enter an authorization code"
-msgstr ""
-
-msgid "Successfully authenticated with Legendary"
-msgstr ""
-
-#, python-brace-format
-msgid "Legendary authentication failed: {0}"
-msgstr ""
-
-msgid "Legendary executable not found"
-msgstr ""
-
-msgid "Unexpected error during authentication"
-msgstr ""
-
msgid "Confirm Reset"
msgstr ""
@@ -505,6 +494,42 @@ msgstr ""
msgid "Launching"
msgstr ""
+msgid "System Overlay"
+msgstr ""
+
+msgid "Reboot"
+msgstr ""
+
+msgid "Shutdown"
+msgstr ""
+
+msgid "Suspend"
+msgstr ""
+
+msgid "Exit Application"
+msgstr ""
+
+msgid "Return to Desktop"
+msgstr ""
+
+msgid "portprotonqt-session-select file not found at /usr/bin/"
+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 "Failed to return to desktop"
+msgstr ""
+
msgid "just now"
msgstr ""
diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo
index 50770aa..e423857 100644
Binary files a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo and b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.mo differ
diff --git a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po
index b67ae5b..e139416 100644
--- a/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po
+++ b/portprotonqt/locales/es_ES/LC_MESSAGES/messages.po
@@ -1,6 +1,6 @@
-# Spanish (Spain) translations for PortProtonQT.
+# Spanish (Spain) translations for PortProtonQt.
# Copyright (C) 2025 boria138
-# This file is distributed under the same license as the PortProtonQT
+# This file is distributed under the same license as the PortProtonQt
# project.
# FIRST AUTHOR , 2025.
#
@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-06-06 20:01+0500\n"
+"POT-Creation-Date: 2025-06-11 23:15+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language: es_ES\n"
@@ -20,6 +20,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
+msgid "Remove from Favorites"
+msgstr ""
+
+msgid "Add to Favorites"
+msgstr ""
+
msgid "Remove from Desktop"
msgstr ""
@@ -47,6 +53,14 @@ msgstr ""
msgid "Add to Steam"
msgstr ""
+#, python-brace-format
+msgid "Added '{0}' to favorites"
+msgstr ""
+
+#, python-brace-format
+msgid "Removed '{0}' from favorites"
+msgstr ""
+
msgid "Error"
msgstr ""
@@ -362,19 +376,10 @@ msgstr ""
msgid "Auto Fullscreen on Gamepad connected:"
msgstr ""
-msgid "Open Legendary Login"
+msgid "Gamepad haptic feedback"
msgstr ""
-msgid "Legendary Authentication:"
-msgstr ""
-
-msgid "Enter Legendary Authorization Code"
-msgstr ""
-
-msgid "Authorization Code:"
-msgstr ""
-
-msgid "Submit Code"
+msgid "Gamepad haptic feedback:"
msgstr ""
msgid "Save Settings"
@@ -392,22 +397,6 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
-msgid "Please enter an authorization code"
-msgstr ""
-
-msgid "Successfully authenticated with Legendary"
-msgstr ""
-
-#, python-brace-format
-msgid "Legendary authentication failed: {0}"
-msgstr ""
-
-msgid "Legendary executable not found"
-msgstr ""
-
-msgid "Unexpected error during authentication"
-msgstr ""
-
msgid "Confirm Reset"
msgstr ""
@@ -505,6 +494,42 @@ msgstr ""
msgid "Launching"
msgstr ""
+msgid "System Overlay"
+msgstr ""
+
+msgid "Reboot"
+msgstr ""
+
+msgid "Shutdown"
+msgstr ""
+
+msgid "Suspend"
+msgstr ""
+
+msgid "Exit Application"
+msgstr ""
+
+msgid "Return to Desktop"
+msgstr ""
+
+msgid "portprotonqt-session-select file not found at /usr/bin/"
+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 "Failed to return to desktop"
+msgstr ""
+
msgid "just now"
msgstr ""
diff --git a/portprotonqt/locales/messages.pot b/portprotonqt/locales/messages.pot
index 46314b4..10519af 100644
--- a/portprotonqt/locales/messages.pot
+++ b/portprotonqt/locales/messages.pot
@@ -1,15 +1,15 @@
-# Translations template for PortProtonQT.
+# Translations template for PortProtonQt.
# Copyright (C) 2025 boria138
-# This file is distributed under the same license as the PortProtonQT
+# This file is distributed under the same license as the PortProtonQt
# project.
# FIRST AUTHOR , 2025.
#
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: PortProtonQT 0.1.1\n"
+"Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-06-06 20:01+0500\n"
+"POT-Creation-Date: 2025-06-11 23:15+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -18,6 +18,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
+msgid "Remove from Favorites"
+msgstr ""
+
+msgid "Add to Favorites"
+msgstr ""
+
msgid "Remove from Desktop"
msgstr ""
@@ -45,6 +51,14 @@ msgstr ""
msgid "Add to Steam"
msgstr ""
+#, python-brace-format
+msgid "Added '{0}' to favorites"
+msgstr ""
+
+#, python-brace-format
+msgid "Removed '{0}' from favorites"
+msgstr ""
+
msgid "Error"
msgstr ""
@@ -360,19 +374,10 @@ msgstr ""
msgid "Auto Fullscreen on Gamepad connected:"
msgstr ""
-msgid "Open Legendary Login"
+msgid "Gamepad haptic feedback"
msgstr ""
-msgid "Legendary Authentication:"
-msgstr ""
-
-msgid "Enter Legendary Authorization Code"
-msgstr ""
-
-msgid "Authorization Code:"
-msgstr ""
-
-msgid "Submit Code"
+msgid "Gamepad haptic feedback:"
msgstr ""
msgid "Save Settings"
@@ -390,22 +395,6 @@ msgstr ""
msgid "Failed to open Legendary login page"
msgstr ""
-msgid "Please enter an authorization code"
-msgstr ""
-
-msgid "Successfully authenticated with Legendary"
-msgstr ""
-
-#, python-brace-format
-msgid "Legendary authentication failed: {0}"
-msgstr ""
-
-msgid "Legendary executable not found"
-msgstr ""
-
-msgid "Unexpected error during authentication"
-msgstr ""
-
msgid "Confirm Reset"
msgstr ""
@@ -503,6 +492,42 @@ msgstr ""
msgid "Launching"
msgstr ""
+msgid "System Overlay"
+msgstr ""
+
+msgid "Reboot"
+msgstr ""
+
+msgid "Shutdown"
+msgstr ""
+
+msgid "Suspend"
+msgstr ""
+
+msgid "Exit Application"
+msgstr ""
+
+msgid "Return to Desktop"
+msgstr ""
+
+msgid "portprotonqt-session-select file not found at /usr/bin/"
+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 "Failed to return to desktop"
+msgstr ""
+
msgid "just now"
msgstr ""
diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo
index d774613..e073216 100644
Binary files a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo and b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.mo differ
diff --git a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po
index 3fe79d9..a26b290 100644
--- a/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po
+++ b/portprotonqt/locales/ru_RU/LC_MESSAGES/messages.po
@@ -1,6 +1,6 @@
-# Russian (Russia) translations for PortProtonQT.
+# Russian (Russia) translations for PortProtonQt.
# Copyright (C) 2025 boria138
-# This file is distributed under the same license as the PortProtonQT
+# This file is distributed under the same license as the PortProtonQt
# project.
# FIRST AUTHOR , 2025.
#
@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2025-06-06 20:01+0500\n"
-"PO-Revision-Date: 2025-06-06 20:01+0500\n"
+"POT-Creation-Date: 2025-06-11 23:15+0500\n"
+"PO-Revision-Date: 2025-06-11 23:15+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU \n"
@@ -21,6 +21,12 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.17.0\n"
+msgid "Remove from Favorites"
+msgstr "Удалить из Избранного"
+
+msgid "Add to Favorites"
+msgstr "Добавить в Избранное"
+
msgid "Remove from Desktop"
msgstr "Удалить с рабочего стола"
@@ -48,6 +54,14 @@ msgstr "Удалить из Steam"
msgid "Add to Steam"
msgstr "Добавить в Steam"
+#, python-brace-format
+msgid "Added '{0}' to favorites"
+msgstr "Добавление '{0}' в избранное"
+
+#, python-brace-format
+msgid "Removed '{0}' from favorites"
+msgstr "Удаление '{0}' из избранного"
+
msgid "Error"
msgstr "Ошибка"
@@ -369,20 +383,11 @@ msgstr "Режим полноэкранного отображения прил
msgid "Auto Fullscreen on Gamepad connected:"
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
-msgid "Open Legendary Login"
-msgstr "Открыть браузер для входа в Legendary"
+msgid "Gamepad haptic feedback"
+msgstr "Тактильная обратная связь на геймпаде"
-msgid "Legendary Authentication:"
-msgstr "Авторизация в Legendary:"
-
-msgid "Enter Legendary Authorization Code"
-msgstr "Введите код авторизации Legendary"
-
-msgid "Authorization Code:"
-msgstr "Код авторизации:"
-
-msgid "Submit Code"
-msgstr "Отправить код"
+msgid "Gamepad haptic feedback:"
+msgstr "Тактильная обратная связь на геймпаде:"
msgid "Save Settings"
msgstr "Сохранить настройки"
@@ -399,22 +404,6 @@ msgstr "Открытие страницы входа в Legendary в брауз
msgid "Failed to open Legendary login page"
msgstr "Не удалось открыть страницу входа в Legendary"
-msgid "Please enter an authorization code"
-msgstr "Пожалуйста, введите код авторизации"
-
-msgid "Successfully authenticated with Legendary"
-msgstr "Успешная аутентификация с Legendary"
-
-#, python-brace-format
-msgid "Legendary authentication failed: {0}"
-msgstr "Сбой аутентификации в Legendary: {0}"
-
-msgid "Legendary executable not found"
-msgstr "Не найден исполняемый файл Legendary"
-
-msgid "Unexpected error during authentication"
-msgstr "Неожиданная ошибка при аутентификации"
-
msgid "Confirm Reset"
msgstr "Подтвердите удаление"
@@ -514,6 +503,42 @@ msgstr "Невозможно запустить игру пока запущен
msgid "Launching"
msgstr "Идёт запуск"
+msgid "System Overlay"
+msgstr "Системный оверлей"
+
+msgid "Reboot"
+msgstr "Перезагрузить"
+
+msgid "Shutdown"
+msgstr "Выключить"
+
+msgid "Suspend"
+msgstr "Перейти в ждущий режим"
+
+msgid "Exit Application"
+msgstr "Выйти из приложения"
+
+msgid "Return to Desktop"
+msgstr "Вернуться на рабочий стол"
+
+msgid "portprotonqt-session-select file not found at /usr/bin/"
+msgstr "portprotonqt-session-select не найдет"
+
+msgid "Cancel"
+msgstr "Отмена"
+
+msgid "Failed to reboot the system"
+msgstr "Не удалось перезагрузить систему"
+
+msgid "Failed to shutdown the system"
+msgstr "Не удалось завершить работу системы"
+
+msgid "Failed to suspend the system"
+msgstr "Не удалось перейти в ждущий режим"
+
+msgid "Failed to return to desktop"
+msgstr "Не удалось вернуться на рабочий стол"
+
msgid "just now"
msgstr "только что"
diff --git a/portprotonqt/main_window.py b/portprotonqt/main_window.py
index 142f2ed..2cb38fa 100644
--- a/portprotonqt/main_window.py
+++ b/portprotonqt/main_window.py
@@ -13,6 +13,7 @@ from portprotonqt.game_card import GameCard
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
from portprotonqt.input_manager import InputManager
from portprotonqt.context_menu_manager import ContextMenuManager
+from portprotonqt.system_overlay import SystemOverlay
from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
@@ -25,7 +26,7 @@ from portprotonqt.config_utils import (
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
- clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad
+ clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
)
from portprotonqt.localization import _
from portprotonqt.logger import get_logger
@@ -43,7 +44,7 @@ from datetime import datetime
logger = get_logger(__name__)
class MainWindow(QMainWindow):
- """Main window of PortProtonQT."""
+ """Main window of PortProtonQt."""
settings_saved = Signal()
games_loaded = Signal(list)
update_progress = Signal(int) # Signal to update progress bar
@@ -72,10 +73,10 @@ class MainWindow(QMainWindow):
self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed)
read_time_config()
- # Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQT/legendary
+ # Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQt/legendary_cache
self.legendary_config_path = os.path.join(
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
- "PortProtonQT", "legendary_cache"
+ "PortProtonQt", "legendary_cache"
)
os.makedirs(self.legendary_config_path, exist_ok=True)
os.environ["LEGENDARY_CONFIG_PATH"] = self.legendary_config_path
@@ -97,10 +98,11 @@ class MainWindow(QMainWindow):
if not self.theme:
self.theme = default_styles
self.card_width = read_card_size()
- self.setWindowTitle("PortProtonQT")
+ self.setWindowTitle("PortProtonQt")
self.setMinimumSize(800, 600)
self.games = []
+ self.filtered_games = self.games
self.game_processes = []
self.target_exe = None
self.current_running_button = None
@@ -259,39 +261,28 @@ class MainWindow(QMainWindow):
self.update_status_message.emit
)
elif display_filter == "favorites":
- def on_all_games(portproton_games, steam_games, epic_games):
- games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
+ def on_all_games(portproton_games, steam_games):
+ games = [game for game in portproton_games + steam_games if game[0] in favorites]
self.games_loaded.emit(games)
self._load_portproton_games_async(
lambda pg: self._load_steam_games_async(
- lambda sg: load_egs_games_async(
- self.legendary_path,
- lambda eg: on_all_games(pg, sg, eg),
- self.downloader,
- self.update_progress.emit,
- self.update_status_message.emit
- )
+ lambda sg: on_all_games(pg, sg)
)
)
else:
- def on_all_games(portproton_games, steam_games, epic_games):
+ def on_all_games(portproton_games, steam_games):
seen = set()
games = []
- for game in portproton_games + steam_games + epic_games:
- name = game[0]
- if name not in seen:
- seen.add(name)
+ for game in portproton_games + steam_games:
+ # Уникальный ключ: имя + exec_line
+ key = (game[0], game[4])
+ if key not in seen:
+ seen.add(key)
games.append(game)
self.games_loaded.emit(games)
self._load_portproton_games_async(
lambda pg: self._load_steam_games_async(
- lambda sg: load_egs_games_async(
- self.legendary_path,
- lambda eg: on_all_games(pg, sg, eg),
- self.downloader,
- self.update_progress.emit,
- self.update_status_message.emit
- )
+ lambda sg: on_all_games(pg, sg)
)
)
return []
@@ -394,7 +385,7 @@ class MainWindow(QMainWindow):
builtin_custom_folder = os.path.join(repo_root, "portprotonqt", "custom_data")
xdg_data_home = os.getenv("XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share"))
- user_custom_folder = os.path.join(xdg_data_home, "PortProtonQT", "custom_data")
+ user_custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data")
os.makedirs(user_custom_folder, exist_ok=True)
builtin_cover = ""
@@ -500,6 +491,11 @@ class MainWindow(QMainWindow):
btn.setChecked(i == index)
self.stackedWidget.setCurrentIndex(index)
+ def openSystemOverlay(self):
+ """Opens the system overlay dialog."""
+ overlay = SystemOverlay(self, self.theme)
+ overlay.exec()
+
def createSearchWidget(self) -> tuple[QWidget, QLineEdit]:
self.container = QWidget()
self.container.setStyleSheet(self.theme.CONTAINER_STYLE)
@@ -539,14 +535,20 @@ class MainWindow(QMainWindow):
def startSearchDebounce(self, text):
self.searchDebounceTimer.start()
+ def on_slider_value_changed(self, value: int):
+ self.card_width = value
+ self.sizeSlider.setToolTip(f"{value} px")
+ save_card_size(value)
+ self.updateGameGrid()
+
def filterGamesDelayed(self):
"""Filters games based on search text and updates the grid."""
text = self.searchEdit.text().strip().lower()
if text == "":
- self.updateGameGrid() # Use self.games directly
+ self.filtered_games = self.games
else:
- filtered = [game for game in self.games if text in game[0].lower()]
- self.updateGameGrid(filtered)
+ self.filtered_games = [game for game in self.games if text in game[0].lower()]
+ self.updateGameGrid(self.filtered_games)
def createInstalledTab(self):
self.gamesLibraryWidget = QWidget()
@@ -579,33 +581,16 @@ class MainWindow(QMainWindow):
self.sizeSlider.setFixedWidth(150)
self.sizeSlider.setToolTip(f"{self.card_width} px")
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
+ self.sizeSlider.valueChanged.connect(self.on_slider_value_changed)
sliderLayout.addWidget(self.sizeSlider)
layout.addLayout(sliderLayout)
- self.sliderDebounceTimer = QTimer(self)
- self.sliderDebounceTimer.setSingleShot(True)
- self.sliderDebounceTimer.setInterval(40)
-
- def on_slider_value_changed():
- self.setUpdatesEnabled(False)
- self.card_width = self.sizeSlider.value()
- self.sizeSlider.setToolTip(f"{self.card_width} px")
- self.updateGameGrid()
- self.setUpdatesEnabled(True)
- self.sizeSlider.valueChanged.connect(lambda val: self.sliderDebounceTimer.start())
- self.sliderDebounceTimer.timeout.connect(on_slider_value_changed)
-
def calculate_card_width():
available_width = scrollArea.width() - 20
spacing = self.gamesListLayout._spacing
target_cards_per_row = 8
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
calculated_width = max(200, min(calculated_width, 250))
- if not self.sizeSlider.value() == self.card_width:
- self.card_width = calculated_width
- self.sizeSlider.setValue(self.card_width)
- self.sizeSlider.setToolTip(f"{self.card_width} px")
- self.updateGameGrid()
QTimer.singleShot(0, calculate_card_width)
@@ -621,7 +606,6 @@ class MainWindow(QMainWindow):
self._last_width = self.width()
if abs(self.width() - self._last_width) > 10:
self._last_width = self.width()
- self.sliderDebounceTimer.start()
def loadVisibleImages(self):
visible_region = self.gamesListWidget.visibleRegion()
@@ -638,22 +622,38 @@ class MainWindow(QMainWindow):
if games_list is None:
games_list = self.games
if not games_list:
- self.clearLayout(self.gamesListLayout)
+ # Скрываем все карточки, если список пуст
+ for card in self.game_card_cache.values():
+ card.hide()
self.game_card_cache.clear()
self.pending_images.clear()
+ self.gamesListWidget.updateGeometry()
return
- # Create a set of game names for quick lookup
- current_games = {game_data[0]: game_data for game_data in games_list}
+ # Создаем словарь текущих игр с уникальным ключом (name + exec_line)
+ current_games = {(game_data[0], game_data[4]): game_data for game_data in games_list}
- # Check if the grid is already up-to-date
- if set(current_games.keys()) == set(self.game_card_cache.keys()) and self.card_width == getattr(self, '_last_card_width', None):
- return # No changes needed, skip update
+ # Проверяем, изменился ли список игр или размер карточек
+ current_game_keys = set(current_games.keys())
+ cached_game_keys = set(self.game_card_cache.keys())
+ card_width_changed = self.card_width != getattr(self, '_last_card_width', None)
- # Track if layout has changed to decide if geometry update is needed
+ if current_game_keys == cached_game_keys and not card_width_changed:
+ # Список игр и размер карточек не изменились, обновляем только видимость
+ search_text = self.searchEdit.text().strip().lower()
+ for game_key, card in self.game_card_cache.items():
+ game_name = game_key[0]
+ card.setVisible(search_text in game_name.lower() or not search_text)
+ self.loadVisibleImages()
+ return
+
+ # Обновляем размер карточек, если он изменился
+ if card_width_changed:
+ for card in self.game_card_cache.values():
+ card.setFixedWidth(self.card_width + 20) # Учитываем extra_margin в GameCard
+
+ # Удаляем карточки, которых больше нет в списке
layout_changed = False
-
- # Remove cards for games no longer in the list
for card_key in list(self.game_card_cache.keys()):
if card_key not in current_games:
card = self.game_card_cache.pop(card_key)
@@ -663,11 +663,15 @@ class MainWindow(QMainWindow):
del self.pending_images[card_key]
layout_changed = True
- # Add or update cards for current games
+ # Добавляем новые карточки и обновляем существующие
for game_data in games_list:
game_name = game_data[0]
- if game_name not in self.game_card_cache:
- # Create new card
+ game_key = (game_name, game_data[4])
+ search_text = self.searchEdit.text().strip().lower()
+ should_be_visible = search_text in game_name.lower() or not search_text
+
+ if game_key not in self.game_card_cache:
+ # Создаем новую карточку
card = GameCard(
*game_data,
select_callback=self.openGameDetailPage,
@@ -675,7 +679,7 @@ class MainWindow(QMainWindow):
card_width=self.card_width,
context_menu_manager=self.context_menu_manager
)
- # Connect context menu signals
+ # Подключаем сигналы контекстного меню
card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu)
@@ -685,23 +689,25 @@ class MainWindow(QMainWindow):
card.addToSteamRequested.connect(self.context_menu_manager.add_to_steam)
card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam)
card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder)
- self.game_card_cache[game_name] = card
+ self.game_card_cache[game_key] = card
self.gamesListLayout.addWidget(card)
layout_changed = True
- elif self.card_width != getattr(self, '_last_card_width', None):
- # Update size only if card_width has changed
- card = self.game_card_cache[game_name]
- card.setFixedWidth(self.card_width + 20) # Account for extra_margin in GameCard
+ else:
+ # Обновляем видимость существующей карточки
+ card = self.game_card_cache[game_key]
+ card.setVisible(should_be_visible)
- # Store the current card_width
+ # Сохраняем текущий card_width
self._last_card_width = self.card_width
- # Trigger lazy image loading for visible cards
- self.loadVisibleImages()
-
- # Update layout geometry only if the layout has changed
+ # Принудительно обновляем макет
if layout_changed:
+ self.gamesListLayout.update()
self.gamesListWidget.updateGeometry()
+ self.gamesListWidget.update()
+
+ # Загружаем изображения для видимых карточек
+ self.loadVisibleImages()
def clearLayout(self, layout):
"""Удаляет все виджеты из layout."""
@@ -742,6 +748,7 @@ class MainWindow(QMainWindow):
return
dialog = AddGameDialog(self, self.theme)
+ dialog.setFocus(Qt.FocusReason.OtherFocusReason)
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
# Предзаполняем путь к .exe при drag-and-drop
@@ -778,7 +785,7 @@ class MainWindow(QMainWindow):
os.path.join(os.path.expanduser("~"), ".local", "share"))
custom_folder = os.path.join(
xdg_data_home,
- "PortProtonQT",
+ "PortProtonQt",
"custom_data",
exe_name
)
@@ -920,7 +927,7 @@ class MainWindow(QMainWindow):
# 3. Games display_filter
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
- self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"]
+ self.filter_labels = [_("all"), "steam", "portproton", _("favorites")]
self.gamesDisplayCombo = QComboBox()
self.gamesDisplayCombo.addItems(self.filter_labels)
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
@@ -983,6 +990,7 @@ class MainWindow(QMainWindow):
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
self.autoFullscreenGamepadTitle = QLabel(_("Auto Fullscreen on Gamepad connected:"))
self.autoFullscreenGamepadTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
self.autoFullscreenGamepadTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
@@ -990,36 +998,16 @@ class MainWindow(QMainWindow):
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
- # 7. Legendary Authentication
- self.legendaryAuthButton = AutoSizeButton(
- _("Open Legendary Login"),
- icon=self.theme_manager.get_icon("login")
- )
- self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
- self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
- self.legendaryAuthButton.clicked.connect(self.openLegendaryLogin)
- self.legendaryAuthTitle = QLabel(_("Legendary Authentication:"))
- self.legendaryAuthTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
- self.legendaryAuthTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
- formLayout.addRow(self.legendaryAuthTitle, self.legendaryAuthButton)
-
- self.legendaryCodeEdit = QLineEdit()
- self.legendaryCodeEdit.setPlaceholderText(_("Enter Legendary Authorization Code"))
- self.legendaryCodeEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
- self.legendaryCodeEdit.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
- self.legendaryCodeTitle = QLabel(_("Authorization Code:"))
- self.legendaryCodeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
- self.legendaryCodeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
- formLayout.addRow(self.legendaryCodeTitle, self.legendaryCodeEdit)
-
- self.submitCodeButton = AutoSizeButton(
- _("Submit Code"),
- icon=self.theme_manager.get_icon("save")
- )
- self.submitCodeButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
- self.submitCodeButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
- self.submitCodeButton.clicked.connect(self.submitLegendaryCode)
- formLayout.addRow(QLabel(""), self.submitCodeButton)
+ # 7. Gamepad haptic feedback config
+ self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
+ self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
+ self.gamepadRumbleTitle = QLabel(_("Gamepad haptic feedback:"))
+ self.gamepadRumbleTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
+ self.gamepadRumbleTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
+ current_rumble_state = read_rumble_config()
+ self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
+ formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
layout.addLayout(formLayout)
@@ -1071,37 +1059,6 @@ class MainWindow(QMainWindow):
logger.error(f"Failed to open Legendary login page: {e}")
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
- def submitLegendaryCode(self):
- """Submits the Legendary authorization code using the legendary CLI."""
- auth_code = self.legendaryCodeEdit.text().strip()
- if not auth_code:
- QMessageBox.warning(self, _("Error"), _("Please enter an authorization code"))
- return
-
- try:
- # Execute legendary auth command
- result = subprocess.run(
- [self.legendary_path, "auth", "--code", auth_code],
- capture_output=True,
- text=True,
- check=True
- )
- logger.info("Legendary authentication successful: %s", result.stdout)
- self.statusBar().showMessage(_("Successfully authenticated with Legendary"), 3000)
- self.legendaryCodeEdit.clear()
- # Reload Epic Games Store games after successful authentication
- self.games = self.loadGames()
- self.updateGameGrid()
- except subprocess.CalledProcessError as e:
- logger.error("Legendary authentication failed: %s", e.stderr)
- self.statusBar().showMessage(_("Legendary authentication failed: {0}").format(e.stderr), 5000)
- except FileNotFoundError:
- logger.error("Legendary executable not found at %s", self.legendary_path)
- self.statusBar().showMessage(_("Legendary executable not found"), 5000)
- except Exception as e:
- logger.error("Unexpected error during Legendary authentication: %s", str(e))
- self.statusBar().showMessage(_("Unexpected error during authentication"), 5000)
-
def resetSettings(self):
"""Сбрасывает настройки и перезапускает приложение."""
reply = QMessageBox.question(
@@ -1172,6 +1129,10 @@ class MainWindow(QMainWindow):
auto_fullscreen_gamepad = self.autoFullscreenGamepadCheckBox.isChecked()
save_auto_fullscreen_gamepad(auto_fullscreen_gamepad)
+ # Сохранение настройки виброотдачи геймпада
+ rumble_enabled = self.gamepadRumbleCheckBox.isChecked()
+ save_rumble_config(rumble_enabled)
+
for card in self.game_card_cache.values():
card.update_badge_visibility(filter_key)
@@ -1288,11 +1249,15 @@ class MainWindow(QMainWindow):
self.statusBar().showMessage(_("Theme '{0}' applied successfully").format(selected_theme), 3000)
xdg_data_home = os.getenv("XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share"))
- state_file = os.path.join(xdg_data_home, "PortProtonQT", "state.txt")
+ state_file = os.path.join(xdg_data_home, "PortProtonQt", "state.txt")
os.makedirs(os.path.dirname(state_file), exist_ok=True)
- with open(state_file, "w", encoding="utf-8") as f:
- f.write("theme_tab\n")
- QTimer.singleShot(500, lambda: self.restart_application())
+ try:
+ with open(state_file, "w", encoding="utf-8") as f:
+ f.write("theme_tab\n")
+ logger.info(f"State saved to {state_file}")
+ QTimer.singleShot(500, lambda: self.restart_application())
+ except Exception as e:
+ logger.error(f"Failed to save state to {state_file}: {e}")
else:
self.statusBar().showMessage(_("Error applying theme '{0}'").format(selected_theme), 3000)
@@ -1310,14 +1275,28 @@ class MainWindow(QMainWindow):
def restore_state(self):
"""Восстанавливает состояние приложения после перезапуска."""
- xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
- state_file = os.path.join(xdg_cache_home, "PortProtonQT", "state.txt")
+ xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
+ state_file = os.path.join(xdg_data_home, "PortProtonQt", "state.txt")
+ logger.info(f"Checking for state file: {state_file}")
if os.path.exists(state_file):
- with open(state_file, encoding="utf-8") as f:
- state = f.read().strip()
- if state == "theme_tab":
- self.switchTab(5)
- os.remove(state_file)
+ try:
+ with open(state_file, encoding="utf-8") as f:
+ state = f.read().strip()
+ logger.info(f"State file contents: '{state}'")
+ if state == "theme_tab":
+ logger.info("Restoring to theme tab (index 5)")
+ if self.stackedWidget.count() > 5:
+ self.switchTab(5)
+ else:
+ logger.warning("Theme tab (index 5) not available yet")
+ else:
+ logger.warning(f"Unexpected state value: '{state}'")
+ os.remove(state_file)
+ logger.info(f"State file {state_file} removed")
+ except Exception as e:
+ logger.error(f"Failed to read or process state file {state_file}: {e}")
+ else:
+ logger.info(f"State file {state_file} does not exist")
# ЛОГИКА ДЕТАЛЬНОЙ СТРАНИЦЫ ИГРЫ
def getColorPalette_async(self, cover_path, num_colors=5, sample_step=10, callback=None):
@@ -1514,7 +1493,7 @@ class MainWindow(QMainWindow):
icon_size=16,
icon_space=3,
)
- anticheatLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
+ anticheatLabel.setStyleSheet(self.theme.get_anticheat_badge_style(anticheat_status))
anticheatLabel.setFixedWidth(badge_width)
anticheatLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://areweanticheatyet.com/game/{name.lower().replace(' ', '-')}")))
anticheat_visible = True
diff --git a/portprotonqt/steam_api.py b/portprotonqt/steam_api.py
index 514ba73..fed091e 100644
--- a/portprotonqt/steam_api.py
+++ b/portprotonqt/steam_api.py
@@ -49,7 +49,7 @@ def decode_text(text: str) -> str:
def get_cache_dir():
"""Возвращает путь к каталогу кэша, создаёт его при необходимости."""
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
- cache_dir = os.path.join(xdg_cache_home, "PortProtonQT")
+ cache_dir = os.path.join(xdg_cache_home, "PortProtonQt")
os.makedirs(cache_dir, exist_ok=True)
return cache_dir
diff --git a/portprotonqt/system_overlay.py b/portprotonqt/system_overlay.py
new file mode 100644
index 0000000..6359dce
--- /dev/null
+++ b/portprotonqt/system_overlay.py
@@ -0,0 +1,109 @@
+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
+import os
+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.OVERLAY_BUTTON_STYLE)
+ reboot_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ reboot_button.clicked.connect(self.reboot)
+ layout.addWidget(reboot_button)
+
+ # Shutdown button
+ shutdown_button = QPushButton(_("Shutdown"))
+ shutdown_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
+ shutdown_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ shutdown_button.clicked.connect(self.shutdown)
+ layout.addWidget(shutdown_button)
+
+ # Suspend button
+ suspend_button = QPushButton(_("Suspend"))
+ suspend_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
+ suspend_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ suspend_button.clicked.connect(self.suspend)
+ layout.addWidget(suspend_button)
+
+ # Exit application button
+ exit_button = QPushButton(_("Exit Application"))
+ exit_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
+ exit_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ exit_button.clicked.connect(self.exit_application)
+ layout.addWidget(exit_button)
+
+ # Return to Desktop button
+ desktop_button = QPushButton(_("Return to Desktop"))
+ desktop_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
+ desktop_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
+ desktop_button.clicked.connect(self.return_to_desktop)
+ script_path = "/usr/bin/portprotonqt-session-select"
+ script_exists = os.path.isfile(script_path)
+ desktop_button.setEnabled(script_exists)
+ if not script_exists:
+ desktop_button.setToolTip(_("portprotonqt-session-select file not found at /usr/bin/"))
+ layout.addWidget(desktop_button)
+
+ # Cancel button
+ cancel_button = QPushButton(_("Cancel"))
+ cancel_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
+ 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 return_to_desktop(self):
+ try:
+ script_path = os.path.join(os.path.dirname(__file__), "portprotonqt-session-select")
+ subprocess.run([script_path, "desktop"], check=True)
+ except subprocess.CalledProcessError as e:
+ logger.error(f"Failed to return to desktop: {e}")
+ QMessageBox.warning(self, _("Error"), _("Failed to return to desktop"))
+ self.accept()
+
+ def exit_application(self):
+ QApplication.quit()
+ self.accept()
diff --git a/portprotonqt/theme_manager.py b/portprotonqt/theme_manager.py
index 0686e7d..428b069 100644
--- a/portprotonqt/theme_manager.py
+++ b/portprotonqt/theme_manager.py
@@ -11,7 +11,7 @@ logger = get_logger(__name__)
# Папка, где располагаются все дополнительные темы
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
THEMES_DIRS = [
- os.path.join(xdg_data_home, "PortProtonQT", "themes"),
+ os.path.join(xdg_data_home, "PortProtonQt", "themes"),
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
]
diff --git a/portprotonqt/themes/standart-light/metainfo.ini b/portprotonqt/themes/standart-light/metainfo.ini
index 1b5282d..e899572 100644
--- a/portprotonqt/themes/standart-light/metainfo.ini
+++ b/portprotonqt/themes/standart-light/metainfo.ini
@@ -1,5 +1,5 @@
[Metainfo]
author = BlackSnaker
author_link =
-description = Стандартная тема PortProtonQT (светлый вариант)
+description = Стандартная тема PortProtonQt (светлый вариант)
name = Light
diff --git a/portprotonqt/themes/standart-light/styles.py b/portprotonqt/themes/standart-light/styles.py
index 84d5da4..83f2435 100644
--- a/portprotonqt/themes/standart-light/styles.py
+++ b/portprotonqt/themes/standart-light/styles.py
@@ -416,6 +416,26 @@ def get_protondb_badge_style(tier):
font-weight: bold;
"""
+def get_anticheat_badge_style(status):
+ status = status.lower()
+ status_colors = {
+ "supported": {"background": "rgba(102, 168, 15, 0.7)", "color": "black"},
+ "running": {"background": "rgba(25, 113, 194, 0.7)", "color": "black"},
+ "planned": {"background": "rgba(156, 54, 181, 0.7)", "color": "black"},
+ "broken": {"background": "rgba(232, 89, 12, 0.7)", "color": "black"},
+ "denied": {"background": "rgba(224, 49, 49, 0.7)", "color": "black"}
+ }
+ colors = status_colors.get(status, {"background": "rgba(0, 0, 0, 0.5)", "color": "white"})
+ return f"""
+ qproperty-alignment: AlignCenter;
+ background-color: {colors["background"]};
+ color: {colors["color"]};
+ font-size: 14px;
+ border-radius: 5px;
+ font-family: 'Poppins';
+ font-weight: bold;
+ """
+
# СТИЛИ БЕЙДЖА STEAM
STEAM_BADGE_STYLE= """
qproperty-alignment: AlignCenter;
diff --git a/portprotonqt/themes/standart/metainfo.ini b/portprotonqt/themes/standart/metainfo.ini
index 3814979..78b9340 100644
--- a/portprotonqt/themes/standart/metainfo.ini
+++ b/portprotonqt/themes/standart/metainfo.ini
@@ -1,5 +1,5 @@
[Metainfo]
author = Dervart
author_link =
-description = Стандартная тема PortProtonQT (тёмный вариант)
+description = Стандартная тема PortProtonQt (тёмный вариант)
name = Clean Dark
diff --git a/portprotonqt/themes/standart/styles.py b/portprotonqt/themes/standart/styles.py
index b34d6ca..9e9b288 100644
--- a/portprotonqt/themes/standart/styles.py
+++ b/portprotonqt/themes/standart/styles.py
@@ -8,6 +8,40 @@ current_theme_name = read_theme_from_config()
favoriteLabelSize = 48, 48
pixmapsScaledSize = 60, 60
+CONTEXT_MENU_STYLE = """
+ QMenu {
+ background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
+ stop:0 rgba(40, 40, 40, 0.95),
+ stop:1 rgba(25, 25, 25, 0.95));
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 12px;
+ color: #ffffff;
+ font-family: 'Play';
+ font-size: 16px;
+ padding: 5px;
+ }
+ QMenu::item {
+ padding: 8px 20px;
+ background: transparent;
+ border-radius: 8px;
+ color: #ffffff;
+ }
+ QMenu::item:selected {
+ background: #282a33;
+ color: #09bec8;
+ }
+ QMenu::item:hover {
+ background: #282a33;
+ color: #09bec8;
+ }
+ QMenu::item:focus {
+ background: #409EFF;
+ color: #ffffff;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 8px;
+ }
+"""
+
# СТИЛЬ ШАПКИ ГЛАВНОГО ОКНА
MAIN_WINDOW_HEADER_STYLE = """
QFrame {
@@ -90,6 +124,13 @@ SEARCH_EDIT_STYLE = """
}
"""
+SETTINGS_CHECKBOX_STYLE = """
+ QCheckBox:focus {
+ border: 2px solid #409EFF;
+ background: #404554;
+ }
+"""
+
# ОТКЛЮЧАЕМ РАМКУ У QScrollArea
SCROLL_AREA_STYLE = """
QWidget {
@@ -207,6 +248,28 @@ ACTION_BUTTON_STYLE = """
}
"""
+# СТИЛЬ КНОПОК ОВЕРЛЕЯ
+OVERLAY_BUTTON_STYLE = """
+ QPushButton {
+ background: #3f424d;
+ border: 1px solid rgba(255, 255, 255, 0.20);
+ border-radius: 10px;
+ color: #ffffff;
+ font-size: 16px;
+ font-family: 'Play';
+ }
+ QPushButton:hover {
+ background: #282a33;
+ }
+ QPushButton:pressed {
+ background: #282a33;
+ }
+ QPushButton:focus {
+ border: 2px solid #409EFF;
+ background-color: #404554;
+ }
+"""
+
# ТЕКСТОВЫЕ СТИЛИ: ЗАГОЛОВКИ И ОСНОВНОЙ КОНТЕНТ
TAB_TITLE_STYLE = "font-family: 'Play'; font-size: 24px; color: #ffffff; background-color: none;"
CONTENT_STYLE = """
@@ -416,6 +479,27 @@ def get_protondb_badge_style(tier):
font-weight: bold;
"""
+# СТИЛИ БЕЙДЖА WEANTICHEATYET
+def get_anticheat_badge_style(status):
+ status = status.lower()
+ status_colors = {
+ "supported": {"background": "rgba(102, 168, 15, 0.7)", "color": "black"},
+ "running": {"background": "rgba(25, 113, 194, 0.7)", "color": "black"},
+ "planned": {"background": "rgba(156, 54, 181, 0.7)", "color": "black"},
+ "broken": {"background": "rgba(232, 89, 12, 0.7)", "color": "black"},
+ "denied": {"background": "rgba(224, 49, 49, 0.7)", "color": "black"}
+ }
+ colors = status_colors.get(status, {"background": "rgba(0, 0, 0, 0.5)", "color": "white"})
+ return f"""
+ qproperty-alignment: AlignCenter;
+ background-color: {colors["background"]};
+ color: {colors["color"]};
+ font-size: 16px;
+ border-radius: 5px;
+ font-family: 'Play';
+ font-weight: bold;
+ """
+
# СТИЛИ БЕЙДЖА STEAM
STEAM_BADGE_STYLE= """
qproperty-alignment: AlignCenter;
@@ -457,6 +541,10 @@ MESSAGE_BOX_STYLE = """
background: #09bec8;
border-color: rgba(255, 255, 255, 0.3);
}
+ QMessageBox QPushButton:focus {
+ border: 2px solid #409EFF;
+ background: #404554;
+ }
"""
# СТИЛИ ДЛЯ ВКЛАДКИ НАСТРОЕК PORTPROTON
diff --git a/portprotonqt/time_utils.py b/portprotonqt/time_utils.py
index 71fc463..5829d89 100644
--- a/portprotonqt/time_utils.py
+++ b/portprotonqt/time_utils.py
@@ -10,7 +10,7 @@ logger = get_logger(__name__)
def get_cache_file_path():
"""Возвращает путь к файлу кеша portproton_last_launch."""
cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
- return os.path.join(cache_home, "PortProtonQT", "last_launch")
+ return os.path.join(cache_home, "PortProtonQt", "last_launch")
def save_last_launch(exe_name, launch_time):
"""