forked from Boria138/PortProtonQt
Compare commits
30 Commits
647394ca92
...
84708ed260
Author | SHA1 | Date | |
---|---|---|---|
84708ed260
|
|||
9fe5a8315a
|
|||
c1b8eac127
|
|||
953e4fa715
|
|||
24ca66a1af
|
|||
30a4fc6ed7
|
|||
2d7369d46c
|
|||
0587cf58ed
|
|||
58c7541fa3
|
|||
b9d7fc2326
|
|||
7e9a0be150
|
|||
4e057c204c
|
|||
b35a1b8dfe
|
|||
cc8c22e972
|
|||
b025e0bbcf
|
|||
149e80fa33
|
|||
9373aa1329
|
|||
61f655c08f
|
|||
fce2ef2d0d
|
|||
4a8033a0b7
|
|||
61680ed97f
|
|||
6fa145ee13
|
|||
b0ec4487ca
|
|||
68a52d6980
|
|||
61115411e7
|
|||
55c32457d6
|
|||
b965b23a50
|
|||
e1d7bca05e
|
|||
23bcae32d2
|
|||
08f4a0215b
|
65
CHANGELOG.md
65
CHANGELOG.md
@@ -8,52 +8,56 @@
|
||||
### Added
|
||||
- Кнопки сброса настроек и очистки кэша
|
||||
- Бейдж PortProton
|
||||
- Зависимость на `xdg-utils`
|
||||
- Зависимость от `xdg-utils`
|
||||
- Интеграция статуса WeAntiCheatYet в карточку
|
||||
- Стили в AddGameDialog
|
||||
- Переключение полноэкранного режима через F11 или Select на геймпаде
|
||||
- Выбор QCheckBox через Enter или кнопку A геймпада
|
||||
- Закрытие диалога добавления игры через ESC или кнопку B геймпада
|
||||
- Стили в AddGameDialog
|
||||
- Переключение полноэкранного режима через F11 или кнопку Select на геймпаде
|
||||
- Выбор QCheckBox через Enter или кнопку A на геймпаде
|
||||
- Закрытие диалога добавления игры через ESC или кнопку B на геймпаде
|
||||
- Закрытие окна приложения по комбинации клавиш Ctrl+Q
|
||||
- Сохранение и восстановление размера при рестарте
|
||||
- Сохранение и восстановление размера окна при перезапуске
|
||||
- Переключатель полноэкранного режима приложения
|
||||
- Пункт в контекстное меню “Открыть папку игры”
|
||||
- Пункт в контекстное меню “Добавить в Steam”
|
||||
- Пункт в контекстное меню "Удалить из Steam”
|
||||
- Метод сортировки сначала избранное
|
||||
- Настройка автоматического перехода в режим полноэкранного отображения приложения при подключении геймпада (по умолчанию отключено)
|
||||
- Обработчики для QMenu и QComboBox на геймпаде
|
||||
- Пункт в контекстном меню «Открыть папку игры»
|
||||
- Пункты в контекстном меню «Добавить в Steam» и «Удалить из Steam»
|
||||
- Пункты в контекстном меню «Добавить в Избранное» и «Удалить из Избранного» для переключения статуса избранного через геймпад
|
||||
- Метод сортировки «Сначала избранное»
|
||||
- Настройка автоматического перехода в полноэкранный режим при подключении геймпада (по умолчанию отключена)
|
||||
- Обработчики для QMenu и QComboBox при управлении геймпадом
|
||||
- Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме
|
||||
- Оверлей на кнопку 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: например, при нажатии D-pad вниз фокус переходит на карточку в следующей колонке, а не по порядку
|
||||
- Теперь D-pad можно зажимать для переключения карточек
|
||||
- D-pad больше не переключает вкладки только RB и LB
|
||||
- D-pad больше не переключает вкладки, только RB и LB
|
||||
- Кнопка добавления игры больше не фокусируется
|
||||
- Диалог добавления игры теперь открывается только в библиотеке
|
||||
- Аргумент --fullscreen для открытия приложения в режиме полноэкранного отображения
|
||||
- Оверлей на кнопку Xbox / PS для закрытия приложения, выключения, перезагрузки и ухода в сон
|
||||
- Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt
|
||||
|
||||
### Fixed
|
||||
- Обработка несуществующей темы с возвратом к “standart”
|
||||
- Обработка несуществующей темы с возвратом к «standard»
|
||||
- Открытие контекстного меню
|
||||
- Запуск при отсутствии exiftool
|
||||
- Переводы пунктов настроек
|
||||
- Бесконечное обращение к get_portproton_location
|
||||
- Бесконечное обращение к `get_portproton_location`
|
||||
- Ссылки на документацию в README
|
||||
- traceback при загрузке placeholder при отсутствии обложек
|
||||
- Traceback при загрузке placeholder при отсутствии обложек
|
||||
- Утечки памяти при загрузке обложек
|
||||
- Ошибки при подключении геймпада из-за работы в разных потоках
|
||||
- Множественное открытие диалога добавления игры на геймпаде
|
||||
- Многократное открытие диалога добавления игры при использовании геймпада
|
||||
- Перехват событий геймпада во время работы игры
|
||||
|
||||
---
|
||||
@@ -67,16 +71,15 @@
|
||||
- Сборка AppImage
|
||||
|
||||
### Changed
|
||||
- Удалён жёстко заданный ресайз окна
|
||||
- Использован icoextract как python модуль
|
||||
- Удалён жёстко заданный размер окна
|
||||
- Использован `icoextract` как Python-модуль
|
||||
|
||||
### Fixed
|
||||
- Скрытие статус-бара
|
||||
- Чтение списка Steam-игр
|
||||
- Подвисание GUI
|
||||
- Краш при повреждённом Steam
|
||||
- Зависание GUI
|
||||
- Сбой при повреждённом Steam
|
||||
|
||||
---
|
||||
|
||||
|
||||
> См. подробности по каждому коммиту в истории репозитория.
|
||||
|
118
README.md
118
README.md
@@ -1,67 +1,73 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/Castro-Fidel/PortWINE/master/data_from_portwine/img/gui/portproton.svg" width="64">
|
||||
<img src="https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/theme_logo.svg" width="64">
|
||||
<h1 align="center">PortProtonQt</h1>
|
||||
<p align="center">Современный, удобный графический интерфейс, написанный с использованием PySide6(Qt6) и предназначенный для упрощения управления и запуска игр на различных платформах, включая PortProton, Steam и Epic Games Store.</p>
|
||||
<p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p>
|
||||
</div>
|
||||
|
||||
|
||||
## В планах
|
||||
|
||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||
- [X] Добавить возможность управление с геймпада
|
||||
- [ ] Добавить возможность управление с тачскрина
|
||||
- [X] Добавить возможность управление с мыши и клавиатуры
|
||||
- [X] Добавить возможность управления с геймпада
|
||||
- [ ] Добавить возможность управления с тачскрина
|
||||
- [X] Добавить возможность управления с мыши и клавиатуры
|
||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||
- [X] Вынести все константы такие как уровень закругления карточек в темы (Частично вынесено)
|
||||
- [X] Добавить метадату для тем (скришоты, описание, домащняя страница и автор)
|
||||
- [ ] Продумать систему вкладок вместо той что есть сейчас
|
||||
- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
|
||||
- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
|
||||
- [ ] Переделать скриншоты для соответсвия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||
- [X] Брать описание и названия игр с базы данных Steam
|
||||
- [X] Брать обложки для игр со SteamGridDB или CDN Steam
|
||||
- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска
|
||||
- [X] Улучшить функцию поиска SteamApi что бы исправить некорректное определение ID (Graven определается как ENGRAVEN или GRAVENFALL, Spore определается как SporeBound или Spore Valley)
|
||||
- [ ] Убрать логи со SteamApi в релизной версии потому что логи замедляют код
|
||||
- [X] Что-то придумать с ограничением SteamApi в 50 тысяч игр за один запрос (иногда туда не попадают нужные игры и остаются без обложки)
|
||||
- [X] Избавится от любого вызова yad
|
||||
- [X] Написать свою реализацию запрета ухода в сон, а не использовать ту что в PortProton (Оставим это [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
|
||||
- [X] Написать свою реализацию трея, а не использовать ту что в PortProton
|
||||
- [X] Добавить в поиск экранную клавиатуру (Реализовавывать собственную клавиатуру слишком затратно, лучше положится на встроенную в DE клавиатуру malit в KDE, gjs-osk в GNOME,Squeekboard в phosh, стимовская в SteamOS и так далее)
|
||||
- [X] Добавить сортировку карточек по различным критериям (сейчас есть: недавние, кол-во наиграного времени, избранное или по алфавиту)
|
||||
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
||||
- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор)
|
||||
- [ ] Продумать систему вкладок вместо текущей
|
||||
- [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
|
||||
@@ -71,6 +77,12 @@ source .venv/bin/activate
|
||||
|
||||
Запуск производится по команде portprotonqt
|
||||
|
||||
### Установка (release)
|
||||
|
||||
Выберите подходящий пакет для вашей системы или AppImage.
|
||||
|
||||
Запуск производится по команде portprotonqt или по ярлыку в меню
|
||||
|
||||
### Разработка
|
||||
|
||||
В проект встроен линтер (ruff), статический анализатор (pyright) и проверка lock файла, если эти проверки не пройдут PR не будет принят, поэтому перед коммитом введите такую команду
|
||||
@@ -90,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) корректная работоспособность не гарантирована
|
||||
|
@@ -7,7 +7,11 @@
|
||||
<summary>Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store</summary>
|
||||
<summary xml:lang="ru">Современный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store</summary>
|
||||
<description>
|
||||
<p>This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.</p>
|
||||
<p>
|
||||
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.</p>
|
||||
<p xml:lang="ru">
|
||||
Это приложение предоставляет стильный и интуитивно понятный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет ваши игровые библиотеки в одном удобном хабе для простой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают целостный игровой опыт, устраняя необходимость в использовании нескольких лаунчеров. Уникальная интеграция с PortProton улучшает игровой процесс на Linux, позволяя с лёгкостью запускать Windows-игры с минимальными настройками.
|
||||
</p>
|
||||
</description>
|
||||
<launchable type="desktop-id">ru.linux_gaming.PortProtonQt.desktop</launchable>
|
||||
<developer id="ru.linux_gaming">
|
||||
@@ -41,7 +45,7 @@
|
||||
<caption xml:lang="ru">Детали игры</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/src/commit/9c4ad0b7bacac08849aff9036561de7b88a9bad2/portprotonqt/themes/standart/images/screenshots/%D0%9D%D0%B0%D1%81%D1%82%D1%80%D0%BE%D0%B9%D0%BA%D0%B8.png</image>
|
||||
<image>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</image>
|
||||
<caption>Settings</caption>
|
||||
<caption xml:lang="ru">Настройки</caption>
|
||||
</screenshot>
|
||||
|
@@ -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")
|
||||
|
@@ -20,9 +20,9 @@ Current translation status:
|
||||
|
||||
| Locale | Progress | Translated |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 153 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 153 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 of 153 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 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 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -20,9 +20,9 @@
|
||||
|
||||
| Локаль | Прогресс | Переведено |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 153 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 153 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 153 из 153 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 из 162 |
|
||||
|
||||
---
|
||||
|
||||
|
@@ -7,7 +7,7 @@ def parse_args():
|
||||
"""
|
||||
Парсит аргументы командной строки.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="PortProtonQT CLI")
|
||||
parser = argparse.ArgumentParser(description="PortProtonQt CLI")
|
||||
parser.add_argument(
|
||||
"--fullscreen",
|
||||
action="store_true",
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -6,13 +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):
|
||||
"""
|
||||
@@ -41,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")
|
||||
@@ -80,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:
|
||||
@@ -226,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)
|
||||
@@ -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()
|
||||
|
@@ -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}")
|
||||
|
@@ -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):
|
||||
|
@@ -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/"):
|
||||
|
@@ -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, QMenu, QComboBox, QListView
|
||||
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__)
|
||||
|
||||
@@ -34,25 +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},
|
||||
'next_tab': {ecodes.BTN_TR},
|
||||
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
|
||||
'context_menu': {ecodes.BTN_START},
|
||||
'menu': {ecodes.BTN_SELECT},
|
||||
'guide': {ecodes.BTN_MODE},
|
||||
'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
|
||||
@@ -82,6 +82,7 @@ class InputManager(QObject):
|
||||
self.gamepad_thread: threading.Thread | None = None
|
||||
self.running = True
|
||||
self._is_fullscreen = read_fullscreen_config()
|
||||
self.rumble_effect_id: int | None = None # Store the rumble effect ID
|
||||
|
||||
# Add variables for continuous D-pad movement
|
||||
self.dpad_timer = QTimer(self)
|
||||
@@ -125,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:
|
||||
@@ -147,19 +190,19 @@ class InputManager(QObject):
|
||||
|
||||
# Handle QMenu (context menu)
|
||||
if isinstance(popup, QMenu):
|
||||
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
|
||||
if button_code in BUTTONS['confirm']:
|
||||
if popup.activeAction():
|
||||
popup.activeAction().trigger()
|
||||
popup.close()
|
||||
return
|
||||
elif button_code in BUTTONS['back'] or button_code in BUTTONS['menu']:
|
||||
elif button_code in BUTTONS['back']:
|
||||
popup.close()
|
||||
return
|
||||
return
|
||||
|
||||
# Handle QComboBox
|
||||
if isinstance(focused, QComboBox):
|
||||
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
|
||||
if button_code in BUTTONS['confirm']:
|
||||
focused.showPopup()
|
||||
return
|
||||
|
||||
@@ -173,7 +216,7 @@ class InputManager(QObject):
|
||||
break
|
||||
parent = parent.parentWidget()
|
||||
|
||||
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
|
||||
if button_code in BUTTONS['confirm']:
|
||||
idx = focused.currentIndex()
|
||||
if idx.isValid():
|
||||
if combo:
|
||||
@@ -219,18 +262,18 @@ class InputManager(QObject):
|
||||
return
|
||||
|
||||
# Game launch on detail page
|
||||
if (button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None:
|
||||
if (button_code in BUTTONS['confirm']) and self._parent.currentDetailPage is not None and self._parent.current_add_game_dialog is None:
|
||||
if self._parent.current_exec_line:
|
||||
self.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']:
|
||||
# Only open AddGameDialog if in library tab (index 0)
|
||||
if self._parent.stackedWidget.currentIndex() == 0:
|
||||
self._parent.openAddGameDialog()
|
||||
elif button_code in BUTTONS['prev_tab']:
|
||||
@@ -284,8 +327,22 @@ class InputManager(QObject):
|
||||
self.dpad_timer.stop() # Stop timer when D-pad is released
|
||||
return
|
||||
|
||||
# Handle SystemOverlay or AddGameDialog navigation with D-pad
|
||||
if isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0:
|
||||
# Handle SystemOverlay, AddGameDialog, or QMessageBox navigation with D-pad
|
||||
if isinstance(active, QDialog) and code == ecodes.ABS_HAT0X and value != 0:
|
||||
if isinstance(active, QMessageBox): # Specific handling for QMessageBox
|
||||
if not focused or not active.focusWidget():
|
||||
# If no widget is focused, focus the first focusable widget
|
||||
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
||||
if focusables:
|
||||
focusables[0].setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
return
|
||||
if value > 0: # Right
|
||||
active.focusNextChild()
|
||||
elif value < 0: # Left
|
||||
active.focusPreviousChild()
|
||||
return
|
||||
elif isinstance(active, QDialog) and code == ecodes.ABS_HAT0Y and value != 0: # Keep up/down for other dialogs
|
||||
if not focused or not active.focusWidget():
|
||||
# If no widget is focused, focus the first focusable widget
|
||||
focusables = active.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||
@@ -477,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()
|
||||
@@ -708,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)
|
||||
@@ -726,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()
|
||||
@@ -739,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()
|
||||
@@ -775,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)
|
||||
@@ -793,6 +856,7 @@ class InputManager(QObject):
|
||||
finally:
|
||||
if self.gamepad:
|
||||
try:
|
||||
self.stop_rumble()
|
||||
self.gamepad.close()
|
||||
except Exception:
|
||||
pass
|
||||
@@ -802,6 +866,7 @@ class InputManager(QObject):
|
||||
try:
|
||||
self.running = False
|
||||
self.dpad_timer.stop()
|
||||
self.stop_rumble()
|
||||
if self.gamepad_thread:
|
||||
self.gamepad_thread.join()
|
||||
if self.gamepad:
|
||||
|
Binary file not shown.
@@ -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 <EMAIL@ADDRESS>, 2025.
|
||||
#
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:31+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 <EMAIL@ADDRESS>\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,6 +376,12 @@ msgstr ""
|
||||
msgid "Auto Fullscreen on Gamepad connected:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad haptic feedback"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad haptic feedback:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save Settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -489,6 +509,12 @@ msgstr ""
|
||||
msgid "Exit Application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Return to Desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "portprotonqt-session-select file not found at /usr/bin/"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
@@ -501,6 +527,9 @@ msgstr ""
|
||||
msgid "Failed to suspend the system"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to return to desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "just now"
|
||||
msgstr ""
|
||||
|
||||
|
Binary file not shown.
@@ -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 <EMAIL@ADDRESS>, 2025.
|
||||
#
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:31+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 <EMAIL@ADDRESS>\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,6 +376,12 @@ msgstr ""
|
||||
msgid "Auto Fullscreen on Gamepad connected:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad haptic feedback"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad haptic feedback:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save Settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -489,6 +509,12 @@ msgstr ""
|
||||
msgid "Exit Application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Return to Desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "portprotonqt-session-select file not found at /usr/bin/"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
@@ -501,6 +527,9 @@ msgstr ""
|
||||
msgid "Failed to suspend the system"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to return to desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "just now"
|
||||
msgstr ""
|
||||
|
||||
|
@@ -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 <EMAIL@ADDRESS>, 2025.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PortProtonQT 0.1.1\n"
|
||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:31+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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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,6 +374,12 @@ msgstr ""
|
||||
msgid "Auto Fullscreen on Gamepad connected:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad haptic feedback"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad haptic feedback:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save Settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -487,6 +507,12 @@ msgstr ""
|
||||
msgid "Exit Application"
|
||||
msgstr ""
|
||||
|
||||
msgid "Return to Desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "portprotonqt-session-select file not found at /usr/bin/"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
@@ -499,6 +525,9 @@ msgstr ""
|
||||
msgid "Failed to suspend the system"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to return to desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "just now"
|
||||
msgstr ""
|
||||
|
||||
|
Binary file not shown.
@@ -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 <EMAIL@ADDRESS>, 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-08 09:31+0500\n"
|
||||
"PO-Revision-Date: 2025-06-08 09:31+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 <LL@li.org>\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,6 +383,12 @@ msgstr "Режим полноэкранного отображения прил
|
||||
msgid "Auto Fullscreen on Gamepad connected:"
|
||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
|
||||
|
||||
msgid "Gamepad haptic feedback"
|
||||
msgstr "Тактильная обратная связь на геймпаде"
|
||||
|
||||
msgid "Gamepad haptic feedback:"
|
||||
msgstr "Тактильная обратная связь на геймпаде:"
|
||||
|
||||
msgid "Save Settings"
|
||||
msgstr "Сохранить настройки"
|
||||
|
||||
@@ -498,6 +518,12 @@ msgstr "Перейти в ждущий режим"
|
||||
msgid "Exit Application"
|
||||
msgstr "Выйти из приложения"
|
||||
|
||||
msgid "Return to Desktop"
|
||||
msgstr "Вернуться на рабочий стол"
|
||||
|
||||
msgid "portprotonqt-session-select file not found at /usr/bin/"
|
||||
msgstr "portprotonqt-session-select не найдет"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
@@ -510,6 +536,9 @@ msgstr "Не удалось завершить работу системы"
|
||||
msgid "Failed to suspend the system"
|
||||
msgstr "Не удалось перейти в ждущий режим"
|
||||
|
||||
msgid "Failed to return to desktop"
|
||||
msgstr "Не удалось вернуться на рабочий стол"
|
||||
|
||||
msgid "just now"
|
||||
msgstr "только что"
|
||||
|
||||
|
@@ -26,7 +26,7 @@ from portprotonqt.config_utils import (
|
||||
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
|
||||
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
|
||||
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
|
||||
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad
|
||||
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
|
||||
)
|
||||
from portprotonqt.localization import _
|
||||
from portprotonqt.logger import get_logger
|
||||
@@ -44,7 +44,7 @@ from datetime import datetime
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Main window of PortProtonQT."""
|
||||
"""Main window of PortProtonQt."""
|
||||
settings_saved = Signal()
|
||||
games_loaded = Signal(list)
|
||||
update_progress = Signal(int) # Signal to update progress bar
|
||||
@@ -73,10 +73,10 @@ class MainWindow(QMainWindow):
|
||||
self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed)
|
||||
|
||||
read_time_config()
|
||||
# Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQT/legendary
|
||||
# Set LEGENDARY_CONFIG_PATH to ~/.cache/PortProtonQt/legendary_cache
|
||||
self.legendary_config_path = os.path.join(
|
||||
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
||||
"PortProtonQT", "legendary_cache"
|
||||
"PortProtonQt", "legendary_cache"
|
||||
)
|
||||
os.makedirs(self.legendary_config_path, exist_ok=True)
|
||||
os.environ["LEGENDARY_CONFIG_PATH"] = self.legendary_config_path
|
||||
@@ -98,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
|
||||
@@ -273,9 +274,10 @@ class MainWindow(QMainWindow):
|
||||
seen = set()
|
||||
games = []
|
||||
for game in portproton_games + steam_games:
|
||||
name = game[0]
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
# Уникальный ключ: имя + exec_line
|
||||
key = (game[0], game[4])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
games.append(game)
|
||||
self.games_loaded.emit(games)
|
||||
self._load_portproton_games_async(
|
||||
@@ -383,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 = ""
|
||||
@@ -543,10 +545,10 @@ class MainWindow(QMainWindow):
|
||||
"""Filters games based on search text and updates the grid."""
|
||||
text = self.searchEdit.text().strip().lower()
|
||||
if text == "":
|
||||
self.updateGameGrid() # Use self.games directly
|
||||
self.filtered_games = self.games
|
||||
else:
|
||||
filtered = [game for game in self.games if text in game[0].lower()]
|
||||
self.updateGameGrid(filtered)
|
||||
self.filtered_games = [game for game in self.games if text in game[0].lower()]
|
||||
self.updateGameGrid(self.filtered_games)
|
||||
|
||||
def createInstalledTab(self):
|
||||
self.gamesLibraryWidget = QWidget()
|
||||
@@ -620,22 +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)
|
||||
@@ -645,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,
|
||||
@@ -657,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)
|
||||
@@ -667,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."""
|
||||
@@ -761,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
|
||||
)
|
||||
@@ -953,7 +977,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# 5. Fullscreen setting for application
|
||||
self.fullscreenCheckBox = QCheckBox(_("Launch Application in Fullscreen"))
|
||||
#self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
self.fullscreenCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.fullscreenTitle = QLabel(_("Application Fullscreen Mode:"))
|
||||
self.fullscreenTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
@@ -965,6 +989,7 @@ class MainWindow(QMainWindow):
|
||||
# 6. Automatic fullscreen on gamepad connection
|
||||
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
||||
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
self.autoFullscreenGamepadTitle = QLabel(_("Auto Fullscreen on Gamepad connected:"))
|
||||
self.autoFullscreenGamepadTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.autoFullscreenGamepadTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
@@ -972,6 +997,17 @@ class MainWindow(QMainWindow):
|
||||
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
||||
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
||||
|
||||
# 7. Gamepad haptic feedback config
|
||||
self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
|
||||
self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
self.gamepadRumbleTitle = QLabel(_("Gamepad haptic feedback:"))
|
||||
self.gamepadRumbleTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.gamepadRumbleTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
current_rumble_state = read_rumble_config()
|
||||
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
||||
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
||||
|
||||
layout.addLayout(formLayout)
|
||||
|
||||
# Кнопки
|
||||
@@ -1092,6 +1128,10 @@ class MainWindow(QMainWindow):
|
||||
auto_fullscreen_gamepad = self.autoFullscreenGamepadCheckBox.isChecked()
|
||||
save_auto_fullscreen_gamepad(auto_fullscreen_gamepad)
|
||||
|
||||
# Сохранение настройки виброотдачи геймпада
|
||||
rumble_enabled = self.gamepadRumbleCheckBox.isChecked()
|
||||
save_rumble_config(rumble_enabled)
|
||||
|
||||
for card in self.game_card_cache.values():
|
||||
card.update_badge_visibility(filter_key)
|
||||
|
||||
@@ -1208,11 +1248,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)
|
||||
|
||||
@@ -1230,14 +1274,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):
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -3,6 +3,7 @@ from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QMessageBox
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import Qt
|
||||
from portprotonqt.logger import get_logger
|
||||
import os
|
||||
from portprotonqt.localization import _
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -22,35 +23,47 @@ class SystemOverlay(QDialog):
|
||||
|
||||
# Reboot button
|
||||
reboot_button = QPushButton(_("Reboot"))
|
||||
#reboot_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
reboot_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
|
||||
reboot_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
reboot_button.clicked.connect(self.reboot)
|
||||
layout.addWidget(reboot_button)
|
||||
|
||||
# Shutdown button
|
||||
shutdown_button = QPushButton(_("Shutdown"))
|
||||
#shutdown_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
shutdown_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
|
||||
shutdown_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
shutdown_button.clicked.connect(self.shutdown)
|
||||
layout.addWidget(shutdown_button)
|
||||
|
||||
# Suspend button
|
||||
suspend_button = QPushButton(_("Suspend"))
|
||||
#suspend_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
suspend_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
|
||||
suspend_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
suspend_button.clicked.connect(self.suspend)
|
||||
layout.addWidget(suspend_button)
|
||||
|
||||
# Exit application button
|
||||
exit_button = QPushButton(_("Exit Application"))
|
||||
#exit_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
||||
exit_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
|
||||
exit_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
exit_button.clicked.connect(self.exit_application)
|
||||
layout.addWidget(exit_button)
|
||||
|
||||
# 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.ACTION_BUTTON_STYLE)
|
||||
cancel_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
|
||||
cancel_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
cancel_button.clicked.connect(self.reject)
|
||||
layout.addWidget(cancel_button)
|
||||
@@ -82,6 +95,15 @@ class SystemOverlay(QDialog):
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to suspend the system"))
|
||||
self.accept()
|
||||
|
||||
def return_to_desktop(self):
|
||||
try:
|
||||
script_path = os.path.join(os.path.dirname(__file__), "portprotonqt-session-select")
|
||||
subprocess.run([script_path, "desktop"], check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to return to desktop: {e}")
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to return to desktop"))
|
||||
self.accept()
|
||||
|
||||
def exit_application(self):
|
||||
QApplication.quit()
|
||||
self.accept()
|
||||
|
@@ -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")
|
||||
]
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
[Metainfo]
|
||||
author = BlackSnaker
|
||||
author_link =
|
||||
description = Стандартная тема PortProtonQT (светлый вариант)
|
||||
description = Стандартная тема PortProtonQt (светлый вариант)
|
||||
name = Light
|
||||
|
@@ -1,5 +1,5 @@
|
||||
[Metainfo]
|
||||
author = Dervart
|
||||
author_link =
|
||||
description = Стандартная тема PortProtonQT (тёмный вариант)
|
||||
description = Стандартная тема PortProtonQt (тёмный вариант)
|
||||
name = Clean Dark
|
||||
|
@@ -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 {
|
||||
@@ -206,6 +247,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 = """
|
||||
@@ -456,6 +519,10 @@ MESSAGE_BOX_STYLE = """
|
||||
background: #09bec8;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
QMessageBox QPushButton:focus {
|
||||
border: 2px solid #409EFF;
|
||||
background: #404554;
|
||||
}
|
||||
"""
|
||||
|
||||
# СТИЛИ ДЛЯ ВКЛАДКИ НАСТРОЕК PORTPROTON
|
||||
|
@@ -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):
|
||||
"""
|
||||
|
Reference in New Issue
Block a user