merge upstream
This commit is contained in:
66
CHANGELOG.md
66
CHANGELOG.md
@ -7,50 +7,57 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Кнопки сброса настроек и очистки кэша
|
- Кнопки сброса настроек и очистки кэша
|
||||||
- Начальная интеграция с EGS с помощью [Legendary](https://github.com/derrod/legendary)
|
|
||||||
- Бейдж EGS
|
|
||||||
- Бейдж PortProton
|
- Бейдж PortProton
|
||||||
- Зависимость на `xdg-utils`
|
- Зависимость от `xdg-utils`
|
||||||
- Интеграция статуса WeAntiCheatYet в карточку
|
- Интеграция статуса WeAntiCheatYet в карточку
|
||||||
- Стили в AddGameDialog
|
- Стили в AddGameDialog
|
||||||
- Переключение полноэкранного режима через F11 или Select на геймпаде
|
- Переключение полноэкранного режима через F11 или кнопку Select на геймпаде
|
||||||
- Выбор QCheckBox через Enter или кнопку A геймпада
|
- Выбор QCheckBox через Enter или кнопку A на геймпаде
|
||||||
- Закрытие диалога добавления игры через ESC или кнопку B геймпада
|
- Закрытие диалога добавления игры через ESC или кнопку B на геймпаде
|
||||||
- Закрытие окна приложения по комбинации клавиш Ctrl+Q
|
- Закрытие окна приложения по комбинации клавиш 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
|
### Changed
|
||||||
- Обновлены все иконки
|
- Обновлены все иконки
|
||||||
- Переименован `_get_steam_home` → `get_steam_home`
|
- Переименована функция `_get_steam_home` в `get_steam_home`
|
||||||
- Переименован `steam_game` → `game_source`
|
- Переименован `steam_game` в `game_source`
|
||||||
- Догика контекстного меню вынесена в `ContextMenuManager`
|
- Логика контекстного меню вынесена в `ContextMenuManager`
|
||||||
- Бейдж Steam теперь открывает Steam Community
|
- Бейдж Steam теперь открывает Steam Community
|
||||||
- Изменена лицензия с MIT на GPL-3.0 для совместимости с кодом от legendary
|
- Изменена лицензия с MIT на GPL-3.0 для совместимости с кодом от legendary
|
||||||
- Оптимизирована генерация карточек для предотвращения лагов при поиске и изменения размера окна
|
- Оптимизирована генерация карточек для предотвращения задержек при поиске и изменении размера окна
|
||||||
- Бейджи с карточек так же теперь дублируются и на странице с деталями, а не только в библиотеке
|
- Бейджи с карточек теперь отображаются также на странице с деталями, а не только в библиотеке
|
||||||
- Установка ширины бейджа в две трети ширины карточки
|
- Установлена ширина бейджа в две трети ширины карточки
|
||||||
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
|
- Бейджи источников (`Steam`, `EGS`, `PortProton`) теперь отображаются только при активном фильтре `all` или `favorites`
|
||||||
- Карточки теперь фокусируются в направлении движения стрелок или D-pad, например если нажать D-pad вниз то перейдёшь на карточку со следующей колонки, а не по порядку
|
- Карточки теперь фокусируются в направлении движения стрелок или D-pad: например, при нажатии D-pad вниз фокус переходит на карточку в следующей колонке, а не по порядку
|
||||||
- D-pad больше не переключает вкладки только RB и LB
|
- Теперь D-pad можно зажимать для переключения карточек
|
||||||
|
- D-pad больше не переключает вкладки, только RB и LB
|
||||||
- Кнопка добавления игры больше не фокусируется
|
- Кнопка добавления игры больше не фокусируется
|
||||||
|
- Диалог добавления игры теперь открывается только в библиотеке
|
||||||
|
- Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Обработка несуществующей темы с возвратом к “standart”
|
- Обработка несуществующей темы с возвратом к «standard»
|
||||||
- Открытие контекстного меню
|
- Открытие контекстного меню
|
||||||
- Запуск при отсутствии exiftool
|
- Запуск при отсутствии exiftool
|
||||||
- Переводы пунктов настроек
|
- Переводы пунктов настроек
|
||||||
- Бесконечное обращение к get_portproton_location
|
- Бесконечное обращение к `get_portproton_location`
|
||||||
- Ссылки на документацию в README
|
- Ссылки на документацию в README
|
||||||
- traceback при загрузке placeholder при отсутствии обложек
|
- Traceback при загрузке placeholder при отсутствии обложек
|
||||||
- Утечки памяти при загрузке обложек
|
- Утечки памяти при загрузке обложек
|
||||||
- Ошибки при подключении геймпада из-за работы в разных потоках
|
- Ошибки при подключении геймпада из-за работы в разных потоках
|
||||||
- Множественное открытие диалога добавления игры на геймпаде
|
- Многократное открытие диалога добавления игры при использовании геймпада
|
||||||
- Перехват событий геймпада во время работы игры
|
- Перехват событий геймпада во время работы игры
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -64,16 +71,15 @@
|
|||||||
- Сборка AppImage
|
- Сборка AppImage
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Удалён жёстко заданный ресайз окна
|
- Удалён жёстко заданный размер окна
|
||||||
- Использован icoextract как python модуль
|
- Использован `icoextract` как Python-модуль
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Скрытие статус-бара
|
- Скрытие статус-бара
|
||||||
- Чтение списка Steam-игр
|
- Чтение списка Steam-игр
|
||||||
- Подвисание GUI
|
- Зависание GUI
|
||||||
- Краш при повреждённом Steam
|
- Сбой при повреждённом Steam
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
> См. подробности по каждому коммиту в истории репозитория.
|
> См. подробности по каждому коммиту в истории репозитория.
|
||||||
|
117
README.md
117
README.md
@ -1,66 +1,73 @@
|
|||||||
<div align="center">
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
## В планах
|
## В планах
|
||||||
|
|
||||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||||
- [ ] Добавить возможность управление с геймпада
|
- [X] Добавить возможность управления с геймпада
|
||||||
- [ ] Добавить возможность управление с тачскрина
|
- [ ] Добавить возможность управления с тачскрина
|
||||||
- [X] Добавить возможность управление с мыши и клавиатуры
|
- [X] Добавить возможность управления с мыши и клавиатуры
|
||||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||||
- [X] Вынести все константы такие как уровень закругления карточек в темы (Частично вынесено)
|
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
||||||
- [X] Добавить метадату для тем (скришоты, описание, домащняя страница и автор)
|
- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор)
|
||||||
- [ ] Продумать систему вкладок вместо той что есть сейчас
|
- [ ] Продумать систему вкладок вместо текущей
|
||||||
- [ ] Добавить Gamescope сессию на подобие той что есть в SteamOS
|
- [X] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
|
||||||
- [ ] Написать адаптивный дизайн (За эталон берём SteamDeck с разрешением 1280х800)
|
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
||||||
- [X] Брать описание и названия игр с базы данных Steam
|
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||||
- [X] Брать обложки для игр со SteamGridDB или CDN Steam
|
- [X] Получать описания и названия игр из базы данных Steam
|
||||||
- [X] Оптимизировать работу со SteamApi что бы ускорить время запуска
|
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
|
||||||
- [X] Улучшить функцию поиска SteamApi что бы исправить некорректное определение ID (Graven определается как ENGRAVEN или GRAVENFALL, Spore определается как SporeBound или Spore Valley)
|
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
||||||
- [ ] Убрать логи со SteamApi в релизной версии потому что логи замедляют код
|
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
||||||
- [X] Что-то придумать с ограничением SteamApi в 50 тысяч игр за один запрос (иногда туда не попадают нужные игры и остаются без обложки)
|
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||||
- [X] Избавится от любого вызова yad
|
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
||||||
- [X] Написать свою реализацию запрета ухода в сон, а не использовать ту что в PortProton (Оставим это [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
|
- [X] Избавиться от вызовов yad
|
||||||
- [X] Написать свою реализацию трея, а не использовать ту что в PortProton
|
- [X] Реализовать собственный механизм запрета ухода в спящий режим вместо использования механизма PortProton (оставлено для [PortProton 2.0](https://github.com/Castro-Fidel/PortProton_2.0))
|
||||||
- [X] Добавить в поиск экранную клавиатуру (Реализовавывать собственную клавиатуру слишком затратно, лучше положится на встроенную в DE клавиатуру malit в KDE, gjs-osk в GNOME,Squeekboard в phosh, стимовская в SteamOS и так далее)
|
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
||||||
- [X] Добавить сортировку карточек по различным критериям (сейчас есть: недавние, кол-во наиграного времени, избранное или по алфавиту)
|
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
||||||
|
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
||||||
- [X] Добавить индикацию запуска приложения
|
- [X] Добавить индикацию запуска приложения
|
||||||
- [X] Достичь паритета функционала с Ingame
|
- [X] Достигнуть паритета функциональности с Ingame
|
||||||
- [ ] Достичь паритета функционала с PortProton
|
- [ ] Достигнуть паритета функциональности с PortProton
|
||||||
- [X] Добавить возможность изменения названия, описания и обложки через файлы .local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}
|
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
||||||
- [X] Добавить встроенное переопределение имени, описания и обложки, например по пути portprotonqt/custom_data [Документация](documentation/metadata_override/)
|
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
||||||
- [X] Добавить в карточку игры сведения о поддержке геймадов
|
- [X] Добавить в карточку игры сведения о поддержке геймпада
|
||||||
- [X] Добавить в карточки данные с ProtonDB
|
- [X] Добавить в карточки данные с ProtonDB
|
||||||
- [X] Добавить в карточки данные с Are We Anti-Cheat Yet?
|
- [X] Добавить в карточки данные с AreWeAntiCheatYet
|
||||||
- [X] Продублировать бейджы с карточки на страницу с деталями игрыы
|
- [X] Продублировать бейджи с карточки на страницу с деталями игры
|
||||||
- [X] Добавить парсинг ярлыков со Steam
|
- [X] Добавить парсинг ярлыков из Steam
|
||||||
- [X] Добавить парсинг ярлыков с EGS
|
- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки)
|
||||||
- [ ] Избавится от бинарника legendary
|
- [ ] Избавиться от бинарника legendary
|
||||||
- [ ] Добавить запуск и скачивание игр с EGS
|
- [ ] Добавить запуск и скачивание игр из EGS
|
||||||
- [ ] Добавить авторизацию в EGS через WebView, а не вручную
|
- [ ] Добавить авторизацию в EGS через WebView вместо ручного ввода
|
||||||
- [X] Брать описания для игр с EGS из их [api](https://store-content.ak.epicgames.com/api)
|
- [X] Получать описания для игр из EGS через их [API](https://store-content.ak.epicgames.com/api)
|
||||||
- [X] Брать slug через Graphql [запрос](https://launcher.store.epicgames.com/graphql)
|
- [X] Получать slug через GraphQL [запрос](https://launcher.store.epicgames.com/graphql)
|
||||||
- [X] Добавить на карточку бейдж того что игра со стима
|
- [X] Добавить на карточку бейдж, указывающий, что игра из Steam
|
||||||
- [X] Добавить поддержку Flatpak и Snap версии Steam
|
- [X] Добавить поддержку версий Steam для Flatpak и Snap
|
||||||
- [X] Выводить данные о самом недавнем пользователе Steam, а не первом попавшемся
|
- [X] Отображать данные о самом последнем пользователе Steam, а не первом попавшемся
|
||||||
- [X] Исправить склонения в детальном выводе времени, например не 3 часов назад, а 3 часа назад
|
- [X] Исправить склонения в детальном выводе времени, например, не «3 часов назад», а «3 часа назад»
|
||||||
- [X] Добавить перевод через gettext [Документация](documentation/localization_guide)
|
- [X] Добавить перевод через gettext [Документация](documentation/localization_guide)
|
||||||
- [X] Писать описание игр и прочие данные на языке системы
|
- [X] Отображать описания игр и другие данные на языке системы
|
||||||
- [X] Добавить недокументированные параметры конфигурации в GUI (time detail_level, games sort_method, games display_filter)
|
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
||||||
- [X] Добавить систему избранного к карточкам
|
- [X] Добавить систему избранного для карточек
|
||||||
- [X] Заменить все print на logging
|
- [X] Заменить все `print` на `logging`
|
||||||
- [ ] Привести все логи к одному языку
|
- [ ] Привести все логи к единому языку
|
||||||
- [X] Стилизовать все элементы без стилей(QMessageBox, QSlider, QDialog)
|
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
||||||
- [X] Убрать жёсткую привязку путей на стрелочки QComboBox в styles.py
|
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
||||||
- [X] Исправить частичное применение тем на лету
|
- [X] Исправить частичное применение тем на лету
|
||||||
- [X] Исправить наложение подписей скриншотов при первом перелистывание в полноэкранном режиме
|
- [X] Исправить наложение подписей скриншотов при первом перелистывании в полноэкранном режиме
|
||||||
- [ ] Добавить GOG (?)
|
- [ ] Добавить поддержку GOG (?)
|
||||||
- [ ] Определится уже наконец с названием (PortProtonQt или PortProtonQT)
|
- [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
|
```sh
|
||||||
uv python install 3.10
|
uv python install 3.10
|
||||||
@ -70,6 +77,12 @@ source .venv/bin/activate
|
|||||||
|
|
||||||
Запуск производится по команде portprotonqt
|
Запуск производится по команде portprotonqt
|
||||||
|
|
||||||
|
### Установка (release)
|
||||||
|
|
||||||
|
Выберите подходящий пакет для вашей системы или AppImage.
|
||||||
|
|
||||||
|
Запуск производится по команде portprotonqt или по ярлыку в меню
|
||||||
|
|
||||||
### Разработка
|
### Разработка
|
||||||
|
|
||||||
В проект встроен линтер (ruff), статический анализатор (pyright) и проверка lock файла, если эти проверки не пройдут PR не будет принят, поэтому перед коммитом введите такую команду
|
В проект встроен линтер (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) - Дизайнер - программист
|
* [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]
|
> [!WARNING]
|
||||||
> Проект находится на стадии WIP (work in progress) корректная работоспособность не гарантирована
|
> Проект находится на стадии WIP (work in progress) корректная работоспособность не гарантирована
|
||||||
|
@ -45,7 +45,7 @@ Requires: perl-Image-ExifTool
|
|||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}-git
|
%description -n python3-%{pypi_name}-git
|
||||||
PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store.
|
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
|
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
|
||||||
@ -62,6 +62,8 @@ cp -r build-aux/share %{buildroot}/usr/
|
|||||||
|
|
||||||
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/*
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
@ -42,7 +42,7 @@ Requires: perl-Image-ExifTool
|
|||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}
|
%description -n python3-%{pypi_name}
|
||||||
PortProtonQt is a modern, user-friendly graphical interface designed to streamline the management and launching of games across multiple platforms, including PortProton, Steam, and Epic Games Store.
|
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt
|
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt
|
||||||
@ -61,6 +61,8 @@ cp -r build-aux/share %{buildroot}/usr/
|
|||||||
|
|
||||||
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/*
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop">
|
||||||
|
<name>PortProtonQt</name>
|
||||||
|
<id>ru.linux_gaming.PortProtonQt</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>GPL-3.0-or-later</project_license>
|
||||||
|
<summary>Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store</summary>
|
||||||
|
<summary xml:lang="ru">Современный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store</summary>
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.</p>
|
||||||
|
<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">
|
||||||
|
<name>Boria138</name>
|
||||||
|
</developer>
|
||||||
|
<recommends>
|
||||||
|
<control>keyboard</control>
|
||||||
|
<control>pointing</control>
|
||||||
|
<control>touch</control>
|
||||||
|
<control>gamepad</control>
|
||||||
|
</recommends>
|
||||||
|
<branding>
|
||||||
|
<color type="primary" scheme_preference="light">#007AFF</color>
|
||||||
|
<color type="primary" scheme_preference="dark">#09BEC8</color>
|
||||||
|
</branding>
|
||||||
|
<categories>
|
||||||
|
<category>Game</category>
|
||||||
|
<category>Utility</category>
|
||||||
|
</categories>
|
||||||
|
<url type="homepage">https://git.linux-gaming.ru/Boria138/PortProtonQt</url>
|
||||||
|
<url type="bugtracker">https://git.linux-gaming.ru/Boria138/PortProtonQt/issues</url>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%91%D0%B8%D0%B1%D0%BB%D0%B8%D0%BE%D1%82%D0%B5%D0%BA%D0%B0.png</image>
|
||||||
|
<caption>Library</caption>
|
||||||
|
<caption xml:lang="ru">Библиотека</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9A%D0%B0%D1%80%D1%82%D0%BE%D1%87%D0%BA%D0%B0.png</image>
|
||||||
|
<caption>Card detail page</caption>
|
||||||
|
<caption xml:lang="ru">Детали игры</caption>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/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>
|
||||||
|
</screenshots>
|
||||||
|
<keywords>
|
||||||
|
<keyword translate="no">wine</keyword>
|
||||||
|
<keyword translate="no">proton</keyword>
|
||||||
|
<keyword translate="no">steam</keyword>
|
||||||
|
<keyword translate="no">windows</keyword>
|
||||||
|
<keyword translate="no">epic games store</keyword>
|
||||||
|
<keyword translate="no">egs</keyword>
|
||||||
|
<keyword translate="no">qt</keyword>
|
||||||
|
<keyword translate="no">portproton</keyword>
|
||||||
|
<keyword>games</keyword>
|
||||||
|
</keywords>
|
||||||
|
<content_rating type="oars-1.1" />
|
||||||
|
</component>
|
@ -106,7 +106,7 @@ def compile_locales() -> None:
|
|||||||
def extract_strings() -> None:
|
def extract_strings() -> None:
|
||||||
input_dir = (Path(__file__).parent.parent / "portprotonqt").resolve()
|
input_dir = (Path(__file__).parent.parent / "portprotonqt").resolve()
|
||||||
CommandLineInterface().run([
|
CommandLineInterface().run([
|
||||||
"pybabel", "extract", "--project=PortProtonQT",
|
"pybabel", "extract", "--project=PortProtonQt",
|
||||||
f"--version={_get_version()}",
|
f"--version={_get_version()}",
|
||||||
"--strip-comment-tag",
|
"--strip-comment-tag",
|
||||||
"--no-location",
|
"--no-location",
|
||||||
@ -231,7 +231,7 @@ def main(args) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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("--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("--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")
|
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 |
|
| Locale | Progress | Translated |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 154 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 162 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 154 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 162 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 of 154 |
|
| [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 из 154 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 154 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 162 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 154 из 154 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 из 162 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -4,8 +4,9 @@ from PySide6.QtWidgets import QApplication
|
|||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
from portprotonqt.tray import SystemTray
|
from portprotonqt.tray import SystemTray
|
||||||
from portprotonqt.config_utils import read_theme_from_config
|
from portprotonqt.config_utils import read_theme_from_config, save_fullscreen_config
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
|
from portprotonqt.cli import parse_args
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -28,7 +29,17 @@ def main():
|
|||||||
else:
|
else:
|
||||||
logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}")
|
logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}")
|
||||||
|
|
||||||
|
# Парсинг аргументов командной строки
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
|
|
||||||
|
# Обработка флага --fullscreen
|
||||||
|
if args.fullscreen:
|
||||||
|
logger.info("Запуск в полноэкранном режиме по флагу --fullscreen")
|
||||||
|
save_fullscreen_config(True)
|
||||||
|
window.showFullScreen()
|
||||||
|
|
||||||
current_theme_name = read_theme_from_config()
|
current_theme_name = read_theme_from_config()
|
||||||
tray = SystemTray(app, current_theme_name)
|
tray = SystemTray(app, current_theme_name)
|
||||||
tray.show_action.triggered.connect(window.show)
|
tray.show_action.triggered.connect(window.show)
|
||||||
@ -43,7 +54,9 @@ def main():
|
|||||||
tray.hide_action.triggered.connect(window.hide)
|
tray.hide_action.triggered.connect(window.hide)
|
||||||
|
|
||||||
window.settings_saved.connect(recreate_tray)
|
window.settings_saved.connect(recreate_tray)
|
||||||
|
|
||||||
window.show()
|
window.show()
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
16
portprotonqt/cli.py
Normal file
16
portprotonqt/cli.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import argparse
|
||||||
|
from portprotonqt.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""
|
||||||
|
Парсит аргументы командной строки.
|
||||||
|
"""
|
||||||
|
parser = argparse.ArgumentParser(description="PortProtonQt CLI")
|
||||||
|
parser.add_argument(
|
||||||
|
"--fullscreen",
|
||||||
|
action="store_true",
|
||||||
|
help="Запустить приложение в полноэкранном режиме и сохранить эту настройку"
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
@ -10,7 +10,7 @@ _portproton_location = None
|
|||||||
# Пути к конфигурационным файлам
|
# Пути к конфигурационным файлам
|
||||||
CONFIG_FILE = os.path.join(
|
CONFIG_FILE = os.path.join(
|
||||||
os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")),
|
os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")),
|
||||||
"PortProtonQT.conf"
|
"PortProtonQt.conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
PORTPROTON_CONFIG_FILE = os.path.join(
|
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"))
|
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
THEMES_DIRS = [
|
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")
|
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:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(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():
|
def ensure_default_proxy_config():
|
||||||
"""
|
"""
|
||||||
Проверяет наличие секции [Proxy] в конфигурационном файле.
|
Проверяет наличие секции [Proxy] в конфигурационном файле.
|
||||||
@ -342,7 +377,6 @@ def ensure_default_proxy_config():
|
|||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
|
||||||
|
|
||||||
def read_proxy_config():
|
def read_proxy_config():
|
||||||
"""
|
"""
|
||||||
Читает настройки прокси из секции [Proxy] конфигурационного файла.
|
Читает настройки прокси из секции [Proxy] конфигурационного файла.
|
||||||
@ -421,8 +455,6 @@ def save_fullscreen_config(fullscreen):
|
|||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def read_window_geometry() -> tuple[int, int]:
|
def read_window_geometry() -> tuple[int, int]:
|
||||||
"""
|
"""
|
||||||
Читает ширину и высоту окна из секции [MainWindow] конфигурационного файла.
|
Читает ширину и высоту окна из секции [MainWindow] конфигурационного файла.
|
||||||
@ -472,14 +504,14 @@ def reset_config():
|
|||||||
|
|
||||||
def clear_cache():
|
def clear_cache():
|
||||||
"""
|
"""
|
||||||
Очищает кэш PortProtonQT, удаляя папку кэша.
|
Очищает кэш PortProtonQt, удаляя папку кэша.
|
||||||
"""
|
"""
|
||||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
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):
|
if os.path.exists(cache_dir):
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(cache_dir)
|
shutil.rmtree(cache_dir)
|
||||||
logger.info("Кэш PortProtonQT удалён: %s", cache_dir)
|
logger.info("Кэш PortProtonQt удалён: %s", cache_dir)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Ошибка при удалении кэша: %s", e)
|
logger.error("Ошибка при удалении кэша: %s", e)
|
||||||
|
|
||||||
|
@ -6,12 +6,13 @@ import subprocess
|
|||||||
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu
|
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu
|
||||||
from PySide6.QtCore import QUrl, QPoint
|
from PySide6.QtCore import QUrl, QPoint
|
||||||
from PySide6.QtGui import QDesktopServices
|
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.localization import _
|
||||||
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
||||||
|
from portprotonqt.dialogs import AddGameDialog
|
||||||
|
|
||||||
class ContextMenuManager:
|
class ContextMenuManager:
|
||||||
"""Manages context menu actions for game management in PortProtonQT."""
|
"""Manages context menu actions for game management in PortProtonQt."""
|
||||||
|
|
||||||
def __init__(self, parent, portproton_location, theme, load_games_callback, update_game_grid_callback):
|
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 = 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"):
|
if game_card.game_source not in ("steam", "epic"):
|
||||||
desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip()
|
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")
|
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
||||||
@ -79,6 +92,26 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
menu.exec(game_card.mapToGlobal(pos))
|
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):
|
def _check_portproton(self):
|
||||||
"""Check if PortProton is available."""
|
"""Check if PortProton is available."""
|
||||||
if self.portproton_location is None:
|
if self.portproton_location is None:
|
||||||
@ -225,7 +258,7 @@ class ContextMenuManager:
|
|||||||
"XDG_DATA_HOME",
|
"XDG_DATA_HOME",
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share")
|
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):
|
if os.path.exists(custom_folder):
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(custom_folder)
|
shutil.rmtree(custom_folder)
|
||||||
@ -321,7 +354,6 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
def edit_game_shortcut(self, game_name, exec_line, cover_path):
|
def edit_game_shortcut(self, game_name, exec_line, cover_path):
|
||||||
"""Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
|
"""Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
|
||||||
from portprotonqt.dialogs import AddGameDialog # Local import to avoid circular dependency
|
|
||||||
|
|
||||||
if not self._check_portproton():
|
if not self._check_portproton():
|
||||||
return
|
return
|
||||||
@ -385,7 +417,7 @@ class ContextMenuManager:
|
|||||||
"XDG_DATA_HOME",
|
"XDG_DATA_HOME",
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share")
|
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)
|
os.makedirs(custom_folder, exist_ok=True)
|
||||||
|
|
||||||
ext = os.path.splitext(new_cover_path)[1].lower()
|
ext = os.path.splitext(new_cover_path)[1].lower()
|
||||||
|
@ -303,7 +303,7 @@ class Downloader(QObject):
|
|||||||
|
|
||||||
local_path = os.path.join(
|
local_path = os.path.join(
|
||||||
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
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}")
|
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",
|
"XDG_CACHE_HOME",
|
||||||
os.path.join(os.path.expanduser("~"), ".cache")
|
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)
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
return cache_dir
|
return cache_dir
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ def get_egs_game_description_async(
|
|||||||
Asynchronously fetches the game description from the Epic Games Store API.
|
Asynchronously fetches the game description from the Epic Games Store API.
|
||||||
Prioritizes GraphQL API with namespace for slug and description.
|
Prioritizes GraphQL API with namespace for slug and description.
|
||||||
Falls back to legacy API if GraphQL provides a slug but no 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.
|
Handles DNS resolution failures gracefully.
|
||||||
"""
|
"""
|
||||||
cache_dir = get_cache_dir()
|
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:
|
except Exception as e:
|
||||||
logger.warning("Error processing metadata for %s: %s", app_name, str(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 ""
|
local_path = os.path.join(image_folder, f"{app_name}.jpg") if cover_url else ""
|
||||||
|
|
||||||
def on_description_fetched(api_description: str):
|
def on_description_fetched(api_description: str):
|
||||||
|
@ -199,7 +199,7 @@ class GameCard(QFrame):
|
|||||||
icon_size=16,
|
icon_size=16,
|
||||||
icon_space=3,
|
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))
|
self.anticheatLabel.setFixedWidth(int(card_width * 2/3))
|
||||||
anticheat_visible = True
|
anticheat_visible = True
|
||||||
else:
|
else:
|
||||||
@ -261,46 +261,45 @@ class GameCard(QFrame):
|
|||||||
self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
|
self.steam_visible = (str(self.game_source).lower() == "steam" and display_filter in ("all", "favorites"))
|
||||||
self.egs_visible = (str(self.game_source).lower() == "epic" and display_filter in ("all", "favorites"))
|
self.egs_visible = (str(self.game_source).lower() == "epic" and display_filter in ("all", "favorites"))
|
||||||
self.portproton_visible = (str(self.game_source).lower() == "portproton" and display_filter in ("all", "favorites"))
|
self.portproton_visible = (str(self.game_source).lower() == "portproton" and display_filter in ("all", "favorites"))
|
||||||
|
protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
|
||||||
|
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
|
||||||
|
|
||||||
|
# Обновляем видимость бейджей
|
||||||
self.steamLabel.setVisible(self.steam_visible)
|
self.steamLabel.setVisible(self.steam_visible)
|
||||||
self.egsLabel.setVisible(self.egs_visible)
|
self.egsLabel.setVisible(self.egs_visible)
|
||||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||||
|
self.protondbLabel.setVisible(protondb_visible)
|
||||||
|
self.anticheatLabel.setVisible(anticheat_visible)
|
||||||
|
|
||||||
# Reposition badges
|
# Подготавливаем список всех бейджей с их текущей видимостью
|
||||||
|
badges = [
|
||||||
|
(self.steam_visible, self.steamLabel),
|
||||||
|
(self.egs_visible, self.egsLabel),
|
||||||
|
(self.portproton_visible, self.portprotonLabel),
|
||||||
|
(protondb_visible, self.protondbLabel),
|
||||||
|
(anticheat_visible, self.anticheatLabel),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Пересчитываем позиции бейджей
|
||||||
right_margin = 8
|
right_margin = 8
|
||||||
badge_spacing = 5
|
badge_spacing = 5
|
||||||
top_y = 10
|
top_y = 10
|
||||||
badge_y_positions = []
|
badge_y_positions = []
|
||||||
badge_width = int(self.coverLabel.width() * 2/3)
|
badge_width = int(self.coverLabel.width() * 2/3)
|
||||||
if self.steam_visible:
|
|
||||||
steam_x = self.coverLabel.width() - badge_width - right_margin
|
|
||||||
self.steamLabel.move(steam_x, top_y)
|
|
||||||
badge_y_positions.append(top_y + self.steamLabel.height())
|
|
||||||
if self.egs_visible:
|
|
||||||
egs_x = self.coverLabel.width() - badge_width - right_margin
|
|
||||||
egs_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
|
||||||
self.egsLabel.move(egs_x, egs_y)
|
|
||||||
badge_y_positions.append(egs_y + self.egsLabel.height())
|
|
||||||
if self.portproton_visible:
|
|
||||||
portproton_x = self.coverLabel.width() - badge_width - right_margin
|
|
||||||
portproton_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
|
||||||
self.portprotonLabel.move(portproton_x, portproton_y)
|
|
||||||
badge_y_positions.append(portproton_y + self.portprotonLabel.height())
|
|
||||||
if self.protondbLabel.isVisible():
|
|
||||||
protondb_x = self.coverLabel.width() - badge_width - right_margin
|
|
||||||
protondb_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
|
||||||
self.protondbLabel.move(protondb_x, protondb_y)
|
|
||||||
badge_y_positions.append(protondb_y + self.protondbLabel.height())
|
|
||||||
if self.anticheatLabel.isVisible():
|
|
||||||
anticheat_x = self.coverLabel.width() - badge_width - right_margin
|
|
||||||
anticheat_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
|
||||||
self.anticheatLabel.move(anticheat_x, anticheat_y)
|
|
||||||
|
|
||||||
self.anticheatLabel.raise_()
|
for is_visible, badge in badges:
|
||||||
self.protondbLabel.raise_()
|
if is_visible:
|
||||||
self.portprotonLabel.raise_()
|
badge_x = self.coverLabel.width() - badge_width - right_margin
|
||||||
self.egsLabel.raise_()
|
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||||
self.steamLabel.raise_()
|
badge.move(badge_x, badge_y)
|
||||||
|
badge_y_positions.append(badge_y + badge.height())
|
||||||
|
|
||||||
|
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
|
||||||
|
self.anticheatLabel.raise_()
|
||||||
|
self.protondbLabel.raise_()
|
||||||
|
self.portprotonLabel.raise_()
|
||||||
|
self.egsLabel.raise_()
|
||||||
|
self.steamLabel.raise_()
|
||||||
|
|
||||||
def _show_context_menu(self, pos):
|
def _show_context_menu(self, pos):
|
||||||
"""Delegate context menu display to ContextMenuManager."""
|
"""Delegate context menu display to ContextMenuManager."""
|
||||||
|
@ -35,10 +35,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
y = (scaled.height() - height) // 2
|
y = (scaled.height() - height) // 2
|
||||||
cropped = scaled.copy(x, y, width, height)
|
cropped = scaled.copy(x, y, width, height)
|
||||||
callback(cropped)
|
callback(cropped)
|
||||||
# Removed: pixmap = None (unnecessary, causes type error)
|
|
||||||
|
|
||||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
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)
|
os.makedirs(image_folder, exist_ok=True)
|
||||||
|
|
||||||
if cover and cover.startswith("https://steamcdn-a.akamaihd.net/steam/apps/"):
|
if cover and cover.startswith("https://steamcdn-a.akamaihd.net/steam/apps/"):
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
from typing import Protocol, cast
|
from typing import Protocol, cast
|
||||||
from evdev import InputDevice, ecodes, list_devices
|
from evdev import InputDevice, InputEvent, ecodes, list_devices, ff
|
||||||
import pyudev
|
from pyudev import Context, Monitor, MonitorObserver, Device
|
||||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog
|
from PySide6.QtWidgets import QWidget, QStackedWidget, QApplication, QScrollArea, QLineEdit, QDialog, QMenu, QComboBox, QListView, QMessageBox
|
||||||
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot
|
from PySide6.QtCore import Qt, QObject, QEvent, QPoint, Signal, Slot, QTimer
|
||||||
from PySide6.QtGui import QKeyEvent
|
from PySide6.QtGui import QKeyEvent
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.image_utils import FullscreenDialog
|
from portprotonqt.image_utils import FullscreenDialog
|
||||||
from portprotonqt.custom_widgets import NavLabel
|
from portprotonqt.custom_widgets import NavLabel
|
||||||
from portprotonqt.game_card import GameCard
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -25,6 +25,8 @@ class MainWindowProtocol(Protocol):
|
|||||||
...
|
...
|
||||||
def toggleGame(self, exec_line: str | None, button: QWidget | None = None) -> None:
|
def toggleGame(self, exec_line: str | None, button: QWidget | None = None) -> None:
|
||||||
...
|
...
|
||||||
|
def openSystemOverlay(self) -> None:
|
||||||
|
...
|
||||||
stackedWidget: QStackedWidget
|
stackedWidget: QStackedWidget
|
||||||
tabButtons: dict[int, QWidget]
|
tabButtons: dict[int, QWidget]
|
||||||
gamesListWidget: QWidget
|
gamesListWidget: QWidget
|
||||||
@ -32,24 +34,25 @@ class MainWindowProtocol(Protocol):
|
|||||||
current_exec_line: str | None
|
current_exec_line: str | None
|
||||||
current_add_game_dialog: QDialog | 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 = {
|
BUTTONS = {
|
||||||
'confirm': {ecodes.BTN_A},
|
'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, # A / Cross
|
||||||
'back': {ecodes.BTN_B},
|
'back': {ecodes.BTN_B, ecodes.BTN_EAST}, # B / Circle
|
||||||
'add_game': {ecodes.BTN_Y},
|
'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, # Y / Triangle
|
||||||
'prev_tab': {ecodes.BTN_TL, ecodes.BTN_TRIGGER_HAPPY7},
|
'prev_tab': {ecodes.BTN_TL}, # LB / L1
|
||||||
'next_tab': {ecodes.BTN_TR, ecodes.BTN_TRIGGER_HAPPY5},
|
'next_tab': {ecodes.BTN_TR}, # RB / R1
|
||||||
'confirm_stick': {ecodes.BTN_THUMBL, ecodes.BTN_THUMBR},
|
'context_menu': {ecodes.BTN_START}, # Start / Options
|
||||||
'context_menu': {ecodes.BTN_START},
|
'menu': {ecodes.BTN_SELECT}, # Select / Share
|
||||||
'menu': {ecodes.BTN_SELECT},
|
'guide': {ecodes.BTN_MODE}, # Xbox / PS Home
|
||||||
}
|
}
|
||||||
|
|
||||||
class InputManager(QObject):
|
class InputManager(QObject):
|
||||||
"""
|
"""
|
||||||
Manages input from gamepads and keyboards for navigating the application interface.
|
Manages input from gamepads and keyboards for navigating the application interface.
|
||||||
Supports gamepad hotplugging, button and axis events, and keyboard event filtering
|
Supports gamepad hotplugging, button and axis events, and keyboard event filtering
|
||||||
for seamless UI interaction. Enables fullscreen mode when a gamepad is connected
|
for seamless UI interaction.
|
||||||
and restores normal mode when disconnected.
|
|
||||||
"""
|
"""
|
||||||
# Signals for gamepad events
|
# Signals for gamepad events
|
||||||
button_pressed = Signal(int) # Signal for button presses
|
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.currentDetailPage = getattr(self._parent, 'currentDetailPage', None)
|
||||||
self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None)
|
self._parent.current_exec_line = getattr(self._parent, 'current_exec_line', None)
|
||||||
self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
|
self._parent.current_add_game_dialog = getattr(self._parent, 'current_add_game_dialog', None)
|
||||||
|
|
||||||
self.axis_deadzone = axis_deadzone
|
self.axis_deadzone = axis_deadzone
|
||||||
self.initial_axis_move_delay = initial_axis_move_delay
|
self.initial_axis_move_delay = initial_axis_move_delay
|
||||||
self.repeat_axis_move_delay = repeat_axis_move_delay
|
self.repeat_axis_move_delay = repeat_axis_move_delay
|
||||||
@ -80,6 +82,13 @@ class InputManager(QObject):
|
|||||||
self.gamepad_thread: threading.Thread | None = None
|
self.gamepad_thread: threading.Thread | None = None
|
||||||
self.running = True
|
self.running = True
|
||||||
self._is_fullscreen = read_fullscreen_config()
|
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
|
# Connect signals to slots
|
||||||
self.button_pressed.connect(self.handle_button_slot)
|
self.button_pressed.connect(self.handle_button_slot)
|
||||||
@ -117,6 +126,48 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in handle_fullscreen_slot: {e}", exc_info=True)
|
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)
|
@Slot(int)
|
||||||
def handle_button_slot(self, button_code: int) -> None:
|
def handle_button_slot(self, button_code: int) -> None:
|
||||||
try:
|
try:
|
||||||
@ -129,10 +180,66 @@ class InputManager(QObject):
|
|||||||
return
|
return
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
|
popup = QApplication.activePopupWidget()
|
||||||
|
|
||||||
|
# Handle Guide button to open system overlay
|
||||||
|
if button_code in BUTTONS['guide']:
|
||||||
|
if not popup and not isinstance(active, QDialog):
|
||||||
|
self._parent.openSystemOverlay()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle QMenu (context menu)
|
||||||
|
if isinstance(popup, QMenu):
|
||||||
|
if button_code in BUTTONS['confirm']:
|
||||||
|
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
|
# Закрытие AddGameDialog на кнопку B
|
||||||
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
|
if button_code in BUTTONS['back'] and isinstance(active, QDialog):
|
||||||
active.reject() # Закрываем диалог
|
active.reject()
|
||||||
return
|
return
|
||||||
|
|
||||||
# FullscreenDialog
|
# FullscreenDialog
|
||||||
@ -149,22 +256,26 @@ class InputManager(QObject):
|
|||||||
if isinstance(focused, GameCard):
|
if isinstance(focused, GameCard):
|
||||||
if button_code in BUTTONS['context_menu']:
|
if button_code in BUTTONS['context_menu']:
|
||||||
pos = QPoint(focused.width() // 2, focused.height() // 2)
|
pos = QPoint(focused.width() // 2, focused.height() // 2)
|
||||||
focused._show_context_menu(pos)
|
menu = focused._show_context_menu(pos)
|
||||||
|
if menu:
|
||||||
|
menu.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Game launch on detail page
|
# Game launch on detail page
|
||||||
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:
|
if self._parent.current_exec_line:
|
||||||
|
self.trigger_rumble()
|
||||||
self._parent.toggleGame(self._parent.current_exec_line, None)
|
self._parent.toggleGame(self._parent.current_exec_line, None)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Standard navigation
|
# Standard navigation
|
||||||
if button_code in BUTTONS['confirm'] or button_code in BUTTONS['confirm_stick']:
|
if button_code in BUTTONS['confirm']:
|
||||||
self._parent.activateFocusedWidget()
|
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))
|
self._parent.goBackDetailPage(getattr(self._parent, 'currentDetailPage', None))
|
||||||
elif button_code in BUTTONS['add_game']:
|
elif button_code in BUTTONS['add_game']:
|
||||||
self._parent.openAddGameDialog()
|
if self._parent.stackedWidget.currentIndex() == 0:
|
||||||
|
self._parent.openAddGameDialog()
|
||||||
elif button_code in BUTTONS['prev_tab']:
|
elif button_code in BUTTONS['prev_tab']:
|
||||||
idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
|
idx = (self._parent.stackedWidget.currentIndex() - 1) % len(self._parent.tabButtons)
|
||||||
self._parent.switchTab(idx)
|
self._parent.switchTab(idx)
|
||||||
@ -176,6 +287,14 @@ class InputManager(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
logger.error(f"Error in handle_button_slot: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def handle_dpad_repeat(self) -> None:
|
||||||
|
"""Handle repeated D-pad input while the D-pad is held."""
|
||||||
|
if self.current_dpad_code is not None and self.current_dpad_value != 0:
|
||||||
|
now = time.time()
|
||||||
|
if (now - self.last_move_time) >= self.current_axis_delay:
|
||||||
|
self.handle_dpad_slot(self.current_dpad_code, self.current_dpad_value, now)
|
||||||
|
self.last_move_time = now
|
||||||
|
self.current_axis_delay = self.repeat_axis_move_delay
|
||||||
|
|
||||||
@Slot(int, int, float)
|
@Slot(int, int, float)
|
||||||
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
def handle_dpad_slot(self, code: int, value: int, current_time: float) -> None:
|
||||||
@ -188,6 +307,85 @@ class InputManager(QObject):
|
|||||||
if not app:
|
if not app:
|
||||||
return
|
return
|
||||||
active = QApplication.activeWindow()
|
active = QApplication.activeWindow()
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
popup = QApplication.activePopupWidget()
|
||||||
|
|
||||||
|
# Update D-pad state
|
||||||
|
if value != 0:
|
||||||
|
self.current_dpad_code = code
|
||||||
|
self.current_dpad_value = value
|
||||||
|
if not self.axis_moving:
|
||||||
|
self.axis_moving = True
|
||||||
|
self.last_move_time = current_time
|
||||||
|
self.current_axis_delay = self.initial_axis_move_delay
|
||||||
|
self.dpad_timer.start(int(self.repeat_axis_move_delay * 1000)) # Start timer (in milliseconds)
|
||||||
|
else:
|
||||||
|
self.current_dpad_code = None
|
||||||
|
self.current_dpad_value = 0
|
||||||
|
self.axis_moving = False
|
||||||
|
self.current_axis_delay = self.initial_axis_move_delay
|
||||||
|
self.dpad_timer.stop() # Stop timer when D-pad is released
|
||||||
|
return
|
||||||
|
|
||||||
|
# Handle SystemOverlay, 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
|
# Fullscreen horizontal navigation
|
||||||
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
|
if isinstance(active, FullscreenDialog) and code == ecodes.ABS_HAT0X:
|
||||||
@ -197,19 +395,6 @@ class InputManager(QObject):
|
|||||||
active.show_next()
|
active.show_next()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle repeated D-pad movement
|
|
||||||
if value != 0:
|
|
||||||
if not self.axis_moving:
|
|
||||||
self.axis_moving = True
|
|
||||||
elif (current_time - self.last_move_time) < self.current_axis_delay:
|
|
||||||
return
|
|
||||||
self.last_move_time = current_time
|
|
||||||
self.current_axis_delay = self.repeat_axis_move_delay
|
|
||||||
else:
|
|
||||||
self.axis_moving = False
|
|
||||||
self.current_axis_delay = self.initial_axis_move_delay
|
|
||||||
return
|
|
||||||
|
|
||||||
# Library tab navigation (index 0)
|
# Library tab navigation (index 0)
|
||||||
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
if self._parent.stackedWidget.currentIndex() == 0 and code in (ecodes.ABS_HAT0X, ecodes.ABS_HAT0Y):
|
||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
@ -280,7 +465,6 @@ class InputManager(QObject):
|
|||||||
next_card.setFocus()
|
next_card.setFocus()
|
||||||
if scroll_area:
|
if scroll_area:
|
||||||
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
scroll_area.ensureWidgetVisible(next_card, 50, 50)
|
||||||
|
|
||||||
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
|
elif code == ecodes.ABS_HAT0Y and value != 0: # Up/Down
|
||||||
if value > 0: # Down
|
if value > 0: # Down
|
||||||
next_row_idx = current_row_idx + 1
|
next_row_idx = current_row_idx + 1
|
||||||
@ -350,6 +534,12 @@ class InputManager(QObject):
|
|||||||
focused = QApplication.focusWidget()
|
focused = QApplication.focusWidget()
|
||||||
popup = QApplication.activePopupWidget()
|
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
|
# Close application with Ctrl+Q
|
||||||
if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
|
if key == Qt.Key.Key_Q and modifiers & Qt.KeyboardModifier.ControlModifier:
|
||||||
app.quit()
|
app.quit()
|
||||||
@ -390,6 +580,23 @@ class InputManager(QObject):
|
|||||||
focused._show_context_menu(pos)
|
focused._show_context_menu(pos)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Handle Up/Down keys for non-GameCard tabs
|
||||||
|
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down) and not isinstance(focused, GameCard):
|
||||||
|
page = self._parent.stackedWidget.currentWidget()
|
||||||
|
if key == Qt.Key.Key_Down:
|
||||||
|
if isinstance(focused, NavLabel):
|
||||||
|
focusables = page.findChildren(QWidget, options=Qt.FindChildOption.FindChildrenRecursively)
|
||||||
|
focusables = [w for w in focusables if w.focusPolicy() & Qt.FocusPolicy.StrongFocus]
|
||||||
|
if focusables:
|
||||||
|
focusables[0].setFocus()
|
||||||
|
return True
|
||||||
|
elif focused:
|
||||||
|
focused.focusNextChild()
|
||||||
|
return True
|
||||||
|
elif key == Qt.Key.Key_Up and focused:
|
||||||
|
focused.focusPreviousChild()
|
||||||
|
return True
|
||||||
|
|
||||||
# Tab switching with Left/Right keys (non-GameCard focus or no focus)
|
# Tab switching with Left/Right keys (non-GameCard focus or no focus)
|
||||||
idx = self._parent.stackedWidget.currentIndex()
|
idx = self._parent.stackedWidget.currentIndex()
|
||||||
total = len(self._parent.tabButtons)
|
total = len(self._parent.tabButtons)
|
||||||
@ -520,6 +727,9 @@ class InputManager(QObject):
|
|||||||
if focusables:
|
if focusables:
|
||||||
focusables[0].setFocus()
|
focusables[0].setFocus()
|
||||||
return True
|
return True
|
||||||
|
elif focused:
|
||||||
|
focused.focusNextChild()
|
||||||
|
return True
|
||||||
# Navigate up through tab content
|
# Navigate up through tab content
|
||||||
if key == Qt.Key.Key_Up:
|
if key == Qt.Key.Key_Up:
|
||||||
if isinstance(focused, NavLabel):
|
if isinstance(focused, NavLabel):
|
||||||
@ -540,8 +750,10 @@ class InputManager(QObject):
|
|||||||
elif key == Qt.Key.Key_E:
|
elif key == Qt.Key.Key_E:
|
||||||
if isinstance(focused, QLineEdit):
|
if isinstance(focused, QLineEdit):
|
||||||
return False
|
return False
|
||||||
self._parent.openAddGameDialog()
|
# Only open AddGameDialog if in library tab (index 0)
|
||||||
return True
|
if self._parent.stackedWidget.currentIndex() == 0:
|
||||||
|
self._parent.openAddGameDialog()
|
||||||
|
return True
|
||||||
|
|
||||||
# Toggle fullscreen with F11
|
# Toggle fullscreen with F11
|
||||||
if key == Qt.Key.Key_F11:
|
if key == Qt.Key.Key_F11:
|
||||||
@ -559,17 +771,17 @@ class InputManager(QObject):
|
|||||||
|
|
||||||
def run_udev_monitor(self) -> None:
|
def run_udev_monitor(self) -> None:
|
||||||
try:
|
try:
|
||||||
context = pyudev.Context()
|
context = Context()
|
||||||
monitor = pyudev.Monitor.from_netlink(context)
|
monitor = Monitor.from_netlink(context)
|
||||||
monitor.filter_by(subsystem='input')
|
monitor.filter_by(subsystem='input')
|
||||||
observer = pyudev.MonitorObserver(monitor, self.handle_udev_event)
|
observer = MonitorObserver(monitor, self.handle_udev_event)
|
||||||
observer.start()
|
observer.start()
|
||||||
while self.running:
|
while self.running:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in udev monitor: {e}", exc_info=True)
|
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:
|
try:
|
||||||
if action == 'add':
|
if action == 'add':
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
@ -577,6 +789,7 @@ class InputManager(QObject):
|
|||||||
elif action == 'remove' and self.gamepad:
|
elif action == 'remove' and self.gamepad:
|
||||||
if not any(self.gamepad.path == path for path in list_devices()):
|
if not any(self.gamepad.path == path for path in list_devices()):
|
||||||
logger.info("Gamepad disconnected")
|
logger.info("Gamepad disconnected")
|
||||||
|
self.stop_rumble()
|
||||||
self.gamepad = None
|
self.gamepad = None
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join()
|
||||||
@ -590,6 +803,7 @@ class InputManager(QObject):
|
|||||||
new_gamepad = self.find_gamepad()
|
new_gamepad = self.find_gamepad()
|
||||||
if new_gamepad and new_gamepad != self.gamepad:
|
if new_gamepad and new_gamepad != self.gamepad:
|
||||||
logger.info(f"Gamepad connected: {new_gamepad.name}")
|
logger.info(f"Gamepad connected: {new_gamepad.name}")
|
||||||
|
self.stop_rumble()
|
||||||
self.gamepad = new_gamepad
|
self.gamepad = new_gamepad
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join()
|
||||||
@ -626,9 +840,7 @@ class InputManager(QObject):
|
|||||||
continue
|
continue
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if event.type == ecodes.EV_KEY and event.value == 1:
|
if event.type == ecodes.EV_KEY and event.value == 1:
|
||||||
# Обработка кнопки Select для переключения полноэкранного режима
|
|
||||||
if event.code in BUTTONS['menu']:
|
if event.code in BUTTONS['menu']:
|
||||||
# Переключаем полноэкранный режим
|
|
||||||
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
self.toggle_fullscreen.emit(not self._is_fullscreen)
|
||||||
else:
|
else:
|
||||||
self.button_pressed.emit(event.code)
|
self.button_pressed.emit(event.code)
|
||||||
@ -644,6 +856,7 @@ class InputManager(QObject):
|
|||||||
finally:
|
finally:
|
||||||
if self.gamepad:
|
if self.gamepad:
|
||||||
try:
|
try:
|
||||||
|
self.stop_rumble()
|
||||||
self.gamepad.close()
|
self.gamepad.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@ -652,6 +865,8 @@ class InputManager(QObject):
|
|||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.running = False
|
self.running = False
|
||||||
|
self.dpad_timer.stop()
|
||||||
|
self.stop_rumble()
|
||||||
if self.gamepad_thread:
|
if self.gamepad_thread:
|
||||||
self.gamepad_thread.join()
|
self.gamepad_thread.join()
|
||||||
if self.gamepad:
|
if self.gamepad:
|
||||||
|
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
# German (Germany) translations for PortProtonQT.
|
# German (Germany) translations for PortProtonQt.
|
||||||
# Copyright (C) 2025 boria138
|
# 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.
|
# project.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
#
|
#
|
||||||
@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
|
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
@ -20,6 +20,12 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
|
msgid "Remove from Favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add to Favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove from Desktop"
|
msgid "Remove from Desktop"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -47,6 +53,14 @@ msgstr ""
|
|||||||
msgid "Add to Steam"
|
msgid "Add to Steam"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Added '{0}' to favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Removed '{0}' from favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Error"
|
msgid "Error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -362,19 +376,10 @@ msgstr ""
|
|||||||
msgid "Auto Fullscreen on Gamepad connected:"
|
msgid "Auto Fullscreen on Gamepad connected:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Open Legendary Login"
|
msgid "Gamepad haptic feedback"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legendary Authentication:"
|
msgid "Gamepad haptic feedback:"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enter Legendary Authorization Code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Authorization Code:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Submit Code"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
@ -392,22 +397,6 @@ msgstr ""
|
|||||||
msgid "Failed to open Legendary login page"
|
msgid "Failed to open Legendary login page"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Please enter an authorization code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Successfully authenticated with Legendary"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Legendary authentication failed: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Legendary executable not found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Unexpected error during authentication"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Reset"
|
msgid "Confirm Reset"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -505,6 +494,42 @@ msgstr ""
|
|||||||
msgid "Launching"
|
msgid "Launching"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "System Overlay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reboot"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Shutdown"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Suspend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Exit Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "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"
|
msgid "just now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
# Spanish (Spain) translations for PortProtonQT.
|
# Spanish (Spain) translations for PortProtonQt.
|
||||||
# Copyright (C) 2025 boria138
|
# 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.
|
# project.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
#
|
#
|
||||||
@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
|
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@ -20,6 +20,12 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
|
msgid "Remove from Favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add to Favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove from Desktop"
|
msgid "Remove from Desktop"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -47,6 +53,14 @@ msgstr ""
|
|||||||
msgid "Add to Steam"
|
msgid "Add to Steam"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Added '{0}' to favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Removed '{0}' from favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Error"
|
msgid "Error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -362,19 +376,10 @@ msgstr ""
|
|||||||
msgid "Auto Fullscreen on Gamepad connected:"
|
msgid "Auto Fullscreen on Gamepad connected:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Open Legendary Login"
|
msgid "Gamepad haptic feedback"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legendary Authentication:"
|
msgid "Gamepad haptic feedback:"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enter Legendary Authorization Code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Authorization Code:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Submit Code"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
@ -392,22 +397,6 @@ msgstr ""
|
|||||||
msgid "Failed to open Legendary login page"
|
msgid "Failed to open Legendary login page"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Please enter an authorization code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Successfully authenticated with Legendary"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Legendary authentication failed: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Legendary executable not found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Unexpected error during authentication"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Reset"
|
msgid "Confirm Reset"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -505,6 +494,42 @@ msgstr ""
|
|||||||
msgid "Launching"
|
msgid "Launching"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "System Overlay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reboot"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Shutdown"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Suspend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Exit Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "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"
|
msgid "just now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
# Translations template for PortProtonQT.
|
# Translations template for PortProtonQt.
|
||||||
# Copyright (C) 2025 boria138
|
# 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.
|
# project.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
#
|
#
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PortProtonQT 0.1.1\n"
|
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
|
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -18,6 +18,12 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
|
msgid "Remove from Favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add to Favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove from Desktop"
|
msgid "Remove from Desktop"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -45,6 +51,14 @@ msgstr ""
|
|||||||
msgid "Add to Steam"
|
msgid "Add to Steam"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Added '{0}' to favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Removed '{0}' from favorites"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Error"
|
msgid "Error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -360,19 +374,10 @@ msgstr ""
|
|||||||
msgid "Auto Fullscreen on Gamepad connected:"
|
msgid "Auto Fullscreen on Gamepad connected:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Open Legendary Login"
|
msgid "Gamepad haptic feedback"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legendary Authentication:"
|
msgid "Gamepad haptic feedback:"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enter Legendary Authorization Code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Authorization Code:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Submit Code"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
@ -390,22 +395,6 @@ msgstr ""
|
|||||||
msgid "Failed to open Legendary login page"
|
msgid "Failed to open Legendary login page"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Please enter an authorization code"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Successfully authenticated with Legendary"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Legendary authentication failed: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Legendary executable not found"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Unexpected error during authentication"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Reset"
|
msgid "Confirm Reset"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -503,6 +492,42 @@ msgstr ""
|
|||||||
msgid "Launching"
|
msgid "Launching"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "System Overlay"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Reboot"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Shutdown"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Suspend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Exit Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "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"
|
msgid "just now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
# Russian (Russia) translations for PortProtonQT.
|
# Russian (Russia) translations for PortProtonQt.
|
||||||
# Copyright (C) 2025 boria138
|
# 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.
|
# project.
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
|
||||||
#
|
#
|
||||||
@ -9,8 +9,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-06-06 20:01+0500\n"
|
"POT-Creation-Date: 2025-06-11 23:15+0500\n"
|
||||||
"PO-Revision-Date: 2025-06-06 20:01+0500\n"
|
"PO-Revision-Date: 2025-06-11 23:15+0500\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language: ru_RU\n"
|
"Language: ru_RU\n"
|
||||||
"Language-Team: ru_RU <LL@li.org>\n"
|
"Language-Team: ru_RU <LL@li.org>\n"
|
||||||
@ -21,6 +21,12 @@ msgstr ""
|
|||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
|
||||||
|
msgid "Remove from Favorites"
|
||||||
|
msgstr "Удалить из Избранного"
|
||||||
|
|
||||||
|
msgid "Add to Favorites"
|
||||||
|
msgstr "Добавить в Избранное"
|
||||||
|
|
||||||
msgid "Remove from Desktop"
|
msgid "Remove from Desktop"
|
||||||
msgstr "Удалить с рабочего стола"
|
msgstr "Удалить с рабочего стола"
|
||||||
|
|
||||||
@ -48,6 +54,14 @@ msgstr "Удалить из Steam"
|
|||||||
msgid "Add to Steam"
|
msgid "Add to Steam"
|
||||||
msgstr "Добавить в 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"
|
msgid "Error"
|
||||||
msgstr "Ошибка"
|
msgstr "Ошибка"
|
||||||
|
|
||||||
@ -369,20 +383,11 @@ msgstr "Режим полноэкранного отображения прил
|
|||||||
msgid "Auto Fullscreen on Gamepad connected:"
|
msgid "Auto Fullscreen on Gamepad connected:"
|
||||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
|
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
|
||||||
|
|
||||||
msgid "Open Legendary Login"
|
msgid "Gamepad haptic feedback"
|
||||||
msgstr "Открыть браузер для входа в Legendary"
|
msgstr "Тактильная обратная связь на геймпаде"
|
||||||
|
|
||||||
msgid "Legendary Authentication:"
|
msgid "Gamepad haptic feedback:"
|
||||||
msgstr "Авторизация в Legendary:"
|
msgstr "Тактильная обратная связь на геймпаде:"
|
||||||
|
|
||||||
msgid "Enter Legendary Authorization Code"
|
|
||||||
msgstr "Введите код авторизации Legendary"
|
|
||||||
|
|
||||||
msgid "Authorization Code:"
|
|
||||||
msgstr "Код авторизации:"
|
|
||||||
|
|
||||||
msgid "Submit Code"
|
|
||||||
msgstr "Отправить код"
|
|
||||||
|
|
||||||
msgid "Save Settings"
|
msgid "Save Settings"
|
||||||
msgstr "Сохранить настройки"
|
msgstr "Сохранить настройки"
|
||||||
@ -399,22 +404,6 @@ msgstr "Открытие страницы входа в Legendary в брауз
|
|||||||
msgid "Failed to open Legendary login page"
|
msgid "Failed to open Legendary login page"
|
||||||
msgstr "Не удалось открыть страницу входа в Legendary"
|
msgstr "Не удалось открыть страницу входа в Legendary"
|
||||||
|
|
||||||
msgid "Please enter an authorization code"
|
|
||||||
msgstr "Пожалуйста, введите код авторизации"
|
|
||||||
|
|
||||||
msgid "Successfully authenticated with Legendary"
|
|
||||||
msgstr "Успешная аутентификация с Legendary"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Legendary authentication failed: {0}"
|
|
||||||
msgstr "Сбой аутентификации в Legendary: {0}"
|
|
||||||
|
|
||||||
msgid "Legendary executable not found"
|
|
||||||
msgstr "Не найден исполняемый файл Legendary"
|
|
||||||
|
|
||||||
msgid "Unexpected error during authentication"
|
|
||||||
msgstr "Неожиданная ошибка при аутентификации"
|
|
||||||
|
|
||||||
msgid "Confirm Reset"
|
msgid "Confirm Reset"
|
||||||
msgstr "Подтвердите удаление"
|
msgstr "Подтвердите удаление"
|
||||||
|
|
||||||
@ -514,6 +503,42 @@ msgstr "Невозможно запустить игру пока запущен
|
|||||||
msgid "Launching"
|
msgid "Launching"
|
||||||
msgstr "Идёт запуск"
|
msgstr "Идёт запуск"
|
||||||
|
|
||||||
|
msgid "System Overlay"
|
||||||
|
msgstr "Системный оверлей"
|
||||||
|
|
||||||
|
msgid "Reboot"
|
||||||
|
msgstr "Перезагрузить"
|
||||||
|
|
||||||
|
msgid "Shutdown"
|
||||||
|
msgstr "Выключить"
|
||||||
|
|
||||||
|
msgid "Suspend"
|
||||||
|
msgstr "Перейти в ждущий режим"
|
||||||
|
|
||||||
|
msgid "Exit Application"
|
||||||
|
msgstr "Выйти из приложения"
|
||||||
|
|
||||||
|
msgid "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"
|
msgid "just now"
|
||||||
msgstr "только что"
|
msgstr "только что"
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ from portprotonqt.game_card import GameCard
|
|||||||
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
|
from portprotonqt.custom_widgets import FlowLayout, ClickableLabel, AutoSizeButton, NavLabel
|
||||||
from portprotonqt.input_manager import InputManager
|
from portprotonqt.input_manager import InputManager
|
||||||
from portprotonqt.context_menu_manager import ContextMenuManager
|
from portprotonqt.context_menu_manager import ContextMenuManager
|
||||||
|
from portprotonqt.system_overlay import SystemOverlay
|
||||||
|
|
||||||
from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel
|
from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel
|
||||||
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
|
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
|
||||||
@ -25,7 +26,7 @@ from portprotonqt.config_utils import (
|
|||||||
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
|
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_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
|
||||||
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_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.localization import _
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
@ -43,7 +44,7 @@ from datetime import datetime
|
|||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
"""Main window of PortProtonQT."""
|
"""Main window of PortProtonQt."""
|
||||||
settings_saved = Signal()
|
settings_saved = Signal()
|
||||||
games_loaded = Signal(list)
|
games_loaded = Signal(list)
|
||||||
update_progress = Signal(int) # Signal to update progress bar
|
update_progress = Signal(int) # Signal to update progress bar
|
||||||
@ -72,10 +73,10 @@ class MainWindow(QMainWindow):
|
|||||||
self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed)
|
self.settingsDebounceTimer.timeout.connect(self.applySettingsDelayed)
|
||||||
|
|
||||||
read_time_config()
|
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(
|
self.legendary_config_path = os.path.join(
|
||||||
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
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.makedirs(self.legendary_config_path, exist_ok=True)
|
||||||
os.environ["LEGENDARY_CONFIG_PATH"] = self.legendary_config_path
|
os.environ["LEGENDARY_CONFIG_PATH"] = self.legendary_config_path
|
||||||
@ -97,10 +98,11 @@ class MainWindow(QMainWindow):
|
|||||||
if not self.theme:
|
if not self.theme:
|
||||||
self.theme = default_styles
|
self.theme = default_styles
|
||||||
self.card_width = read_card_size()
|
self.card_width = read_card_size()
|
||||||
self.setWindowTitle("PortProtonQT")
|
self.setWindowTitle("PortProtonQt")
|
||||||
self.setMinimumSize(800, 600)
|
self.setMinimumSize(800, 600)
|
||||||
|
|
||||||
self.games = []
|
self.games = []
|
||||||
|
self.filtered_games = self.games
|
||||||
self.game_processes = []
|
self.game_processes = []
|
||||||
self.target_exe = None
|
self.target_exe = None
|
||||||
self.current_running_button = None
|
self.current_running_button = None
|
||||||
@ -259,39 +261,28 @@ class MainWindow(QMainWindow):
|
|||||||
self.update_status_message.emit
|
self.update_status_message.emit
|
||||||
)
|
)
|
||||||
elif display_filter == "favorites":
|
elif display_filter == "favorites":
|
||||||
def on_all_games(portproton_games, steam_games, epic_games):
|
def on_all_games(portproton_games, steam_games):
|
||||||
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
|
games = [game for game in portproton_games + steam_games if game[0] in favorites]
|
||||||
self.games_loaded.emit(games)
|
self.games_loaded.emit(games)
|
||||||
self._load_portproton_games_async(
|
self._load_portproton_games_async(
|
||||||
lambda pg: self._load_steam_games_async(
|
lambda pg: self._load_steam_games_async(
|
||||||
lambda sg: load_egs_games_async(
|
lambda sg: on_all_games(pg, sg)
|
||||||
self.legendary_path,
|
|
||||||
lambda eg: on_all_games(pg, sg, eg),
|
|
||||||
self.downloader,
|
|
||||||
self.update_progress.emit,
|
|
||||||
self.update_status_message.emit
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
def on_all_games(portproton_games, steam_games, epic_games):
|
def on_all_games(portproton_games, steam_games):
|
||||||
seen = set()
|
seen = set()
|
||||||
games = []
|
games = []
|
||||||
for game in portproton_games + steam_games + epic_games:
|
for game in portproton_games + steam_games:
|
||||||
name = game[0]
|
# Уникальный ключ: имя + exec_line
|
||||||
if name not in seen:
|
key = (game[0], game[4])
|
||||||
seen.add(name)
|
if key not in seen:
|
||||||
|
seen.add(key)
|
||||||
games.append(game)
|
games.append(game)
|
||||||
self.games_loaded.emit(games)
|
self.games_loaded.emit(games)
|
||||||
self._load_portproton_games_async(
|
self._load_portproton_games_async(
|
||||||
lambda pg: self._load_steam_games_async(
|
lambda pg: self._load_steam_games_async(
|
||||||
lambda sg: load_egs_games_async(
|
lambda sg: on_all_games(pg, sg)
|
||||||
self.legendary_path,
|
|
||||||
lambda eg: on_all_games(pg, sg, eg),
|
|
||||||
self.downloader,
|
|
||||||
self.update_progress.emit,
|
|
||||||
self.update_status_message.emit
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
@ -394,7 +385,7 @@ class MainWindow(QMainWindow):
|
|||||||
builtin_custom_folder = os.path.join(repo_root, "portprotonqt", "custom_data")
|
builtin_custom_folder = os.path.join(repo_root, "portprotonqt", "custom_data")
|
||||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
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)
|
os.makedirs(user_custom_folder, exist_ok=True)
|
||||||
|
|
||||||
builtin_cover = ""
|
builtin_cover = ""
|
||||||
@ -500,6 +491,11 @@ class MainWindow(QMainWindow):
|
|||||||
btn.setChecked(i == index)
|
btn.setChecked(i == index)
|
||||||
self.stackedWidget.setCurrentIndex(index)
|
self.stackedWidget.setCurrentIndex(index)
|
||||||
|
|
||||||
|
def openSystemOverlay(self):
|
||||||
|
"""Opens the system overlay dialog."""
|
||||||
|
overlay = SystemOverlay(self, self.theme)
|
||||||
|
overlay.exec()
|
||||||
|
|
||||||
def createSearchWidget(self) -> tuple[QWidget, QLineEdit]:
|
def createSearchWidget(self) -> tuple[QWidget, QLineEdit]:
|
||||||
self.container = QWidget()
|
self.container = QWidget()
|
||||||
self.container.setStyleSheet(self.theme.CONTAINER_STYLE)
|
self.container.setStyleSheet(self.theme.CONTAINER_STYLE)
|
||||||
@ -539,14 +535,20 @@ class MainWindow(QMainWindow):
|
|||||||
def startSearchDebounce(self, text):
|
def startSearchDebounce(self, text):
|
||||||
self.searchDebounceTimer.start()
|
self.searchDebounceTimer.start()
|
||||||
|
|
||||||
|
def on_slider_value_changed(self, value: int):
|
||||||
|
self.card_width = value
|
||||||
|
self.sizeSlider.setToolTip(f"{value} px")
|
||||||
|
save_card_size(value)
|
||||||
|
self.updateGameGrid()
|
||||||
|
|
||||||
def filterGamesDelayed(self):
|
def filterGamesDelayed(self):
|
||||||
"""Filters games based on search text and updates the grid."""
|
"""Filters games based on search text and updates the grid."""
|
||||||
text = self.searchEdit.text().strip().lower()
|
text = self.searchEdit.text().strip().lower()
|
||||||
if text == "":
|
if text == "":
|
||||||
self.updateGameGrid() # Use self.games directly
|
self.filtered_games = self.games
|
||||||
else:
|
else:
|
||||||
filtered = [game for game in self.games if text in game[0].lower()]
|
self.filtered_games = [game for game in self.games if text in game[0].lower()]
|
||||||
self.updateGameGrid(filtered)
|
self.updateGameGrid(self.filtered_games)
|
||||||
|
|
||||||
def createInstalledTab(self):
|
def createInstalledTab(self):
|
||||||
self.gamesLibraryWidget = QWidget()
|
self.gamesLibraryWidget = QWidget()
|
||||||
@ -579,33 +581,16 @@ class MainWindow(QMainWindow):
|
|||||||
self.sizeSlider.setFixedWidth(150)
|
self.sizeSlider.setFixedWidth(150)
|
||||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||||
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
|
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
|
||||||
|
self.sizeSlider.valueChanged.connect(self.on_slider_value_changed)
|
||||||
sliderLayout.addWidget(self.sizeSlider)
|
sliderLayout.addWidget(self.sizeSlider)
|
||||||
layout.addLayout(sliderLayout)
|
layout.addLayout(sliderLayout)
|
||||||
|
|
||||||
self.sliderDebounceTimer = QTimer(self)
|
|
||||||
self.sliderDebounceTimer.setSingleShot(True)
|
|
||||||
self.sliderDebounceTimer.setInterval(40)
|
|
||||||
|
|
||||||
def on_slider_value_changed():
|
|
||||||
self.setUpdatesEnabled(False)
|
|
||||||
self.card_width = self.sizeSlider.value()
|
|
||||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
|
||||||
self.updateGameGrid()
|
|
||||||
self.setUpdatesEnabled(True)
|
|
||||||
self.sizeSlider.valueChanged.connect(lambda val: self.sliderDebounceTimer.start())
|
|
||||||
self.sliderDebounceTimer.timeout.connect(on_slider_value_changed)
|
|
||||||
|
|
||||||
def calculate_card_width():
|
def calculate_card_width():
|
||||||
available_width = scrollArea.width() - 20
|
available_width = scrollArea.width() - 20
|
||||||
spacing = self.gamesListLayout._spacing
|
spacing = self.gamesListLayout._spacing
|
||||||
target_cards_per_row = 8
|
target_cards_per_row = 8
|
||||||
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
|
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
|
||||||
calculated_width = max(200, min(calculated_width, 250))
|
calculated_width = max(200, min(calculated_width, 250))
|
||||||
if not self.sizeSlider.value() == self.card_width:
|
|
||||||
self.card_width = calculated_width
|
|
||||||
self.sizeSlider.setValue(self.card_width)
|
|
||||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
|
||||||
self.updateGameGrid()
|
|
||||||
|
|
||||||
QTimer.singleShot(0, calculate_card_width)
|
QTimer.singleShot(0, calculate_card_width)
|
||||||
|
|
||||||
@ -621,7 +606,6 @@ class MainWindow(QMainWindow):
|
|||||||
self._last_width = self.width()
|
self._last_width = self.width()
|
||||||
if abs(self.width() - self._last_width) > 10:
|
if abs(self.width() - self._last_width) > 10:
|
||||||
self._last_width = self.width()
|
self._last_width = self.width()
|
||||||
self.sliderDebounceTimer.start()
|
|
||||||
|
|
||||||
def loadVisibleImages(self):
|
def loadVisibleImages(self):
|
||||||
visible_region = self.gamesListWidget.visibleRegion()
|
visible_region = self.gamesListWidget.visibleRegion()
|
||||||
@ -638,22 +622,38 @@ class MainWindow(QMainWindow):
|
|||||||
if games_list is None:
|
if games_list is None:
|
||||||
games_list = self.games
|
games_list = self.games
|
||||||
if not games_list:
|
if not games_list:
|
||||||
self.clearLayout(self.gamesListLayout)
|
# Скрываем все карточки, если список пуст
|
||||||
|
for card in self.game_card_cache.values():
|
||||||
|
card.hide()
|
||||||
self.game_card_cache.clear()
|
self.game_card_cache.clear()
|
||||||
self.pending_images.clear()
|
self.pending_images.clear()
|
||||||
|
self.gamesListWidget.updateGeometry()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create a set of game names for quick lookup
|
# Создаем словарь текущих игр с уникальным ключом (name + exec_line)
|
||||||
current_games = {game_data[0]: game_data for game_data in games_list}
|
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):
|
current_game_keys = set(current_games.keys())
|
||||||
return # No changes needed, skip update
|
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
|
layout_changed = False
|
||||||
|
|
||||||
# Remove cards for games no longer in the list
|
|
||||||
for card_key in list(self.game_card_cache.keys()):
|
for card_key in list(self.game_card_cache.keys()):
|
||||||
if card_key not in current_games:
|
if card_key not in current_games:
|
||||||
card = self.game_card_cache.pop(card_key)
|
card = self.game_card_cache.pop(card_key)
|
||||||
@ -663,11 +663,15 @@ class MainWindow(QMainWindow):
|
|||||||
del self.pending_images[card_key]
|
del self.pending_images[card_key]
|
||||||
layout_changed = True
|
layout_changed = True
|
||||||
|
|
||||||
# Add or update cards for current games
|
# Добавляем новые карточки и обновляем существующие
|
||||||
for game_data in games_list:
|
for game_data in games_list:
|
||||||
game_name = game_data[0]
|
game_name = game_data[0]
|
||||||
if game_name not in self.game_card_cache:
|
game_key = (game_name, game_data[4])
|
||||||
# Create new card
|
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(
|
card = GameCard(
|
||||||
*game_data,
|
*game_data,
|
||||||
select_callback=self.openGameDetailPage,
|
select_callback=self.openGameDetailPage,
|
||||||
@ -675,7 +679,7 @@ class MainWindow(QMainWindow):
|
|||||||
card_width=self.card_width,
|
card_width=self.card_width,
|
||||||
context_menu_manager=self.context_menu_manager
|
context_menu_manager=self.context_menu_manager
|
||||||
)
|
)
|
||||||
# Connect context menu signals
|
# Подключаем сигналы контекстного меню
|
||||||
card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
|
card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
|
||||||
card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
|
card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
|
||||||
card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu)
|
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.addToSteamRequested.connect(self.context_menu_manager.add_to_steam)
|
||||||
card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam)
|
card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam)
|
||||||
card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder)
|
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)
|
self.gamesListLayout.addWidget(card)
|
||||||
layout_changed = True
|
layout_changed = True
|
||||||
elif self.card_width != getattr(self, '_last_card_width', None):
|
else:
|
||||||
# Update size only if card_width has changed
|
# Обновляем видимость существующей карточки
|
||||||
card = self.game_card_cache[game_name]
|
card = self.game_card_cache[game_key]
|
||||||
card.setFixedWidth(self.card_width + 20) # Account for extra_margin in GameCard
|
card.setVisible(should_be_visible)
|
||||||
|
|
||||||
# Store the current card_width
|
# Сохраняем текущий card_width
|
||||||
self._last_card_width = self.card_width
|
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:
|
if layout_changed:
|
||||||
|
self.gamesListLayout.update()
|
||||||
self.gamesListWidget.updateGeometry()
|
self.gamesListWidget.updateGeometry()
|
||||||
|
self.gamesListWidget.update()
|
||||||
|
|
||||||
|
# Загружаем изображения для видимых карточек
|
||||||
|
self.loadVisibleImages()
|
||||||
|
|
||||||
def clearLayout(self, layout):
|
def clearLayout(self, layout):
|
||||||
"""Удаляет все виджеты из layout."""
|
"""Удаляет все виджеты из layout."""
|
||||||
@ -742,6 +748,7 @@ class MainWindow(QMainWindow):
|
|||||||
return
|
return
|
||||||
|
|
||||||
dialog = AddGameDialog(self, self.theme)
|
dialog = AddGameDialog(self, self.theme)
|
||||||
|
dialog.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||||
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
|
self.current_add_game_dialog = dialog # Сохраняем ссылку на диалог
|
||||||
|
|
||||||
# Предзаполняем путь к .exe при drag-and-drop
|
# Предзаполняем путь к .exe при drag-and-drop
|
||||||
@ -778,7 +785,7 @@ class MainWindow(QMainWindow):
|
|||||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
custom_folder = os.path.join(
|
custom_folder = os.path.join(
|
||||||
xdg_data_home,
|
xdg_data_home,
|
||||||
"PortProtonQT",
|
"PortProtonQt",
|
||||||
"custom_data",
|
"custom_data",
|
||||||
exe_name
|
exe_name
|
||||||
)
|
)
|
||||||
@ -920,7 +927,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# 3. Games display_filter
|
# 3. Games display_filter
|
||||||
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
|
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
|
||||||
self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"]
|
self.filter_labels = [_("all"), "steam", "portproton", _("favorites")]
|
||||||
self.gamesDisplayCombo = QComboBox()
|
self.gamesDisplayCombo = QComboBox()
|
||||||
self.gamesDisplayCombo.addItems(self.filter_labels)
|
self.gamesDisplayCombo.addItems(self.filter_labels)
|
||||||
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||||
@ -983,6 +990,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
||||||
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
|
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
self.autoFullscreenGamepadTitle = QLabel(_("Auto Fullscreen on Gamepad connected:"))
|
self.autoFullscreenGamepadTitle = QLabel(_("Auto Fullscreen on Gamepad connected:"))
|
||||||
self.autoFullscreenGamepadTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
self.autoFullscreenGamepadTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
self.autoFullscreenGamepadTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
self.autoFullscreenGamepadTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
@ -990,36 +998,16 @@ class MainWindow(QMainWindow):
|
|||||||
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
||||||
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
||||||
|
|
||||||
# 7. Legendary Authentication
|
# 7. Gamepad haptic feedback config
|
||||||
self.legendaryAuthButton = AutoSizeButton(
|
self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
|
||||||
_("Open Legendary Login"),
|
self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||||
icon=self.theme_manager.get_icon("login")
|
self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||||
)
|
self.gamepadRumbleTitle = QLabel(_("Gamepad haptic feedback:"))
|
||||||
self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
self.gamepadRumbleTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||||
self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
self.gamepadRumbleTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
self.legendaryAuthButton.clicked.connect(self.openLegendaryLogin)
|
current_rumble_state = read_rumble_config()
|
||||||
self.legendaryAuthTitle = QLabel(_("Legendary Authentication:"))
|
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
||||||
self.legendaryAuthTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
||||||
self.legendaryAuthTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
||||||
formLayout.addRow(self.legendaryAuthTitle, self.legendaryAuthButton)
|
|
||||||
|
|
||||||
self.legendaryCodeEdit = QLineEdit()
|
|
||||||
self.legendaryCodeEdit.setPlaceholderText(_("Enter Legendary Authorization Code"))
|
|
||||||
self.legendaryCodeEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
|
|
||||||
self.legendaryCodeEdit.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
||||||
self.legendaryCodeTitle = QLabel(_("Authorization Code:"))
|
|
||||||
self.legendaryCodeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
|
||||||
self.legendaryCodeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
||||||
formLayout.addRow(self.legendaryCodeTitle, self.legendaryCodeEdit)
|
|
||||||
|
|
||||||
self.submitCodeButton = AutoSizeButton(
|
|
||||||
_("Submit Code"),
|
|
||||||
icon=self.theme_manager.get_icon("save")
|
|
||||||
)
|
|
||||||
self.submitCodeButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
|
||||||
self.submitCodeButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
||||||
self.submitCodeButton.clicked.connect(self.submitLegendaryCode)
|
|
||||||
formLayout.addRow(QLabel(""), self.submitCodeButton)
|
|
||||||
|
|
||||||
layout.addLayout(formLayout)
|
layout.addLayout(formLayout)
|
||||||
|
|
||||||
@ -1071,37 +1059,6 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"Failed to open Legendary login page: {e}")
|
logger.error(f"Failed to open Legendary login page: {e}")
|
||||||
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
|
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
|
||||||
|
|
||||||
def submitLegendaryCode(self):
|
|
||||||
"""Submits the Legendary authorization code using the legendary CLI."""
|
|
||||||
auth_code = self.legendaryCodeEdit.text().strip()
|
|
||||||
if not auth_code:
|
|
||||||
QMessageBox.warning(self, _("Error"), _("Please enter an authorization code"))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Execute legendary auth command
|
|
||||||
result = subprocess.run(
|
|
||||||
[self.legendary_path, "auth", "--code", auth_code],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
logger.info("Legendary authentication successful: %s", result.stdout)
|
|
||||||
self.statusBar().showMessage(_("Successfully authenticated with Legendary"), 3000)
|
|
||||||
self.legendaryCodeEdit.clear()
|
|
||||||
# Reload Epic Games Store games after successful authentication
|
|
||||||
self.games = self.loadGames()
|
|
||||||
self.updateGameGrid()
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
logger.error("Legendary authentication failed: %s", e.stderr)
|
|
||||||
self.statusBar().showMessage(_("Legendary authentication failed: {0}").format(e.stderr), 5000)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error("Legendary executable not found at %s", self.legendary_path)
|
|
||||||
self.statusBar().showMessage(_("Legendary executable not found"), 5000)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Unexpected error during Legendary authentication: %s", str(e))
|
|
||||||
self.statusBar().showMessage(_("Unexpected error during authentication"), 5000)
|
|
||||||
|
|
||||||
def resetSettings(self):
|
def resetSettings(self):
|
||||||
"""Сбрасывает настройки и перезапускает приложение."""
|
"""Сбрасывает настройки и перезапускает приложение."""
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
@ -1172,6 +1129,10 @@ class MainWindow(QMainWindow):
|
|||||||
auto_fullscreen_gamepad = self.autoFullscreenGamepadCheckBox.isChecked()
|
auto_fullscreen_gamepad = self.autoFullscreenGamepadCheckBox.isChecked()
|
||||||
save_auto_fullscreen_gamepad(auto_fullscreen_gamepad)
|
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():
|
for card in self.game_card_cache.values():
|
||||||
card.update_badge_visibility(filter_key)
|
card.update_badge_visibility(filter_key)
|
||||||
|
|
||||||
@ -1288,11 +1249,15 @@ class MainWindow(QMainWindow):
|
|||||||
self.statusBar().showMessage(_("Theme '{0}' applied successfully").format(selected_theme), 3000)
|
self.statusBar().showMessage(_("Theme '{0}' applied successfully").format(selected_theme), 3000)
|
||||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
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)
|
os.makedirs(os.path.dirname(state_file), exist_ok=True)
|
||||||
with open(state_file, "w", encoding="utf-8") as f:
|
try:
|
||||||
f.write("theme_tab\n")
|
with open(state_file, "w", encoding="utf-8") as f:
|
||||||
QTimer.singleShot(500, lambda: self.restart_application())
|
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:
|
else:
|
||||||
self.statusBar().showMessage(_("Error applying theme '{0}'").format(selected_theme), 3000)
|
self.statusBar().showMessage(_("Error applying theme '{0}'").format(selected_theme), 3000)
|
||||||
|
|
||||||
@ -1310,14 +1275,28 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def restore_state(self):
|
def restore_state(self):
|
||||||
"""Восстанавливает состояние приложения после перезапуска."""
|
"""Восстанавливает состояние приложения после перезапуска."""
|
||||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
state_file = os.path.join(xdg_cache_home, "PortProtonQT", "state.txt")
|
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):
|
if os.path.exists(state_file):
|
||||||
with open(state_file, encoding="utf-8") as f:
|
try:
|
||||||
state = f.read().strip()
|
with open(state_file, encoding="utf-8") as f:
|
||||||
if state == "theme_tab":
|
state = f.read().strip()
|
||||||
self.switchTab(5)
|
logger.info(f"State file contents: '{state}'")
|
||||||
os.remove(state_file)
|
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):
|
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_size=16,
|
||||||
icon_space=3,
|
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.setFixedWidth(badge_width)
|
||||||
anticheatLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://areweanticheatyet.com/game/{name.lower().replace(' ', '-')}")))
|
anticheatLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://areweanticheatyet.com/game/{name.lower().replace(' ', '-')}")))
|
||||||
anticheat_visible = True
|
anticheat_visible = True
|
||||||
|
@ -49,7 +49,7 @@ def decode_text(text: str) -> str:
|
|||||||
def get_cache_dir():
|
def get_cache_dir():
|
||||||
"""Возвращает путь к каталогу кэша, создаёт его при необходимости."""
|
"""Возвращает путь к каталогу кэша, создаёт его при необходимости."""
|
||||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
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)
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
return cache_dir
|
return cache_dir
|
||||||
|
|
||||||
|
109
portprotonqt/system_overlay.py
Normal file
109
portprotonqt/system_overlay.py
Normal file
@ -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()
|
@ -11,7 +11,7 @@ logger = get_logger(__name__)
|
|||||||
# Папка, где располагаются все дополнительные темы
|
# Папка, где располагаются все дополнительные темы
|
||||||
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
THEMES_DIRS = [
|
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")
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "themes")
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[Metainfo]
|
[Metainfo]
|
||||||
author = BlackSnaker
|
author = BlackSnaker
|
||||||
author_link =
|
author_link =
|
||||||
description = Стандартная тема PortProtonQT (светлый вариант)
|
description = Стандартная тема PortProtonQt (светлый вариант)
|
||||||
name = Light
|
name = Light
|
||||||
|
@ -416,6 +416,26 @@ def get_protondb_badge_style(tier):
|
|||||||
font-weight: bold;
|
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
|
||||||
STEAM_BADGE_STYLE= """
|
STEAM_BADGE_STYLE= """
|
||||||
qproperty-alignment: AlignCenter;
|
qproperty-alignment: AlignCenter;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[Metainfo]
|
[Metainfo]
|
||||||
author = Dervart
|
author = Dervart
|
||||||
author_link =
|
author_link =
|
||||||
description = Стандартная тема PortProtonQT (тёмный вариант)
|
description = Стандартная тема PortProtonQt (тёмный вариант)
|
||||||
name = Clean Dark
|
name = Clean Dark
|
||||||
|
@ -8,6 +8,40 @@ current_theme_name = read_theme_from_config()
|
|||||||
favoriteLabelSize = 48, 48
|
favoriteLabelSize = 48, 48
|
||||||
pixmapsScaledSize = 60, 60
|
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 = """
|
MAIN_WINDOW_HEADER_STYLE = """
|
||||||
QFrame {
|
QFrame {
|
||||||
@ -90,6 +124,13 @@ SEARCH_EDIT_STYLE = """
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
SETTINGS_CHECKBOX_STYLE = """
|
||||||
|
QCheckBox:focus {
|
||||||
|
border: 2px solid #409EFF;
|
||||||
|
background: #404554;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
# ОТКЛЮЧАЕМ РАМКУ У QScrollArea
|
# ОТКЛЮЧАЕМ РАМКУ У QScrollArea
|
||||||
SCROLL_AREA_STYLE = """
|
SCROLL_AREA_STYLE = """
|
||||||
QWidget {
|
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;"
|
TAB_TITLE_STYLE = "font-family: 'Play'; font-size: 24px; color: #ffffff; background-color: none;"
|
||||||
CONTENT_STYLE = """
|
CONTENT_STYLE = """
|
||||||
@ -416,6 +479,27 @@ def get_protondb_badge_style(tier):
|
|||||||
font-weight: bold;
|
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
|
||||||
STEAM_BADGE_STYLE= """
|
STEAM_BADGE_STYLE= """
|
||||||
qproperty-alignment: AlignCenter;
|
qproperty-alignment: AlignCenter;
|
||||||
@ -457,6 +541,10 @@ MESSAGE_BOX_STYLE = """
|
|||||||
background: #09bec8;
|
background: #09bec8;
|
||||||
border-color: rgba(255, 255, 255, 0.3);
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
QMessageBox QPushButton:focus {
|
||||||
|
border: 2px solid #409EFF;
|
||||||
|
background: #404554;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# СТИЛИ ДЛЯ ВКЛАДКИ НАСТРОЕК PORTPROTON
|
# СТИЛИ ДЛЯ ВКЛАДКИ НАСТРОЕК PORTPROTON
|
||||||
|
@ -10,7 +10,7 @@ logger = get_logger(__name__)
|
|||||||
def get_cache_file_path():
|
def get_cache_file_path():
|
||||||
"""Возвращает путь к файлу кеша portproton_last_launch."""
|
"""Возвращает путь к файлу кеша portproton_last_launch."""
|
||||||
cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
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):
|
def save_last_launch(exe_name, launch_time):
|
||||||
"""
|
"""
|
||||||
|
Reference in New Issue
Block a user