78 Commits
main ... main

Author SHA1 Message Date
b82080600f fix(renovate): disable workflow and python version update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 23:01:00 +05:00
05693514aa fix(renovate): uv lock file maintance
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:50:52 +05:00
1c2835a933 chore(deps): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:41:01 +05:00
d229914fb6 Revert "chore(deps): pin dependencies"
This reverts commit 4d58830910.
2025-06-20 22:06:21 +05:00
ce69a18249 fix(renovate): workflow ignore
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 22:06:01 +05:00
4d58830910 chore(deps): pin dependencies 2025-06-20 16:52:33 +00:00
016ba537be fix(renovate): config syntax again
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:51:37 +05:00
6eeb93f6ba fix(renovate): config syntax
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:48:49 +05:00
3f5d058740 fix(renovate): RENOVATE_CONFIG_FILE
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:44:25 +05:00
1a9228b76d ci: added renovate auto update bot
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-20 21:37:14 +05:00
e9e0bea854 feat: stay overlay on top
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 19:31:33 +05:00
f7d9f5c150 chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 18:36:45 +05:00
bcb5987d31 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 18:32:50 +05:00
b1aa987e4e fix: ensure application quits on window close
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-19 18:30:24 +05:00
f4c8b70bd0 feat: add --session CLI argument for start gamescope
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-18 22:48:24 +05:00
ff960df77c feat: transfer focus to hovered GameCard with mutual exclusivity
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 23:11:25 +05:00
a57f509295 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 23:01:38 +05:00
32bbe89911 fix: enforce mutual exclusivity of hovered and focused states in GameCard
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 22:58:57 +05:00
593db00166 fix(themes): typo in GAME_CARD_ANIMATION
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 14:39:21 +05:00
79a78c785b chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 14:26:19 +05:00
0b92d058a9 feat: move GameCard animation properties to styles
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-17 14:24:06 +05:00
9df22edfc9 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 22:36:56 +05:00
4559231712 fix: prevent multiple GameCard highlight animations on rapid mouse movement
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 22:34:06 +05:00
18dbd42369 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 17:14:22 +05:00
76c0e607c5 fedora 40 is EOL
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 17:11:52 +05:00
a91c9dacd8 fix(build): fedora dependency
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-16 17:10:18 +05:00
62b8da2dc4 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 22:53:23 +05:00
b77609cb5f fix: resolve Pyright type errors
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 22:51:50 +05:00
56b105d7b4 fix: completly exit on app close
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 22:33:33 +05:00
14687d12ca feat(ci): extract downloaded artifacts
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 22:14:01 +05:00
6a648a2a8d fix(ci): install original-awk
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 18:33:41 +05:00
c0b2006338 fix(ci): release body generate
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 18:13:39 +05:00
2c2fc082a7 fix(ci): use download-artifact@v3
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 17:51:44 +05:00
66e1871304 chore: bump ver
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 17:17:50 +05:00
6daa28b393 chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 16:54:36 +05:00
a3445898e5 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 16:49:59 +05:00
076d06a9c0 fix(input-manager): remap add_game to X for avoid conflicts with PS
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 16:44:26 +05:00
d85e7f058f fix(input-manager): restore gamepad rumble on game launch by delaying disable_gamepad_handling
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 16:39:32 +05:00
dd05ef8a1f feat: close QMessageBox using confirm key on gamepad
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 16:35:48 +05:00
326b2d7411 chore: update steam apps list 2025-06-15T10:52:34Z 2025-06-15 10:52:34 +00:00
d280cf2531 feat(dev-scripts): parse all ppdb topics from our forum
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 15:48:16 +05:00
3cc40154b0 fix: disable gamepad handling on game start thanks to @Vector_null
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 14:00:06 +05:00
f765b5e840 fix: restore else block in ClickableLabel paintEvent to render text without icon
Restore the `else` block in `paintEvent` of `ClickableLabel` to set `text_rect` when no icon is present. This fixes a regression where `favoriteLabel` in `GameCard` was invisible but clickable, as text (`★` or `☆`) was not rendered without a pixmap.

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 00:52:18 +05:00
c54c3273a0 chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 14:18:14 +05:00
502b5b5256 feat: change badge position and size on slider change
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 14:17:11 +05:00
0b45ba963a chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 11:51:04 +05:00
7becbf5de2 feat(input_manager): added change slider size to RT and LT
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 11:49:28 +05:00
66b4b82d49 feat: change game card size only on slider released
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 11:14:22 +05:00
dbf3a30119 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 10:38:31 +05:00
4c2e2a9c8d feat: drop title translate from FramelessWindow
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 10:35:46 +05:00
802d5a2ba1 chore(metainfo): sync screenshots with standart theme
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 00:07:42 +05:00
1d47caf4aa chore(readme): update todo
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 00:01:21 +05:00
502664438c chore: update screenshots in standart theme
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-14 00:00:02 +05:00
f4e155dade chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-13 23:49:25 +05:00
74400d1389 feat: align keyboard arrow key navigation with D-pad logic
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-13 23:34:11 +05:00
2a46cf7a2f feat: no longer lock the full screen button when automatic full screen mode is enabled
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-13 19:31:35 +05:00
f105af01ef fix: resolve Pyright type errors in SystemOverlay class
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-13 17:43:49 +05:00
e9ecb466b2 combobox styles for hover & focus 2025-06-13 18:09:06 +07:00
2ce41697ef changed context_menu_style 2025-06-13 17:50:36 +07:00
997e66afa6 system_overlay now borderless thanks to @Vector_null 2025-06-13 17:36:27 +07:00
bad91fed4e dialogs.py add input styles 2025-06-13 11:44:20 +07:00
a1bdff73fe getAntiCheatIconFilename expand the list of status 2025-06-13 11:21:57 +07:00
0c7cb0092b change getAntiCheatIconFilename 2025-06-13 11:11:32 +07:00
120f2a5590 change color of icons for areweanticheat 2025-06-13 11:04:17 +07:00
fbe8d87b3d system_overlay change from QPushButton to AutoSizeButton 2025-06-13 10:56:39 +07:00
568120fb0e add icons for system_overlay 2025-06-13 10:55:01 +07:00
bff5e456cf change ACTION_BUTTON_STYLE style 2025-06-13 09:30:43 +07:00
de3b95d06c merge upstream 2025-06-12 09:52:06 +00:00
db95120b87 change addgame, play, find_games hover color 2025-06-12 16:50:45 +07:00
337db17467 add areweanticheat status icons 2025-06-12 16:41:43 +07:00
dbf1340f88 feat: added colors to AreWeAntiCheatYet badges
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-12 14:37:03 +05:00
09066521e8 add epic games & gog icons 2025-06-12 16:25:39 +07:00
186ee048f7 add background color to NAV_WIDGET_STYLE 2025-06-12 15:58:03 +07:00
79e2ad1997 change nav buttons focus color 2025-06-12 15:50:24 +07:00
a4a3271df9 fix hardlink to checkbox icon 2025-06-12 15:44:56 +07:00
213709e88b add login icon 2025-06-12 15:37:45 +07:00
9f86eae5ef fix focus styles for buttons, text fields & checkbox 2025-06-12 15:13:46 +07:00
748f9c886b add checkboxes styles 2025-06-12 14:33:37 +07:00
63 changed files with 8980 additions and 1424 deletions

View File

@ -40,7 +40,7 @@ jobs:
strategy: strategy:
matrix: matrix:
fedora_version: [40, 41, 42, rawhide] fedora_version: [41, 42, rawhide]
container: container:
image: fedora:${{ matrix.fedora_version }} image: fedora:${{ matrix.fedora_version }}

View File

@ -8,7 +8,7 @@ on:
env: env:
# Common version, will be used for tagging the release # Common version, will be used for tagging the release
VERSION: v0.1.1 VERSION: 0.1.2
PKGDEST: "/tmp/portprotonqt" PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt" PACKAGE: "portprotonqt"
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
@ -97,7 +97,7 @@ jobs:
strategy: strategy:
matrix: matrix:
fedora_version: [40, 41, 42, rawhide] fedora_version: [41, 42, rawhide]
container: container:
image: fedora:${{ matrix.fedora_version }} image: fedora:${{ matrix.fedora_version }}
@ -140,16 +140,29 @@ jobs:
needs: [build-appimage, build-arch, build-fedora] needs: [build-appimage, build-arch, build-fedora]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: https://gitea.com/actions/checkout@v4
- name: Install required dependencies
run: |
sudo apt update
sudo apt install -y original-awk unzip
- name: Download all artifacts - name: Download all artifacts
uses: https://gitea.com/actions/download-artifact@v4 uses: https://gitea.com/actions/download-artifact@v3
with: with:
path: release/ path: release/
- name: Extract downloaded artifacts
run: |
mkdir -p extracted
find release/ -name '*.zip' -exec unzip -o {} -d extracted/ \;
find extracted/ -type f -exec mv {} release/ \;
rm -rf extracted/
- name: Extract changelog for version - name: Extract changelog for version
id: changelog id: changelog
run: | run: |
VERSION="${{ env.VERSION }}" VERSION="${{ env.VERSION }}"
VERSION=${VERSION#v} # Remove 'v' prefix if present
awk "/^## \\[$VERSION\\]/ {flag=1; next} /^## \\[/ || /^---/ {flag=0} flag" CHANGELOG.md > changelog.txt awk "/^## \\[$VERSION\\]/ {flag=1; next} /^## \\[/ || /^---/ {flag=0} flag" CHANGELOG.md > changelog.txt
- name: Release - name: Release
@ -157,7 +170,7 @@ jobs:
with: with:
body_path: changelog.txt body_path: changelog.txt
token: ${{ env.GITEA_TOKEN }} token: ${{ env.GITEA_TOKEN }}
tag_name: ${{ env.VERSION }} tag_name: v${{ env.VERSION }}
prerelease: true prerelease: true
files: release/**/* files: release/**/*
sha256sum: true sha256sum: true

View File

@ -30,6 +30,8 @@ jobs:
run: python dev-scripts/get_id.py run: python dev-scripts/get_id.py
env: env:
STEAM_KEY: ${{ secrets.STEAM_KEY }} STEAM_KEY: ${{ secrets.STEAM_KEY }}
LINUX_GAMING_API_KEY: ${{ secrets.LINUX_GAMING_API_KEY }}
LINUX_GAMING_API_USERNAME: ${{ secrets.LINUX_GAMING_API_USERNAME }}
- name: Commit and push changes - name: Commit and push changes
env: env:

View File

@ -0,0 +1,18 @@
name: renovate
on:
workflow_dispatch:
schedule:
- cron: "@daily"
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:41.1.3
steps:
- uses: https://gitea.com/actions/checkout@v4
- run: renovate
env:
RENOVATE_CONFIG_FILE: "/workspace/Boria138/PortProtonQt/config.js"
LOG_LEVEL: "debug"
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}

View File

@ -5,29 +5,45 @@
## [Unreleased] ## [Unreleased]
### Added
- Аргумент `--session` для запуска приложения в gamescope с GAMESCOPE_CMD
### Changed
- Удалены сборки для Fedora 40
- Перенесены параметры анимации GameCard в `styles.py` с подробной документацией для поддержки кастомизации тем.
- Статус выделения и наведения на карточки теперь взаимоисключают друг друга
### Fixed
- Дублирование обводки выделения карточек при быстром перемешении мыши
- Завершение приложения при закритие окна
---
## [0.1.2] - 2025-06-15
### Added ### Added
- Кнопки сброса настроек и очистки кэша - Кнопки сброса настроек и очистки кэша
- Бейдж PortProton - Бейдж PortProton
- Зависимость от `xdg-utils` - Зависимость от `xdg-utils`
- Интеграция статуса WeAntiCheatYet в карточку - Интеграция статуса WeAntiCheatYet в карточку
- Стили в 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 при управлении геймпадом - Поддержка управления геймпадом в `QMenu` и `QComboBox`
- Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме - Аргумент `--fullscreen` для запуска приложения в полноэкранном режиме
- Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или между сессиями - Оверлей на кнопку Insert или кнопку Xbox/PS на геймпаде для закрытия приложения, выключения, перезагрузки и перехода в спящий режим или переключения между сессиями
- [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) - [Gamescope сессия](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
- Мапинги управления для Dualshock 4 и DualSense - Пресеты управления для DualShock 4 и DualSense
- Настройка тактильной обратной связи на геймпаде при запуске игры (по умолчанию отключена) - Настройка тактильной отдачи на геймпаде при запуске игры (по умолчанию выключена)
- Переводы пунктов настроек
### Changed ### Changed
- Обновлены все иконки - Обновлены все иконки
@ -36,29 +52,33 @@
- Логика контекстного меню вынесена в `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 для непрерывного переключения карточек
- D-pad больше не переключает вкладки, только RB и LB - Объединён обработчик управления стрелками клавиатуры и D-pad для консистентности
- D-pad больше не переключает вкладки (только кнопки RB/LB)
- Кнопка добавления игры больше не фокусируется - Кнопка добавления игры больше не фокусируется
- Диалог добавления игры теперь открывается только в библиотеке - Диалог добавления игры теперь открывается только в библиотеке
- Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt - Удалены все упоминания PortProtonQT из кода и заменены на PortProtonQt
- Размер карточек теперь меняется только при отпускании слайдера
- Слайдер теперь управляется через тригеры на геймпаде
- Диалог добавления игры теперь открывается на X, а не на Y
### Fixed ### Fixed
- Обработка несуществующей темы с возвратом к «standard» - Возврат к теме «standard» при выборе несуществующей темы
- Открытие контекстного меню - Корректное открытие контекстного меню
- Запуск при отсутствии exiftool - Запуск приложения при отсутствии `exiftool`
- Переводы пунктов настроек - Предотвращено бесконечное обращение к `get_portproton_location`
- Бесконечное обращение к `get_portproton_location` - Обновлены ссылки на документацию в README
- Ссылки на документацию в README - Устранён traceback при отсутствии обложек (placeholder)
- Traceback при загрузке placeholder при отсутствии обложек - Устранены утечки памяти при загрузке обложек
- Утечки памяти при загрузке обложек - Исправлены ошибки при подключении геймпада
- Ошибки при подключении геймпада из-за работы в разных потоках - Предотвращено многократное открытие диалога добавления игры через геймпад
- Многократное открытие диалога добавления игры при использовании геймпада - Корректная обработка событий геймпада во время игры
- Перехват событий геймпада во время работы игры - Убийсво всех процессов "зомби" при закрытии программы
--- ---

View File

@ -4,7 +4,6 @@
<p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p> <p align="center">Удобный графический интерфейс для управления и запуска игр из PortProton, Steam и Epic Games Store. Оно объединяет библиотеки игр в единый центр для лёгкой навигации и организации. Лёгкая структура и кроссплатформенная поддержка обеспечивают цельный игровой опыт без необходимости использования нескольких лаунчеров. Интеграция с PortProton упрощает запуск Windows-игр на Linux с минимальной настройкой.</p>
</div> </div>
## В планах ## В планах
- [X] Адаптировать структуру проекта для поддержки инструментов сборки - [X] Адаптировать структуру проекта для поддержки инструментов сборки
@ -15,7 +14,8 @@
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено) - [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
- [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор) - [X] Добавить метаданные для тем (скриншоты, описание, домашняя страница и автор)
- [ ] Продумать систему вкладок вместо текущей - [ ] Продумать систему вкладок вместо текущей
- [X] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt) - [ ] [Добавить сессию Gamescope, аналогичную той, что используется в SteamOS](https://git.linux-gaming.ru/Boria138/gamescope-session-portprotonqt)
- [ ] Разобраться почему теряется часть стилей в Gamescope
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800) - [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots) - [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
- [X] Получать описания и названия игр из базы данных Steam - [X] Получать описания и названия игр из базы данных Steam
@ -41,7 +41,10 @@
- [X] Добавить парсинг ярлыков из Steam - [X] Добавить парсинг ярлыков из Steam
- [X] Добавить парсинг ярлыков из EGS (скрыто для переработки) - [X] Добавить парсинг ярлыков из EGS (скрыто для переработки)
- [ ] Избавиться от бинарника legendary - [ ] Избавиться от бинарника legendary
- [ ] Добавить запуск и скачивание игр из EGS - [X] Добавить запуск игр из EGS
- [ ] Добавить скачивание игр из EGS
- [ ] Добавить поддержку запуска сторонних игр из EGS
- [ ] Добавить поддержку запуска игр с EOS
- [ ] Добавить авторизацию в 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)
@ -63,9 +66,11 @@
- [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант) - [X] Определиться с названием (PortProtonQt или PortProtonQT или вообще третий вариант)
- [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?) - [ ] Добавить данные с HowLongToBeat на страницу с деталями игры (?)
- [X] Добавить виброотдачу на геймпаде при запуске игры - [X] Добавить виброотдачу на геймпаде при запуске игры
- [ ] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63) - [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)) - [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
- [ ] Скопировать логику управления с D-pad на стрелки с клавиатуры - [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
- [ ] Доделать светлую тему
- [ ] Добавить подсказки к управлению с геймпада
### Установка (devel) ### Установка (devel)
@ -109,6 +114,5 @@ pre-commit run --all-files
> [!WARNING] > [!WARNING]
> Проект находится на стадии WIP (work in progress) корректная работоспособность не гарантирована > Проект находится на стадии WIP (work in progress) корректная работоспособность не гарантирована
> [!WARNING] > [!WARNING]
> **Будьте осторожны!** Если вы берёте тему не из официального репозитория или надёжного источника, убедитесь, что в её файле `styles.py` нет вредоносного или нежелательного кода. Поскольку `styles.py` — это обычный Python-файл, он может содержать любые инструкции. Всегда проверяйте содержимое чужих тем перед использованием. > **Будьте осторожны!** Если вы берёте тему не из официального репозитория или надёжного источника, убедитесь, что в её файле `styles.py` нет вредоносного или нежелательного кода. Поскольку `styles.py` — это обычный Python-файл, он может содержать любые инструкции. Всегда проверяйте содержимое чужих тем перед использованием.

View File

@ -25,7 +25,7 @@ AppDir:
id: ru.linux_gaming.PortProtonQt id: ru.linux_gaming.PortProtonQt
name: PortProtonQt name: PortProtonQt
icon: ru.linux_gaming.PortProtonQt icon: ru.linux_gaming.PortProtonQt
version: 0.1.1 version: 0.1.2
exec: usr/bin/python3 exec: usr/bin/python3
exec_args: "-m portprotonqt.app $@" exec_args: "-m portprotonqt.app $@"

View File

@ -1,5 +1,5 @@
pkgname=portprotonqt pkgname=portprotonqt
pkgver=0.1.1 pkgver=0.1.2
pkgrel=1 pkgrel=1
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store" pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
arch=('any') arch=('any')

View File

@ -28,19 +28,19 @@ BuildRequires: git
%package -n python3-%{pypi_name}-git %package -n python3-%{pypi_name}-git
Summary: %{summary} Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}} %{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3dist(babel) Requires: python3-babel
Requires: python3dist(evdev) Requires: python3-evdev
Requires: python3dist(icoextract) Requires: python3-icoextract
Requires: python3dist(numpy) Requires: python3-numpy
Requires: python3dist(orjson) Requires: python3-orjson
Requires: python3dist(psutil) Requires: python3-psutil
Requires: python3dist(pyside6) Requires: python3-pyside6
Requires: python3dist(pyudev) Requires: python3-pyudev
Requires: python3dist(requests) Requires: python3-requests
Requires: python3dist(tqdm) Requires: python3-tqdm
Requires: python3dist(vdf) Requires: python3-vdf
Requires: python3dist(pefile) Requires: python3-pefile
Requires: python3dist(pillow) Requires: python3-pillow
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils

View File

@ -1,5 +1,5 @@
%global pypi_name portprotonqt %global pypi_name portprotonqt
%global pypi_version 0.1.1 %global pypi_version 0.1.2
%global oname PortProtonQt %global oname PortProtonQt
Name: python-%{pypi_name} Name: python-%{pypi_name}
@ -25,19 +25,19 @@ BuildRequires: git
%package -n python3-%{pypi_name} %package -n python3-%{pypi_name}
Summary: %{summary} Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}} %{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3dist(babel) Requires: python3-babel
Requires: python3dist(evdev) Requires: python3-evdev
Requires: python3dist(icoextract) Requires: python3-icoextract
Requires: python3dist(numpy) Requires: python3-numpy
Requires: python3dist(orjson) Requires: python3-orjson
Requires: python3dist(psutil) Requires: python3-psutil
Requires: python3dist(pyside6) Requires: python3-pyside6
Requires: python3dist(pyudev) Requires: python3-pyudev
Requires: python3dist(requests) Requires: python3-requests
Requires: python3dist(tqdm) Requires: python3-tqdm
Requires: python3dist(vdf) Requires: python3-vdf
Requires: python3dist(pefile) Requires: python3-pefile
Requires: python3dist(pillow) Requires: python3-pillow
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils

View File

@ -49,6 +49,16 @@
<caption>Settings</caption> <caption>Settings</caption>
<caption xml:lang="ru">Настройки</caption> <caption xml:lang="ru">Настройки</caption>
</screenshot> </screenshot>
<screenshot>
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9A%D0%BE%D0%BD%D1%82%D0%B5%D0%BA%D1%81%D1%82%D0%BD%D0%BE%D0%B5%20%D0%BC%D0%B5%D0%BD%D1%8E.png</image>
<caption>Context Menu</caption>
<caption xml:lang="ru">Контекстное меню</caption>
</screenshot>
<screenshot>
<image>https://git.linux-gaming.ru/Boria138/PortProtonQt/src/branch/main/portprotonqt/themes/standart/images/screenshots/%D0%9E%D0%B2%D0%B5%D1%80%D0%BB%D0%B5%D0%B9.png</image>
<caption>Overlay</caption>
<caption xml:lang="ru">Оверлей</caption>
</screenshot>
</screenshots> </screenshots>
<keywords> <keywords>
<keyword translate="no">wine</keyword> <keyword translate="no">wine</keyword>

8
config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
"endpoint": "https://git.linux-gaming.ru/api/v1",
"gitAuthor": "Renovate Bot <noreply@linux-gaming.ru>",
"platform": "gitea",
"onboardingConfigFileName": "renovate.json",
"autodiscover": true,
"optimizeForDisabled": true,
};

View File

@ -1573,7 +1573,7 @@
}, },
{ {
"normalized_name": "dune awakening", "normalized_name": "dune awakening",
"status": "Broken" "status": "Running"
}, },
{ {
"normalized_name": "warcraft iii reforged", "normalized_name": "warcraft iii reforged",
@ -2337,7 +2337,7 @@
}, },
{ {
"normalized_name": "punishing gray raven", "normalized_name": "punishing gray raven",
"status": "Broken" "status": "Running"
}, },
{ {
"normalized_name": "brainbread 2", "normalized_name": "brainbread 2",
@ -3951,10 +3951,6 @@
"normalized_name": "outpost infinity siege", "normalized_name": "outpost infinity siege",
"status": "Running" "status": "Running"
}, },
{
"normalized_name": "avatar frontiers of pandora",
"status": "Broken"
},
{ {
"normalized_name": "v rising", "normalized_name": "v rising",
"status": "Running" "status": "Running"
@ -4406,5 +4402,17 @@
{ {
"normalized_name": "elden ring nightreign", "normalized_name": "elden ring nightreign",
"status": "Running" "status": "Running"
},
{
"normalized_name": "steel hunters",
"status": "Running"
},
{
"normalized_name": "reverse 1999",
"status": "Running"
},
{
"normalized_name": "ragnarok origin roo",
"status": "Running"
} }
] ]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -6,11 +6,20 @@ import asyncio
import aiohttp import aiohttp
import tarfile import tarfile
# Получаем ключи и данные из переменных окружения
STEAM_KEY = os.environ.get('STEAM_KEY')
LINUX_GAMING_API_KEY = os.environ.get('LINUX_GAMING_API_KEY')
LINUX_GAMING_API_USERNAME = os.environ.get('LINUX_GAMING_API_USERNAME')
# Получаем ключ Steam из переменной окружения. # Конфигурация API
key = os.environ.get('STEAM_KEY') STEAM_BASE_URL = "https://api.steampowered.com/IStoreService/GetAppList/v1/?"
base_url = "https://api.steampowered.com/IStoreService/GetAppList/v1/?" LINUX_GAMING_BASE_URL = "https://linux-gaming.ru"
category = "games" CATEGORY_STEAM = "games"
CATEGORY_LINUX_GAMING = "ppdb"
LINUX_GAMING_HEADERS = {
"Api-Key": LINUX_GAMING_API_KEY,
"Api-Username": LINUX_GAMING_API_USERNAME
}
def normalize_name(s): def normalize_name(s):
""" """
@ -32,13 +41,11 @@ def normalize_name(s):
if s.endswith(suffix): if s.endswith(suffix):
s = s[:-len(suffix)].strip() s = s[:-len(suffix)].strip()
# Удаляем служебные слова, которые не должны влиять на сопоставление
keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"} keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"}
words = s.split() words = s.split()
filtered_words = [word for word in words if word not in keywords_to_remove] filtered_words = [word for word in words if word not in keywords_to_remove]
return " ".join(filtered_words) return " ".join(filtered_words)
def process_steam_apps(steam_apps): def process_steam_apps(steam_apps):
""" """
Для каждого приложения из Steam добавляет ключ "normalized_name", Для каждого приложения из Steam добавляет ключ "normalized_name",
@ -49,16 +56,14 @@ def process_steam_apps(steam_apps):
original = app.get("name", "") original = app.get("name", "")
if not app.get("normalized_name"): if not app.get("normalized_name"):
app["normalized_name"] = normalize_name(original) app["normalized_name"] = normalize_name(original)
# Удаляем ненужные поля
app.pop("name", None) app.pop("name", None)
app.pop("last_modified", None) app.pop("last_modified", None)
app.pop("price_change_number", None) app.pop("price_change_number", None)
return steam_apps return steam_apps
async def get_app_list(session, last_appid, endpoint): async def get_app_list(session, last_appid, endpoint):
""" """
Получает часть списка приложений из API. Получает часть списка приложений из API Steam.
Если last_appid передан, добавляет его к URL для постраничной загрузки. Если last_appid передан, добавляет его к URL для постраничной загрузки.
""" """
url = endpoint url = endpoint
@ -68,7 +73,6 @@ async def get_app_list(session, last_appid, endpoint):
response.raise_for_status() response.raise_for_status()
return await response.json() return await response.json()
async def fetch_games_json(session): async def fetch_games_json(session):
""" """
Загружает JSON с данными из AreWeAntiCheatYet и извлекает поля normalized_name и status. Загружает JSON с данными из AreWeAntiCheatYet и извлекает поля normalized_name и status.
@ -79,21 +83,46 @@ async def fetch_games_json(session):
response.raise_for_status() response.raise_for_status()
text = await response.text() text = await response.text()
data = json.loads(text) data = json.loads(text)
# Извлекаем только поля normalized_name и status
return [{"normalized_name": normalize_name(game["name"]), "status": game["status"]} for game in data] return [{"normalized_name": normalize_name(game["name"]), "status": game["status"]} for game in data]
except Exception as error: except Exception as error:
print(f"Ошибка загрузки games.json: {error}") print(f"Ошибка загрузки games.json: {error}")
return [] return []
async def get_linux_gaming_topics(session, category_slug):
"""
Получает все темы из указанной категории linux-gaming.ru.
Сохраняет только нормализованное название (normalized_title) и slug.
"""
page = 0
all_topics = []
while True:
page += 1
url = f"{LINUX_GAMING_BASE_URL}/c/{category_slug}/l/latest.json?page={page}"
try:
async with session.get(url, headers=LINUX_GAMING_HEADERS) as response:
response.raise_for_status()
data = await response.json()
topics = data.get("topic_list", {}).get("topics", [])
if not topics:
break
for topic in topics:
all_topics.append({
"normalized_title": normalize_name(topic["title"]),
"slug": topic["slug"]
})
print(f"Обработано {len(topics)} тем на странице {page}, всего: {len(all_topics)}.")
except Exception as error:
print(f"Ошибка получения тем для страницы {page}: {error}")
break
return all_topics
async def request_data(): async def request_data():
""" """
Получает данные списка приложений для категории "games" до тех пор, Получает данные из Steam, AreWeAntiCheatYet и linux-gaming.ru,
пока не закончатся результаты, обрабатывает данные для добавления обрабатывает их и сохраняет в JSON-файлы и tar.xz архивы.
нормализованных имён и записывает итоговый результат в JSON-файл.
Отдельно загружает games.json и сохраняет его в отдельный JSON-файл.
""" """
# Параметры запроса для игр. # Параметры запроса для Steam
game_param = "&include_games=true" game_param = "&include_games=true"
dlc_param = "&include_dlc=false" dlc_param = "&include_dlc=false"
software_param = "&include_software=false" software_param = "&include_software=false"
@ -101,13 +130,15 @@ async def request_data():
hardware_param = "&include_hardware=false" hardware_param = "&include_hardware=false"
endpoint = ( endpoint = (
f"{base_url}key={key}" f"{STEAM_BASE_URL}key={STEAM_KEY}"
f"{game_param}{dlc_param}{software_param}{videos_param}{hardware_param}" f"{game_param}{dlc_param}{software_param}{videos_param}{hardware_param}"
f"&max_results=50000" f"&max_results=50000"
) )
output_json = [] output_json = []
total_parsed = 0 total_parsed = 0
linux_gaming_topics = []
anticheat_games = []
try: try:
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
@ -117,58 +148,62 @@ async def request_data():
while have_more_results: while have_more_results:
app_list = await get_app_list(session, last_appid_val, endpoint) app_list = await get_app_list(session, last_appid_val, endpoint)
apps = app_list['response']['apps'] apps = app_list['response']['apps']
# Обрабатываем приложения для добавления нормализованных имён
apps = process_steam_apps(apps) apps = process_steam_apps(apps)
output_json.extend(apps) output_json.extend(apps)
total_parsed += len(apps) total_parsed += len(apps)
have_more_results = app_list['response'].get('have_more_results', False) have_more_results = app_list['response'].get('have_more_results', False)
last_appid_val = app_list['response'].get('last_appid') last_appid_val = app_list['response'].get('last_appid')
print(f"Обработано {len(apps)} игр Steam, всего: {total_parsed}.")
print(f"Обработано {len(apps)} игр, всего: {total_parsed}.") # Загружаем данные AreWeAntiCheatYet
# Загружаем и сохраняем games.json отдельно
anticheat_games = await fetch_games_json(session) anticheat_games = await fetch_games_json(session)
# Загружаем данные linux-gaming.ru
if LINUX_GAMING_API_KEY and LINUX_GAMING_API_USERNAME:
linux_gaming_topics = await get_linux_gaming_topics(session, CATEGORY_LINUX_GAMING)
else:
print("Предупреждение: LINUX_GAMING_API_KEY или LINUX_GAMING_API_USERNAME не установлены.")
except Exception as error: except Exception as error:
print(f"Ошибка получения данных для {category}: {error}") print(f"Ошибка получения данных: {error}")
return False return False
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
data_dir = os.path.join(repo_root, "data") data_dir = os.path.join(repo_root, "data")
os.makedirs(data_dir, exist_ok=True) os.makedirs(data_dir, exist_ok=True)
# Путь к JSON-файлам для Steam # Сохранение данных Steam
output_json_full = os.path.join(data_dir, f"{category}_appid.json") output_json_full = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.json")
output_json_min = os.path.join(data_dir, f"{category}_appid_min.json") output_json_min = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid_min.json")
# Записываем полные данные Steam с отступами
with open(output_json_full, "w", encoding="utf-8") as f: with open(output_json_full, "w", encoding="utf-8") as f:
json.dump(output_json, f, ensure_ascii=False, indent=2) json.dump(output_json, f, ensure_ascii=False, indent=2)
# Записываем минимизированные данные Steam
with open(output_json_min, "w", encoding="utf-8") as f: with open(output_json_min, "w", encoding="utf-8") as f:
json.dump(output_json, f, ensure_ascii=False, separators=(',',':')) json.dump(output_json, f, ensure_ascii=False, separators=(',',':'))
# Путь к JSON-файлам для AreWeAntiCheatYet # Сохранение данных AreWeAntiCheatYet
anticheat_json_full = os.path.join(data_dir, "anticheat_games.json") anticheat_json_full = os.path.join(data_dir, "anticheat_games.json")
anticheat_json_min = os.path.join(data_dir, "anticheat_games_min.json") anticheat_json_min = os.path.join(data_dir, "anticheat_games_min.json")
# Записываем полные данные AreWeAntiCheatYet с отступами
with open(anticheat_json_full, "w", encoding="utf-8") as f: with open(anticheat_json_full, "w", encoding="utf-8") as f:
json.dump(anticheat_games, f, ensure_ascii=False, indent=2) json.dump(anticheat_games, f, ensure_ascii=False, indent=2)
# Записываем минимизированные данные AreWeAntiCheatYet
with open(anticheat_json_min, "w", encoding="utf-8") as f: with open(anticheat_json_min, "w", encoding="utf-8") as f:
json.dump(anticheat_games, f, ensure_ascii=False, separators=(',',':')) json.dump(anticheat_games, f, ensure_ascii=False, separators=(',',':'))
# Упаковка только минифицированных JSON в tar.xz архивы с максимальным сжатием # Сохранение данных linux-gaming.ru
linux_gaming_json_full = os.path.join(data_dir, "linux_gaming_topics.json")
linux_gaming_json_min = os.path.join(data_dir, "linux_gaming_topics_min.json")
if linux_gaming_topics:
with open(linux_gaming_json_full, "w", encoding="utf-8") as f:
json.dump(linux_gaming_topics, f, ensure_ascii=False, indent=2)
with open(linux_gaming_json_min, "w", encoding="utf-8") as f:
json.dump(linux_gaming_topics, f, ensure_ascii=False, separators=(',',':'))
# Упаковка минифицированных JSON в tar.xz архивы
# Архив для Steam # Архив для Steam
steam_archive_path = os.path.join(data_dir, f"{category}_appid.tar.xz") steam_archive_path = os.path.join(data_dir, f"{CATEGORY_STEAM}_appid.tar.xz")
try: try:
with tarfile.open(steam_archive_path, "w:xz", preset=9) as tar: with tarfile.open(steam_archive_path, "w:xz", preset=9) as tar:
tar.add(output_json_min, arcname=os.path.basename(output_json_min)) tar.add(output_json_min, arcname=os.path.basename(output_json_min))
print(f"Упаковано минифицированное JSON Steam в архив: {steam_archive_path}") print(f"Упаковано минифицированное JSON Steam в архив: {steam_archive_path}")
# Удаляем исходный минифицированный файл после упаковки
os.remove(output_json_min) os.remove(output_json_min)
except Exception as e: except Exception as e:
print(f"Ошибка при упаковке архива Steam: {e}") print(f"Ошибка при упаковке архива Steam: {e}")
@ -180,20 +215,29 @@ async def request_data():
with tarfile.open(anticheat_archive_path, "w:xz", preset=9) as tar: with tarfile.open(anticheat_archive_path, "w:xz", preset=9) as tar:
tar.add(anticheat_json_min, arcname=os.path.basename(anticheat_json_min)) tar.add(anticheat_json_min, arcname=os.path.basename(anticheat_json_min))
print(f"Упаковано минифицированное JSON AreWeAntiCheatYet в архив: {anticheat_archive_path}") print(f"Упаковано минифицированное JSON AreWeAntiCheatYet в архив: {anticheat_archive_path}")
# Удаляем исходный минифицированный файл после упаковки
os.remove(anticheat_json_min) os.remove(anticheat_json_min)
except Exception as e: except Exception as e:
print(f"Ошибка при упаковке архива AreWeAntiCheatYet: {e}") print(f"Ошибка при упаковке архива AreWeAntiCheatYet: {e}")
return False return False
return True # Архив для linux-gaming.ru
if linux_gaming_topics:
linux_gaming_archive_path = os.path.join(data_dir, "linux_gaming_topics.tar.xz")
try:
with tarfile.open(linux_gaming_archive_path, "w:xz", preset=9) as tar:
tar.add(linux_gaming_json_min, arcname=os.path.basename(linux_gaming_json_min))
print(f"Упаковано минифицированное JSON linux-gaming.ru в архив: {linux_gaming_archive_path}")
os.remove(linux_gaming_json_min)
except Exception as e:
print(f"Ошибка при упаковке архива linux-gaming.ru: {e}")
return False
return True
async def run(): async def run():
success = await request_data() success = await request_data()
if not success: if not success:
exit(1) exit(1)
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(run()) asyncio.run(run())

View File

@ -20,9 +20,9 @@ Current translation status:
| Locale | Progress | Translated | | Locale | Progress | Translated |
| :----- | -------: | ---------: | | :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 162 | | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 161 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 162 | | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 161 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 of 162 | | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 of 161 |
--- ---

View File

@ -20,9 +20,9 @@
| Локаль | Прогресс | Переведено | | Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: | | :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 162 | | [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 161 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 162 | | [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 161 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 162 из 162 | | [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 161 из 161 |
--- ---

View File

@ -1,4 +1,6 @@
import sys import sys
import os
import subprocess
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
@ -12,7 +14,7 @@ logger = get_logger(__name__)
__app_id__ = "ru.linux_gaming.PortProtonQt" __app_id__ = "ru.linux_gaming.PortProtonQt"
__app_name__ = "PortProtonQt" __app_name__ = "PortProtonQt"
__app_version__ = "0.1.1" __app_version__ = "0.1.2"
def main(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
@ -29,14 +31,19 @@ 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() args = parse_args()
window = MainWindow() window = MainWindow()
# Обработка флага --fullscreen if args.session:
gamescope_cmd = os.getenv("GAMESCOPE_CMD", "gamescope -f --xwayland-count 2")
cmd = f"{gamescope_cmd} -- portprotonqt"
logger.info(f"Executing: {cmd}")
subprocess.Popen(cmd, shell=True)
sys.exit(0)
if args.fullscreen: if args.fullscreen:
logger.info("Запуск в полноэкранном режиме по флагу --fullscreen") logger.info("Launching in fullscreen mode due to --fullscreen flag")
save_fullscreen_config(True) save_fullscreen_config(True)
window.showFullScreen() window.showFullScreen()
@ -47,13 +54,29 @@ def main():
def recreate_tray(): def recreate_tray():
nonlocal tray nonlocal tray
tray.hide_tray() if tray:
logger.debug("Recreating system tray")
tray.cleanup()
tray = None
current_theme = read_theme_from_config() current_theme = read_theme_from_config()
tray = SystemTray(app, current_theme) tray = SystemTray(app, current_theme)
# Ensure window is not None before connecting signals
if window:
tray.show_action.triggered.connect(window.show) tray.show_action.triggered.connect(window.show)
tray.hide_action.triggered.connect(window.hide) tray.hide_action.triggered.connect(window.hide)
def cleanup_on_exit():
nonlocal tray, window
app.aboutToQuit.disconnect()
if tray:
tray.cleanup()
tray = None
if window:
window.close()
app.quit()
window.settings_saved.connect(recreate_tray) window.settings_saved.connect(recreate_tray)
app.aboutToQuit.connect(cleanup_on_exit)
window.show() window.show()

View File

@ -13,4 +13,9 @@ def parse_args():
action="store_true", action="store_true",
help="Запустить приложение в полноэкранном режиме и сохранить эту настройку" help="Запустить приложение в полноэкранном режиме и сохранить эту настройку"
) )
parser.add_argument(
"--session",
action="store_true",
help="Запустить приложение с использованием gamescope"
)
return parser.parse_args() return parser.parse_args()

View File

@ -1,5 +1,5 @@
import numpy as np import numpy as np
from PySide6.QtWidgets import QLabel, QPushButton, QWidget, QLayout, QStyleOption, QLayoutItem from PySide6.QtWidgets import QLabel, QPushButton, QWidget, QLayout, QLayoutItem
from PySide6.QtCore import Qt, Signal, QRect, QPoint, QSize from PySide6.QtCore import Qt, Signal, QRect, QPoint, QSize
from PySide6.QtGui import QFont, QFontMetrics, QPainter from PySide6.QtGui import QFont, QFontMetrics, QPainter
@ -133,18 +133,7 @@ class FlowLayout(QLayout):
class ClickableLabel(QLabel): class ClickableLabel(QLabel):
clicked = Signal() clicked = Signal()
def __init__(self, *args, icon=None, icon_size=16, icon_space=5, change_cursor=True, **kwargs): def __init__(self, *args, icon=None, icon_size=16, icon_space=5, change_cursor=True, font_scale_factor=0.06, **kwargs):
"""
Поддерживаются вызовы:
- ClickableLabel("текст", parent=...) первый аргумент строка,
- ClickableLabel(parent, text="...") если первым аргументом передается родитель.
Аргументы:
icon: QIcon или None иконка, которая будет отрисована вместе с текстом.
icon_size: int размер иконки (ширина и высота).
icon_space: int отступ между иконкой и текстом.
change_cursor: bool изменять ли курсор на PointingHandCursor при наведении (по умолчанию True).
"""
if args and isinstance(args[0], str): if args and isinstance(args[0], str):
text = args[0] text = args[0]
parent = kwargs.get("parent", None) parent = kwargs.get("parent", None)
@ -162,20 +151,38 @@ class ClickableLabel(QLabel):
self._icon = icon self._icon = icon
self._icon_size = icon_size self._icon_size = icon_size
self._icon_space = icon_space self._icon_space = icon_space
self._font_scale_factor = font_scale_factor
self._card_width = 250 # Значение по умолчанию
if change_cursor: if change_cursor:
self.setCursor(Qt.CursorShape.PointingHandCursor) self.setCursor(Qt.CursorShape.PointingHandCursor)
self.updateFontSize()
def setIcon(self, icon): def setIcon(self, icon):
"""Устанавливает иконку и перерисовывает виджет."""
self._icon = icon self._icon = icon
self.update() self.update()
def icon(self): def icon(self):
"""Возвращает текущую иконку."""
return self._icon return self._icon
def setIconSize(self, icon_size: int, icon_space: int):
self._icon_size = icon_size
self._icon_space = icon_space
self.update()
def setCardWidth(self, card_width: int):
"""Обновляет ширину карточки и пересчитывает размер шрифта."""
self._card_width = card_width
self.updateFontSize()
def updateFontSize(self):
"""Обновляет размер шрифта на основе card_width и font_scale_factor."""
font = self.font()
font_size = int(self._card_width * self._font_scale_factor)
font.setPointSize(max(8, font_size)) # Минимальный размер шрифта 8
self.setFont(font)
self.update()
def paintEvent(self, event): def paintEvent(self, event):
"""Переопределяем отрисовку: рисуем иконку и текст в одном лейбле."""
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.setRenderHint(QPainter.RenderHint.Antialiasing)
@ -190,7 +197,6 @@ class ClickableLabel(QLabel):
text = self.text() text = self.text()
if self._icon: if self._icon:
# Получаем QPixmap нужного размера
pixmap = self._icon.pixmap(icon_size, icon_size) pixmap = self._icon.pixmap(icon_size, icon_size)
icon_rect = QRect(0, 0, icon_size, icon_size) icon_rect = QRect(0, 0, icon_size, icon_size)
icon_rect.moveTop(rect.top() + (rect.height() - icon_size) // 2) icon_rect.moveTop(rect.top() + (rect.height() - icon_size) // 2)
@ -214,13 +220,11 @@ class ClickableLabel(QLabel):
if pixmap: if pixmap:
icon_rect.moveLeft(x) icon_rect.moveLeft(x)
text_rect = QRect(x + icon_size + spacing, y, text_width, text_height) text_rect = QRect(x + icon_size + spacing, y, text_width, text_height)
painter.drawPixmap(icon_rect, pixmap)
else: else:
# Устанавливаем text_rect для меток без иконки (например, favoriteLabel)
text_rect = QRect(x, y, text_width, text_height) text_rect = QRect(x, y, text_width, text_height)
option = QStyleOption()
option.initFrom(self)
if pixmap:
painter.drawPixmap(icon_rect, pixmap)
self.style().drawItemText( self.style().drawItemText(
painter, painter,
text_rect, text_rect,

View File

@ -98,7 +98,7 @@ class AddGameDialog(QDialog):
# Game name # Game name
self.nameEdit = QLineEdit(self) self.nameEdit = QLineEdit(self)
self.nameEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE + " QLineEdit { color: #ffffff; font-size: 14px; }") self.nameEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
if game_name: if game_name:
self.nameEdit.setText(game_name) self.nameEdit.setText(game_name)
name_label = QLabel(_("Game Name:")) name_label = QLabel(_("Game Name:"))
@ -107,7 +107,7 @@ class AddGameDialog(QDialog):
# Exe path # Exe path
self.exeEdit = QLineEdit(self) self.exeEdit = QLineEdit(self)
self.exeEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE + " QLineEdit { color: #ffffff; font-size: 14px; }") self.exeEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
if exe_path: if exe_path:
self.exeEdit.setText(exe_path) self.exeEdit.setText(exe_path)
exeBrowseButton = QPushButton(_("Browse..."), self) exeBrowseButton = QPushButton(_("Browse..."), self)
@ -123,7 +123,7 @@ class AddGameDialog(QDialog):
# Cover path # Cover path
self.coverEdit = QLineEdit(self) self.coverEdit = QLineEdit(self)
self.coverEdit.setStyleSheet(self.theme.SEARCH_EDIT_STYLE + " QLineEdit { color: #ffffff; font-size: 14px; }") self.coverEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
if cover_path: if cover_path:
self.coverEdit.setText(cover_path) self.coverEdit.setText(cover_path)
coverBrowseButton = QPushButton(_("Browse..."), self) coverBrowseButton = QPushButton(_("Browse..."), self)

View File

@ -25,6 +25,8 @@ class GameCard(QFrame):
addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path
removeFromSteamRequested = Signal(str, str) # name, exec_line removeFromSteamRequested = Signal(str, str) # name, exec_line
openGameFolderRequested = Signal(str, str) # name, exec_line openGameFolderRequested = Signal(str, str) # name, exec_line
hoverChanged = Signal(str, bool)
focusChanged = Signal(str, bool)
def __init__(self, name, description, cover_path, appid, controller_support, exec_line, def __init__(self, name, description, cover_path, appid, controller_support, exec_line,
last_launch, formatted_playtime, protondb_tier, anticheat_status, last_launch_ts, playtime_seconds, game_source, last_launch, formatted_playtime, protondb_tier, anticheat_status, last_launch_ts, playtime_seconds, game_source,
@ -43,6 +45,7 @@ class GameCard(QFrame):
self.game_source = game_source self.game_source = game_source
self.last_launch_ts = last_launch_ts self.last_launch_ts = last_launch_ts
self.playtime_seconds = playtime_seconds self.playtime_seconds = playtime_seconds
self.card_width = card_width
self.select_callback = select_callback self.select_callback = select_callback
self.context_menu_manager = context_menu_manager self.context_menu_manager = context_menu_manager
@ -54,6 +57,10 @@ class GameCard(QFrame):
self.display_filter = read_display_filter() self.display_filter = read_display_filter()
self.current_theme_name = read_theme_from_config() self.current_theme_name = read_theme_from_config()
self.steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
self.egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
self.portproton_visible = (str(game_source).lower() == "portproton" and self.display_filter in ("all", "favorites"))
# Дополнительное пространство для анимации # Дополнительное пространство для анимации
extra_margin = 20 extra_margin = 20
self.setFixedSize(card_width + extra_margin, int(card_width * 1.6) + extra_margin) self.setFixedSize(card_width + extra_margin, int(card_width * 1.6) + extra_margin)
@ -61,14 +68,14 @@ class GameCard(QFrame):
self.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE) self.setStyleSheet(self.theme.GAME_CARD_WINDOW_STYLE)
# Параметры анимации обводки # Параметры анимации обводки
self._borderWidth = 2 self._borderWidth = self.theme.GAME_CARD_ANIMATION["default_border_width"]
self._gradientAngle = 0.0 self._gradientAngle = self.theme.GAME_CARD_ANIMATION["gradient_start_angle"]
self._hovered = False self._hovered = False
self._focused = False self._focused = False
# Анимации # Анимации
self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth")) self.thickness_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
self.thickness_anim.setDuration(300) self.thickness_anim.setDuration(self.theme.GAME_CARD_ANIMATION["thickness_anim_duration"])
self.gradient_anim = None self.gradient_anim = None
self.pulse_anim = None self.pulse_anim = None
@ -121,9 +128,11 @@ class GameCard(QFrame):
self.update_favorite_icon() self.update_favorite_icon()
self.favoriteLabel.raise_() self.favoriteLabel.raise_()
steam_visible = (str(game_source).lower() == "steam" and self.display_filter in ("all", "favorites")) # Определяем общие параметры для бейджей
egs_visible = (str(game_source).lower() == "epic" and self.display_filter in ("all", "favorites")) badge_width = int(card_width * 2/3)
portproton_visible = (str(game_source).lower() == "portproton" and self.display_filter in ("all", "favorites")) icon_size = int(card_width * 0.06) # 6% от ширины карточки
icon_space = int(card_width * 0.012) # 1.2% от ширины карточки
font_scale_factor = 0.06 # Шрифт будет 6% от card_width
# ProtonDB бейдж # ProtonDB бейдж
tier_text = self.getProtonDBText(protondb_tier) tier_text = self.getProtonDBText(protondb_tier)
@ -134,17 +143,17 @@ class GameCard(QFrame):
tier_text, tier_text,
icon=icon, icon=icon,
parent=coverWidget, parent=coverWidget,
icon_size=16, icon_size=icon_size,
icon_space=3, icon_space=icon_space,
font_scale_factor=font_scale_factor
) )
self.protondbLabel.setStyleSheet(self.theme.get_protondb_badge_style(protondb_tier)) self.protondbLabel.setStyleSheet(self.theme.get_protondb_badge_style(protondb_tier))
self.protondbLabel.setFixedWidth(int(card_width * 2/3)) self.protondbLabel.setFixedWidth(badge_width)
protondb_visible = True self.protondbLabel.setCardWidth(card_width)
else: else:
self.protondbLabel = ClickableLabel("", parent=coverWidget, icon_size=16, icon_space=3) self.protondbLabel = ClickableLabel("", parent=coverWidget, icon_size=icon_size, icon_space=icon_space)
self.protondbLabel.setFixedWidth(int(card_width * 2/3)) self.protondbLabel.setFixedWidth(badge_width)
self.protondbLabel.setVisible(False) self.protondbLabel.setVisible(False)
protondb_visible = False
# Steam бейдж # Steam бейдж
steam_icon = self.theme_manager.get_icon("steam") steam_icon = self.theme_manager.get_icon("steam")
@ -152,12 +161,14 @@ class GameCard(QFrame):
"Steam", "Steam",
icon=steam_icon, icon=steam_icon,
parent=coverWidget, parent=coverWidget,
icon_size=16, icon_size=icon_size,
icon_space=5, icon_space=icon_space,
font_scale_factor=font_scale_factor
) )
self.steamLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE) self.steamLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
self.steamLabel.setFixedWidth(int(card_width * 2/3)) self.steamLabel.setFixedWidth(badge_width)
self.steamLabel.setVisible(steam_visible) self.steamLabel.setCardWidth(card_width)
self.steamLabel.setVisible(self.steam_visible)
# Epic Games Store бейдж # Epic Games Store бейдж
egs_icon = self.theme_manager.get_icon("steam") egs_icon = self.theme_manager.get_icon("steam")
@ -165,27 +176,31 @@ class GameCard(QFrame):
"Epic Games", "Epic Games",
icon=egs_icon, icon=egs_icon,
parent=coverWidget, parent=coverWidget,
icon_size=16, icon_size=icon_size,
icon_space=5, icon_space=icon_space,
font_scale_factor=font_scale_factor,
change_cursor=False change_cursor=False
) )
self.egsLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE) self.egsLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
self.egsLabel.setFixedWidth(int(card_width * 2/3)) self.egsLabel.setFixedWidth(badge_width)
self.egsLabel.setVisible(egs_visible) self.egsLabel.setCardWidth(card_width)
self.egsLabel.setVisible(self.egs_visible)
# PortProton badge # PortProton бейдж
portproton_icon = self.theme_manager.get_icon("ppqt-tray") portproton_icon = self.theme_manager.get_icon("ppqt-tray")
self.portprotonLabel = ClickableLabel( self.portprotonLabel = ClickableLabel(
"PortProton", "PortProton",
icon=portproton_icon, icon=portproton_icon,
parent=coverWidget, parent=coverWidget,
icon_size=16, icon_size=icon_size,
icon_space=5, icon_space=icon_space,
font_scale_factor=font_scale_factor,
change_cursor=False change_cursor=False
) )
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE) self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
self.portprotonLabel.setFixedWidth(int(card_width * 2/3)) self.portprotonLabel.setFixedWidth(badge_width)
self.portprotonLabel.setVisible(portproton_visible) self.portprotonLabel.setCardWidth(card_width)
self.portprotonLabel.setVisible(self.portproton_visible)
# WeAntiCheatYet бейдж # WeAntiCheatYet бейдж
anticheat_text = self.getAntiCheatText(anticheat_status) anticheat_text = self.getAntiCheatText(anticheat_status)
@ -196,53 +211,20 @@ class GameCard(QFrame):
anticheat_text, anticheat_text,
icon=icon, icon=icon,
parent=coverWidget, parent=coverWidget,
icon_size=16, icon_size=icon_size,
icon_space=3, icon_space=icon_space,
font_scale_factor=font_scale_factor
) )
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(badge_width)
anticheat_visible = True self.anticheatLabel.setCardWidth(card_width)
else: else:
self.anticheatLabel = ClickableLabel("", parent=coverWidget, icon_size=16, icon_space=3) self.anticheatLabel = ClickableLabel("", parent=coverWidget, icon_size=icon_size, icon_space=icon_space)
self.anticheatLabel.setFixedWidth(int(card_width * 2/3)) self.anticheatLabel.setFixedWidth(badge_width)
self.anticheatLabel.setVisible(False) self.anticheatLabel.setVisible(False)
anticheat_visible = False
# Расположение бейджей # Расположение бейджей
right_margin = 8 self._position_badges(card_width)
badge_spacing = 5
top_y = 10
badge_y_positions = []
badge_width = int(card_width * 2/3)
if steam_visible:
steam_x = card_width - badge_width - right_margin
self.steamLabel.move(steam_x, top_y)
badge_y_positions.append(top_y + self.steamLabel.height())
if egs_visible:
egs_x = card_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 portproton_visible:
portproton_x = card_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 protondb_visible:
protondb_x = card_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 anticheat_visible:
anticheat_x = card_width - badge_width - right_margin
anticheat_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
self.anticheatLabel.move(anticheat_x, anticheat_y)
self.anticheatLabel.raise_()
self.protondbLabel.raise_()
self.portprotonLabel.raise_()
self.egsLabel.raise_()
self.steamLabel.raise_()
self.protondbLabel.clicked.connect(self.open_protondb_report) self.protondbLabel.clicked.connect(self.open_protondb_report)
self.steamLabel.clicked.connect(self.open_steam_page) self.steamLabel.clicked.connect(self.open_steam_page)
self.anticheatLabel.clicked.connect(self.open_weanticheatyet_page) self.anticheatLabel.clicked.connect(self.open_weanticheatyet_page)
@ -255,8 +237,79 @@ class GameCard(QFrame):
nameLabel.setStyleSheet(self.theme.GAME_CARD_NAME_LABEL_STYLE) nameLabel.setStyleSheet(self.theme.GAME_CARD_NAME_LABEL_STYLE)
layout.addWidget(nameLabel) layout.addWidget(nameLabel)
def _position_badges(self, card_width):
"""Позиционирует бейджи на основе ширины карточки."""
right_margin = 8
badge_spacing = int(card_width * 0.02) # 2% от ширины карточки
top_y = 10
badge_y_positions = []
badge_width = int(card_width * 2/3)
badges = [
(self.steam_visible, self.steamLabel),
(self.egs_visible, self.egsLabel),
(self.portproton_visible, self.portprotonLabel),
(bool(self.getProtonDBText(self.protondb_tier)), self.protondbLabel),
(bool(self.getAntiCheatText(self.anticheat_status)), self.anticheatLabel),
]
for is_visible, badge in badges:
if is_visible:
badge_x = card_width - badge_width - right_margin
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
badge.move(badge_x, badge_y)
badge_y_positions.append(badge_y + badge.height())
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
self.anticheatLabel.raise_()
self.protondbLabel.raise_()
self.portprotonLabel.raise_()
self.egsLabel.raise_()
self.steamLabel.raise_()
def update_card_size(self, new_width: int):
"""Обновляет размер карточки, обложки и бейджей."""
self.card_width = new_width
extra_margin = 20
self.setFixedSize(new_width + extra_margin, int(new_width * 1.6) + extra_margin)
if self.coverLabel is None:
return
coverWidget = self.coverLabel.parentWidget()
if coverWidget is None:
return
coverWidget.setFixedSize(new_width, int(new_width * 1.2))
self.coverLabel.setFixedSize(new_width, int(new_width * 1.2))
label_ref = weakref.ref(self.coverLabel)
def on_cover_loaded(pixmap):
label = label_ref()
if label:
scaled_pixmap = pixmap.scaled(new_width, int(new_width * 1.2), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
rounded_pixmap = round_corners(scaled_pixmap, 15)
label.setPixmap(rounded_pixmap)
load_pixmap_async(self.cover_path or "", new_width, int(new_width * 1.2), on_cover_loaded)
# Обновляем размеры и шрифты бейджей
badge_width = int(new_width * 2/3)
icon_size = int(new_width * 0.06)
icon_space = int(new_width * 0.012)
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
if label is not None:
label.setFixedWidth(badge_width)
label.setIconSize(icon_size, icon_space)
label.setCardWidth(new_width) # Пересчитываем размер шрифта
# Перепозиционируем бейджи
self._position_badges(new_width)
self.update()
def update_badge_visibility(self, display_filter: str): def update_badge_visibility(self, display_filter: str):
"""Update badge visibility based on the provided display_filter.""" """Обновляет видимость бейджей на основе display_filter."""
self.display_filter = display_filter self.display_filter = display_filter
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"))
@ -271,35 +324,8 @@ class GameCard(QFrame):
self.protondbLabel.setVisible(protondb_visible) self.protondbLabel.setVisible(protondb_visible)
self.anticheatLabel.setVisible(anticheat_visible) self.anticheatLabel.setVisible(anticheat_visible)
# Подготавливаем список всех бейджей с их текущей видимостью # Перепозиционируем бейджи
badges = [ self._position_badges(self.card_width)
(self.steam_visible, self.steamLabel),
(self.egs_visible, self.egsLabel),
(self.portproton_visible, self.portprotonLabel),
(protondb_visible, self.protondbLabel),
(anticheat_visible, self.anticheatLabel),
]
# Пересчитываем позиции бейджей
right_margin = 8
badge_spacing = 5
top_y = 10
badge_y_positions = []
badge_width = int(self.coverLabel.width() * 2/3)
for is_visible, badge in badges:
if is_visible:
badge_x = self.coverLabel.width() - badge_width - right_margin
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
badge.move(badge_x, badge_y)
badge_y_positions.append(badge_y + badge.height())
# Поднимаем бейджи в правильном порядке (от нижнего к верхнему)
self.anticheatLabel.raise_()
self.protondbLabel.raise_()
self.portprotonLabel.raise_()
self.egsLabel.raise_()
self.steamLabel.raise_()
def _show_context_menu(self, pos): def _show_context_menu(self, pos):
"""Delegate context menu display to ContextMenuManager.""" """Delegate context menu display to ContextMenuManager."""
@ -322,10 +348,16 @@ class GameCard(QFrame):
@staticmethod @staticmethod
def getAntiCheatIconFilename(status: str) -> str: def getAntiCheatIconFilename(status: str) -> str:
status = status.lower() status = status.lower()
if status in ("supported", "running"): if status in ("supported"):
return "platinum-gold" return "ac_supported"
elif status in ("denied", "planned", "broken"): elif status in ("running"):
return "broken" return "ac_running"
elif status in ("planned"):
return "ac_planned"
elif status in ("denied"):
return "ac_denied"
elif status in ("broken"):
return "ac_broken"
return "" return ""
@staticmethod @staticmethod
@ -417,10 +449,8 @@ class GameCard(QFrame):
if self._hovered or self._focused: if self._hovered or self._focused:
center = self.rect().center() center = self.rect().center()
gradient = QConicalGradient(center, self._gradientAngle) gradient = QConicalGradient(center, self._gradientAngle)
gradient.setColorAt(0, QColor("#00fff5")) for stop in self.theme.GAME_CARD_ANIMATION["gradient_colors"]:
gradient.setColorAt(0.33, QColor("#FF5733")) gradient.setColorAt(stop["position"], QColor(stop["color"]))
gradient.setColorAt(0.66, QColor("#9B59B6"))
gradient.setColorAt(1, QColor("#00fff5"))
pen.setBrush(QBrush(gradient)) pen.setBrush(QBrush(gradient))
else: else:
pen.setColor(QColor(0, 0, 0, 0)) pen.setColor(QColor(0, 0, 0, 0))
@ -437,22 +467,25 @@ class GameCard(QFrame):
if self.pulse_anim: if self.pulse_anim:
self.pulse_anim.stop() self.pulse_anim.stop()
self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth")) self.pulse_anim = QPropertyAnimation(self, QByteArray(b"borderWidth"))
self.pulse_anim.setDuration(800) self.pulse_anim.setDuration(self.theme.GAME_CARD_ANIMATION["pulse_anim_duration"])
self.pulse_anim.setLoopCount(0) self.pulse_anim.setLoopCount(0)
self.pulse_anim.setKeyValueAt(0, 8) self.pulse_anim.setKeyValueAt(0, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.setKeyValueAt(0.5, 10) self.pulse_anim.setKeyValueAt(0.5, self.theme.GAME_CARD_ANIMATION["pulse_max_border_width"])
self.pulse_anim.setKeyValueAt(1, 8) self.pulse_anim.setKeyValueAt(1, self.theme.GAME_CARD_ANIMATION["pulse_min_border_width"])
self.pulse_anim.start() self.pulse_anim.start()
def enterEvent(self, event): def enterEvent(self, event):
self._hovered = True self._hovered = True
self.hoverChanged.emit(self.name, True)
self.setFocus(Qt.FocusReason.MouseFocusReason)
self.thickness_anim.stop() self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False self._isPulseAnimationConnected = False
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutBack)) self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
self.thickness_anim.setStartValue(self._borderWidth) self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(8) self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["hover_border_width"])
self.thickness_anim.finished.connect(self.startPulseAnimation) self.thickness_anim.finished.connect(self.startPulseAnimation)
self._isPulseAnimationConnected = True self._isPulseAnimationConnected = True
self.thickness_anim.start() self.thickness_anim.start()
@ -460,9 +493,9 @@ class GameCard(QFrame):
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle")) self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
self.gradient_anim.setDuration(3000) self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
self.gradient_anim.setStartValue(360) self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
self.gradient_anim.setEndValue(0) self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1) self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start() self.gradient_anim.start()
@ -470,33 +503,37 @@ class GameCard(QFrame):
def leaveEvent(self, event): def leaveEvent(self, event):
self._hovered = False self._hovered = False
if not self._focused: # Сохраняем анимацию, если есть фокус self.hoverChanged.emit(self.name, False)
if not self._focused:
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = None self.gradient_anim = None
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
if self.pulse_anim: if self.pulse_anim:
self.pulse_anim.stop() self.pulse_anim.stop()
self.pulse_anim = None self.pulse_anim = None
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.InBack)) if self.thickness_anim:
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(2)
self.thickness_anim.start()
super().leaveEvent(event)
def focusInEvent(self, event):
self._focused = True
self.thickness_anim.stop() self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False self._isPulseAnimationConnected = False
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.OutBack)) self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
self.thickness_anim.setStartValue(self._borderWidth) self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(12) self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
self.thickness_anim.start()
super().leaveEvent(event)
def focusInEvent(self, event):
if not self._hovered:
self._focused = True
self.focusChanged.emit(self.name, True)
self.thickness_anim.stop()
if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve"]]))
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["focus_border_width"])
self.thickness_anim.finished.connect(self.startPulseAnimation) self.thickness_anim.finished.connect(self.startPulseAnimation)
self._isPulseAnimationConnected = True self._isPulseAnimationConnected = True
self.thickness_anim.start() self.thickness_anim.start()
@ -504,9 +541,9 @@ class GameCard(QFrame):
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle")) self.gradient_anim = QPropertyAnimation(self, QByteArray(b"gradientAngle"))
self.gradient_anim.setDuration(3000) self.gradient_anim.setDuration(self.theme.GAME_CARD_ANIMATION["gradient_anim_duration"])
self.gradient_anim.setStartValue(360) self.gradient_anim.setStartValue(self.theme.GAME_CARD_ANIMATION["gradient_start_angle"])
self.gradient_anim.setEndValue(0) self.gradient_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["gradient_end_angle"])
self.gradient_anim.setLoopCount(-1) self.gradient_anim.setLoopCount(-1)
self.gradient_anim.start() self.gradient_anim.start()
@ -514,22 +551,23 @@ class GameCard(QFrame):
def focusOutEvent(self, event): def focusOutEvent(self, event):
self._focused = False self._focused = False
if not self._hovered: # Сохраняем анимацию, если есть наведение self.focusChanged.emit(self.name, False)
if not self._hovered:
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = None self.gradient_anim = None
if self.pulse_anim:
self.pulse_anim.stop()
self.pulse_anim = None
if self.thickness_anim:
self.thickness_anim.stop() self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False self._isPulseAnimationConnected = False
if self.pulse_anim: self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION["thickness_easing_curve_out"]]))
self.pulse_anim.stop()
self.pulse_anim = None
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.InBack))
self.thickness_anim.setStartValue(self._borderWidth) self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(2) self.thickness_anim.setEndValue(self.theme.GAME_CARD_ANIMATION["default_border_width"])
self.thickness_anim.start() self.thickness_anim.start()
super().focusOutEvent(event) super().focusOutEvent(event)
def mousePressEvent(self, event): def mousePressEvent(self, event):

View File

@ -27,6 +27,8 @@ class MainWindowProtocol(Protocol):
... ...
def openSystemOverlay(self) -> None: def openSystemOverlay(self) -> None:
... ...
def on_slider_released(self) -> None:
...
stackedWidget: QStackedWidget stackedWidget: QStackedWidget
tabButtons: dict[int, QWidget] tabButtons: dict[int, QWidget]
gamesListWidget: QWidget gamesListWidget: QWidget
@ -34,18 +36,20 @@ 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 Xbox and Playstation 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/hid/hid-playstation.c
# https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c # https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c
BUTTONS = { BUTTONS = {
'confirm': {ecodes.BTN_A, ecodes.BTN_SOUTH}, # A / Cross 'confirm': {ecodes.BTN_SOUTH}, # A (Xbox) / Cross (PS)
'back': {ecodes.BTN_B, ecodes.BTN_EAST}, # B / Circle 'back': {ecodes.BTN_EAST}, # B (Xbox) / Circle (PS)
'add_game': {ecodes.BTN_Y, ecodes.BTN_NORTH}, # Y / Triangle 'add_game': {ecodes.BTN_NORTH}, # X (Xbox) / Triangle (PS)
'prev_tab': {ecodes.BTN_TL}, # LB / L1 'prev_tab': {ecodes.BTN_TL}, # LB (Xbox) / L1 (PS)
'next_tab': {ecodes.BTN_TR}, # RB / R1 'next_tab': {ecodes.BTN_TR}, # RB (Xbox) / R1 (PS)
'context_menu': {ecodes.BTN_START}, # Start / Options 'context_menu': {ecodes.BTN_START}, # Start (Xbox) / Options (PS)
'menu': {ecodes.BTN_SELECT}, # Select / Share 'menu': {ecodes.BTN_SELECT}, # Select (Xbox) / Share (PS)
'guide': {ecodes.BTN_MODE}, # Xbox / PS Home 'guide': {ecodes.BTN_MODE}, # Xbox Button / PS Button
'increase_size': {ecodes.ABS_RZ}, # RT (Xbox) / R2 (PS)
'decrease_size': {ecodes.ABS_Z}, # LT (Xbox) / L2 (PS)
} }
class InputManager(QObject): class InputManager(QObject):
@ -68,6 +72,7 @@ class InputManager(QObject):
): ):
super().__init__(cast(QObject, main_window)) super().__init__(cast(QObject, main_window))
self._parent = main_window self._parent = main_window
self._gamepad_handling_enabled = True
# Ensure attributes exist on main_window # Ensure attributes exist on main_window
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)
@ -83,6 +88,10 @@ class InputManager(QObject):
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 self.rumble_effect_id: int | None = None # Store the rumble effect ID
self.lt_pressed = False
self.rt_pressed = False
self.last_trigger_time = 0.0
self.trigger_cooldown = 0.2
# Add variables for continuous D-pad movement # Add variables for continuous D-pad movement
self.dpad_timer = QTimer(self) self.dpad_timer = QTimer(self)
@ -106,8 +115,6 @@ class InputManager(QObject):
@Slot(bool) @Slot(bool)
def handle_fullscreen_slot(self, enable: bool) -> None: def handle_fullscreen_slot(self, enable: bool) -> None:
try: try:
if read_fullscreen_config():
return
window = self._parent window = self._parent
if not isinstance(window, QWidget): if not isinstance(window, QWidget):
return return
@ -126,6 +133,16 @@ 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 disable_gamepad_handling(self) -> None:
"""Отключает обработку событий геймпада."""
self._gamepad_handling_enabled = False
self.stop_rumble()
self.dpad_timer.stop()
def enable_gamepad_handling(self) -> None:
"""Включает обработку событий геймпада."""
self._gamepad_handling_enabled = True
def trigger_rumble(self, duration_ms: int = 200, strong_magnitude: int = 0x8000, weak_magnitude: int = 0x8000) -> None: 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.""" """Trigger a rumble effect on the gamepad if supported."""
if not read_rumble_config(): if not read_rumble_config():
@ -170,8 +187,10 @@ class InputManager(QObject):
@Slot(int) @Slot(int)
def handle_button_slot(self, button_code: int) -> None: def handle_button_slot(self, button_code: int) -> None:
if not self._gamepad_handling_enabled:
return
try: try:
# Игнорировать события геймпада, если игра запущена # Ignore gamepad events if a game is launched
if getattr(self._parent, '_gameLaunched', False): if getattr(self._parent, '_gameLaunched', False):
return return
@ -200,6 +219,16 @@ class InputManager(QObject):
return return
return return
# Handle QMessageBox
if isinstance(active, QMessageBox):
if button_code in BUTTONS['confirm']:
active.accept() # Close QMessageBox with the default button
return
elif button_code in BUTTONS['back']:
active.reject() # Close QMessageBox on back button
return
return
# Handle QComboBox # Handle QComboBox
if isinstance(focused, QComboBox): if isinstance(focused, QComboBox):
if button_code in BUTTONS['confirm']: if button_code in BUTTONS['confirm']:
@ -237,7 +266,7 @@ class InputManager(QObject):
focused.clearSelection() focused.clearSelection()
focused.hide() focused.hide()
# Закрытие AddGameDialog на кнопку B # Close AddGameDialog on B button
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
@ -284,6 +313,20 @@ class InputManager(QObject):
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)
self._parent.tabButtons[idx].setFocus(Qt.FocusReason.OtherFocusReason) self._parent.tabButtons[idx].setFocus(Qt.FocusReason.OtherFocusReason)
elif button_code in BUTTONS['increase_size'] and self._parent.stackedWidget.currentIndex() == 0:
# Increase card size with RT (Xbox) / R2 (PS)
size_slider = getattr(self._parent, 'sizeSlider', None)
if size_slider:
new_value = min(size_slider.value() + 10, size_slider.maximum())
size_slider.setValue(new_value)
self._parent.on_slider_released()
elif button_code in BUTTONS['decrease_size'] and self._parent.stackedWidget.currentIndex() == 0:
# Decrease card size with LT (Xbox) / L2 (PS)
size_slider = getattr(self._parent, 'sizeSlider', None)
if size_slider:
new_value = max(size_slider.value() - 10, size_slider.minimum())
size_slider.setValue(new_value)
self._parent.on_slider_released()
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)
@ -298,8 +341,10 @@ class InputManager(QObject):
@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:
if not self._gamepad_handling_enabled:
return
try: try:
# Игнорировать события геймпада, если игра запущена # Ignore gamepad events if a game is launched
if getattr(self._parent, '_gameLaunched', False): if getattr(self._parent, '_gameLaunched', False):
return return
@ -525,18 +570,21 @@ class InputManager(QObject):
if not app: if not app:
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
# Handle only key press events # Handle key press and release events
if not (isinstance(event, QKeyEvent) and event.type() == QEvent.Type.KeyPress): if not isinstance(event, QKeyEvent):
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
key = event.key() key = event.key()
modifiers = event.modifiers() modifiers = event.modifiers()
focused = QApplication.focusWidget() focused = QApplication.focusWidget()
popup = QApplication.activePopupWidget() popup = QApplication.activePopupWidget()
active_win = QApplication.activeWindow()
# Handle key press events
if event.type() == QEvent.Type.KeyPress:
# Open system overlay with Insert # Open system overlay with Insert
if key == Qt.Key.Key_Insert: if key == Qt.Key.Key_Insert:
if not popup and not isinstance(QApplication.activeWindow(), QDialog): if not popup and not isinstance(active_win, QDialog):
self._parent.openSystemOverlay() self._parent.openSystemOverlay()
return True return True
@ -545,27 +593,60 @@ class InputManager(QObject):
app.quit() app.quit()
return True return True
# Закрытие AddGameDialog на Esc # Close AddGameDialog with Escape
if key == Qt.Key.Key_Escape and isinstance(popup, QDialog): if key == Qt.Key.Key_Escape and isinstance(popup, QDialog):
popup.reject() # Закрываем диалог popup.reject()
return True return True
# Skip navigation keys if a popup is open
if popup:
return False
# FullscreenDialog navigation # FullscreenDialog navigation
active_win = QApplication.activeWindow()
if isinstance(active_win, FullscreenDialog): if isinstance(active_win, FullscreenDialog):
if key == Qt.Key.Key_Right:
active_win.show_next()
return True
if key == Qt.Key.Key_Left:
active_win.show_prev()
return True
if key in (Qt.Key.Key_Escape, Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Backspace): if key in (Qt.Key.Key_Escape, Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Backspace):
active_win.close() active_win.close()
return True return True
elif key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
# Navigate screenshots in FullscreenDialog
if key == Qt.Key.Key_Left:
active_win.show_prev()
elif key == Qt.Key.Key_Right:
active_win.show_next()
return True # Consume event to prevent tab switching
# Handle tab switching with Left/Right arrow keys when not in GameCard focus
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and (not isinstance(focused, GameCard) or focused is None):
idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons)
if key == Qt.Key.Key_Left:
new_idx = (idx - 1) % total
self._parent.switchTab(new_idx)
self._parent.tabButtons[new_idx].setFocus(Qt.FocusReason.OtherFocusReason)
return True
elif key == Qt.Key.Key_Right:
new_idx = (idx + 1) % total
self._parent.switchTab(new_idx)
self._parent.tabButtons[new_idx].setFocus(Qt.FocusReason.OtherFocusReason)
return True
# Map arrow keys to D-pad press events for other contexts
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left, Qt.Key.Key_Right):
now = time.time()
dpad_code = None
dpad_value = 0
if key == Qt.Key.Key_Up:
dpad_code = ecodes.ABS_HAT0Y
dpad_value = -1
elif key == Qt.Key.Key_Down:
dpad_code = ecodes.ABS_HAT0Y
dpad_value = 1
elif key == Qt.Key.Key_Left:
dpad_code = ecodes.ABS_HAT0X
dpad_value = -1
elif key == Qt.Key.Key_Right:
dpad_code = ecodes.ABS_HAT0X
dpad_value = 1
if dpad_code is not None:
self.dpad_moved.emit(dpad_code, dpad_value, now)
return True
# Launch/stop game on detail page # Launch/stop game on detail page
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
@ -575,169 +656,11 @@ class InputManager(QObject):
# Context menu for GameCard # Context menu for GameCard
if isinstance(focused, GameCard): if isinstance(focused, GameCard):
if key == Qt.Key.Key_F10 and Qt.KeyboardModifier.ShiftModifier: if key == Qt.Key.Key_F10 and modifiers & Qt.KeyboardModifier.ShiftModifier:
pos = QPoint(focused.width() // 2, focused.height() // 2) pos = QPoint(focused.width() // 2, focused.height() // 2)
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)
idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons)
if key == Qt.Key.Key_Left and (not isinstance(focused, GameCard) or focused is None):
new = (idx - 1) % total
self._parent.switchTab(new)
self._parent.tabButtons[new].setFocus()
return True
if key == Qt.Key.Key_Right and (not isinstance(focused, GameCard) or focused is None):
new = (idx + 1) % total
self._parent.switchTab(new)
self._parent.tabButtons[new].setFocus()
return True
# Library tab navigation
if self._parent.stackedWidget.currentIndex() == 0:
game_cards = self._parent.gamesListWidget.findChildren(GameCard)
scroll_area = self._parent.gamesListWidget.parentWidget()
while scroll_area and not isinstance(scroll_area, QScrollArea):
scroll_area = scroll_area.parentWidget()
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down):
if not game_cards:
return True
# If no focused widget or not a GameCard, focus the first card
if not isinstance(focused, GameCard) or focused not in game_cards:
game_cards[0].setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(game_cards[0], 50, 50)
return True
# Group cards by rows based on y-coordinate
rows = {}
for card in game_cards:
y = card.pos().y()
if y not in rows:
rows[y] = []
rows[y].append(card)
# Sort cards in each row by x-coordinate
for y in rows:
rows[y].sort(key=lambda c: c.pos().x())
# Sort rows by y-coordinate
sorted_rows = sorted(rows.items(), key=lambda x: x[0])
# Find current row and column
current_y = focused.pos().y()
current_row_idx = next(i for i, (y, _) in enumerate(sorted_rows) if y == current_y)
current_row = sorted_rows[current_row_idx][1]
current_col_idx = current_row.index(focused)
if key == Qt.Key.Key_Right:
next_col_idx = current_col_idx + 1
if next_col_idx < len(current_row):
next_card = current_row[next_col_idx]
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True
else:
# Move to the first card of the next row if available
if current_row_idx < len(sorted_rows) - 1:
next_row = sorted_rows[current_row_idx + 1][1]
next_card = next_row[0] if next_row else None
if next_card:
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True
elif key == Qt.Key.Key_Left:
next_col_idx = current_col_idx - 1
if next_col_idx >= 0:
next_card = current_row[next_col_idx]
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True
else:
# Move to the last card of the previous row if available
if current_row_idx > 0:
prev_row = sorted_rows[current_row_idx - 1][1]
next_card = prev_row[-1] if prev_row else None
if next_card:
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True
elif key == Qt.Key.Key_Down:
next_row_idx = current_row_idx + 1
if next_row_idx < len(sorted_rows):
next_row = sorted_rows[next_row_idx][1]
target_x = focused.pos().x()
next_card = min(
next_row,
key=lambda c: abs(c.pos().x() - target_x),
default=None
)
if next_card:
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True
elif key == Qt.Key.Key_Up:
next_row_idx = current_row_idx - 1
if next_row_idx >= 0:
next_row = sorted_rows[next_row_idx][1]
target_x = focused.pos().x()
next_card = min(
next_row,
key=lambda c: abs(c.pos().x() - target_x),
default=None
)
if next_card:
next_card.setFocus()
if scroll_area:
scroll_area.ensureWidgetVisible(next_card, 50, 50)
return True
elif current_row_idx == 0:
self._parent.tabButtons[0].setFocus()
return True
# Navigate down into tab content
if key == Qt.Key.Key_Down:
if isinstance(focused, NavLabel):
page = self._parent.stackedWidget.currentWidget()
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
# Navigate up through tab content
if key == Qt.Key.Key_Up:
if isinstance(focused, NavLabel):
return True
if focused is not None:
focused.focusPreviousChild()
return True
# General actions: Activate, Back, Add # General actions: Activate, Back, Add
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
self._parent.activateFocusedWidget() self._parent.activateFocusedWidget()
@ -757,11 +680,24 @@ class InputManager(QObject):
# Toggle fullscreen with F11 # Toggle fullscreen with F11
if key == Qt.Key.Key_F11: if key == Qt.Key.Key_F11:
if read_fullscreen_config():
return True
self.toggle_fullscreen.emit(not self._is_fullscreen) self.toggle_fullscreen.emit(not self._is_fullscreen)
return True return True
# Handle key release events for arrow keys
elif event.type() == QEvent.Type.KeyRelease:
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Left, Qt.Key.Key_Right):
now = time.time()
dpad_code = None
if key in (Qt.Key.Key_Up, Qt.Key.Key_Down):
dpad_code = ecodes.ABS_HAT0Y
elif key in (Qt.Key.Key_Left, Qt.Key.Key_Right):
dpad_code = ecodes.ABS_HAT0X
if dpad_code is not None:
# Emit release event with value 0 to stop continuous movement
self.dpad_moved.emit(dpad_code, 0, now)
return True
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
def init_gamepad(self) -> None: def init_gamepad(self) -> None:
@ -809,9 +745,9 @@ class InputManager(QObject):
self.gamepad_thread.join() self.gamepad_thread.join()
self.gamepad_thread = threading.Thread(target=self.monitor_gamepad, daemon=True) self.gamepad_thread = threading.Thread(target=self.monitor_gamepad, daemon=True)
self.gamepad_thread.start() self.gamepad_thread.start()
# Отправляем сигнал для полноэкранного режима только если: # Send signal for fullscreen mode only if:
# 1. auto_fullscreen_gamepad включено # 1. auto_fullscreen_gamepad is enabled
# 2. fullscreen выключено (чтобы не конфликтовать с основной настройкой) # 2. fullscreen is not already enabled (to avoid conflict)
if read_auto_fullscreen_gamepad() and not read_fullscreen_config(): if read_auto_fullscreen_gamepad() and not read_fullscreen_config():
self.toggle_fullscreen.emit(True) self.toggle_fullscreen.emit(True)
except Exception as e: except Exception as e:
@ -845,6 +781,25 @@ class InputManager(QObject):
else: else:
self.button_pressed.emit(event.code) self.button_pressed.emit(event.code)
elif event.type == ecodes.EV_ABS: elif event.type == ecodes.EV_ABS:
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
# Проверяем, достаточно ли времени прошло с последнего срабатывания
if now - self.last_trigger_time < self.trigger_cooldown:
continue
if event.code == ecodes.ABS_Z: # LT/L2
if event.value > 128 and not self.lt_pressed:
self.lt_pressed = True
self.button_pressed.emit(event.code)
self.last_trigger_time = now
elif event.value <= 128 and self.lt_pressed:
self.lt_pressed = False
elif event.code == ecodes.ABS_RZ: # RT/R2
if event.value > 128 and not self.rt_pressed:
self.rt_pressed = True
self.button_pressed.emit(event.code)
self.last_trigger_time = now
elif event.value <= 128 and self.rt_pressed:
self.rt_pressed = False
else:
self.dpad_moved.emit(event.code, event.value, now) self.dpad_moved.emit(event.code, event.value, now)
except OSError as e: except OSError as e:
if e.errno == 19: # ENODEV: No such device if e.errno == 19: # ENODEV: No such device

View File

@ -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-11 23:15+0500\n" "POT-Creation-Date: 2025-06-14 10:37+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"
@ -494,9 +494,6 @@ msgstr ""
msgid "Launching" msgid "Launching"
msgstr "" msgstr ""
msgid "System Overlay"
msgstr ""
msgid "Reboot" msgid "Reboot"
msgstr "" msgstr ""

View File

@ -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-11 23:15+0500\n" "POT-Creation-Date: 2025-06-14 10:37+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"
@ -494,9 +494,6 @@ msgstr ""
msgid "Launching" msgid "Launching"
msgstr "" msgstr ""
msgid "System Overlay"
msgstr ""
msgid "Reboot" msgid "Reboot"
msgstr "" msgstr ""

View File

@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n" "Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-06-11 23:15+0500\n" "POT-Creation-Date: 2025-06-14 10:37+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"
@ -492,9 +492,6 @@ msgstr ""
msgid "Launching" msgid "Launching"
msgstr "" msgstr ""
msgid "System Overlay"
msgstr ""
msgid "Reboot" msgid "Reboot"
msgstr "" msgstr ""

View File

@ -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-11 23:15+0500\n" "POT-Creation-Date: 2025-06-14 10:37+0500\n"
"PO-Revision-Date: 2025-06-11 23:15+0500\n" "PO-Revision-Date: 2025-06-14 10:37+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"
@ -384,10 +384,10 @@ msgid "Auto Fullscreen on Gamepad connected:"
msgstr "Режим полноэкранного отображения приложения при подключении геймпада:" msgstr "Режим полноэкранного отображения приложения при подключении геймпада:"
msgid "Gamepad haptic feedback" msgid "Gamepad haptic feedback"
msgstr "Тактильная обратная связь на геймпаде" msgstr "Тактильная отдача на геймпаде"
msgid "Gamepad haptic feedback:" msgid "Gamepad haptic feedback:"
msgstr "Тактильная обратная связь на геймпаде:" msgstr "Тактильная отдача на геймпаде:"
msgid "Save Settings" msgid "Save Settings"
msgstr "Сохранить настройки" msgstr "Сохранить настройки"
@ -503,9 +503,6 @@ msgstr "Невозможно запустить игру пока запущен
msgid "Launching" msgid "Launching"
msgstr "Идёт запуск" msgstr "Идёт запуск"
msgid "System Overlay"
msgstr "Системный оверлей"
msgid "Reboot" msgid "Reboot"
msgstr "Перезагрузить" msgstr "Перезагрузить"

View File

@ -56,6 +56,7 @@ class MainWindow(QMainWindow):
self.current_exec_line = None self.current_exec_line = None
self.currentDetailPage = None self.currentDetailPage = None
self.current_play_button = None self.current_play_button = None
self.current_focused_card = None
self.pending_games = [] self.pending_games = []
self.game_card_cache = {} self.game_card_cache = {}
self.pending_images = {} self.pending_images = {}
@ -65,6 +66,7 @@ class MainWindow(QMainWindow):
self.games_load_timer.timeout.connect(self.finalize_game_loading) self.games_load_timer.timeout.connect(self.finalize_game_loading)
self.games_loaded.connect(self.on_games_loaded) self.games_loaded.connect(self.on_games_loaded)
self.current_add_game_dialog = None self.current_add_game_dialog = None
self.current_hovered_card = None
# Добавляем таймер для дебаунсинга сохранения настроек # Добавляем таймер для дебаунсинга сохранения настроек
self.settingsDebounceTimer = QTimer(self) self.settingsDebounceTimer = QTimer(self)
@ -241,6 +243,65 @@ class MainWindow(QMainWindow):
self.updateGameGrid() self.updateGameGrid()
self.progress_bar.setVisible(False) self.progress_bar.setVisible(False)
def _on_card_focused(self, game_name: str, is_focused: bool):
"""Обработчик сигнала focusChanged от GameCard."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_focused:
# Если карточка получила фокус
if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем текущую hovered карточку
self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None)
self.current_hovered_card = None
if self.current_focused_card and self.current_focused_card != card:
# Сбрасываем текущую focused карточку
self.current_focused_card._focused = False
self.current_focused_card.clearFocus()
self.current_focused_card = card
else:
# Если карточка потеряла фокус
if self.current_focused_card == card:
self.current_focused_card = None
def _on_card_hovered(self, game_name: str, is_hovered: bool):
"""Обработчик сигнала hoverChanged от GameCard."""
card_key = None
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_hovered:
# Если мышь наведена на карточку
if self.current_focused_card and self.current_focused_card != card:
# Сбрасываем текущую focused карточку
self.current_focused_card._focused = False
self.current_focused_card.clearFocus()
if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем предыдущую hovered карточку
self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None)
self.current_hovered_card = card
else:
# Если мышь покинула карточку
if self.current_hovered_card == card:
self.current_hovered_card = None
def loadGames(self): def loadGames(self):
display_filter = read_display_filter() display_filter = read_display_filter()
favorites = read_favorites() favorites = read_favorites()
@ -535,10 +596,12 @@ class MainWindow(QMainWindow):
def startSearchDebounce(self, text): def startSearchDebounce(self, text):
self.searchDebounceTimer.start() self.searchDebounceTimer.start()
def on_slider_value_changed(self, value: int): def on_slider_released(self):
self.card_width = value self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{value} px") self.sizeSlider.setToolTip(f"{self.card_width} px")
save_card_size(value) save_card_size(self.card_width)
for card in self.game_card_cache.values():
card.update_card_size(self.card_width)
self.updateGameGrid() self.updateGameGrid()
def filterGamesDelayed(self): def filterGamesDelayed(self):
@ -581,7 +644,7 @@ 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) self.sizeSlider.sliderReleased.connect(self.on_slider_released)
sliderLayout.addWidget(self.sizeSlider) sliderLayout.addWidget(self.sizeSlider)
layout.addLayout(sliderLayout) layout.addLayout(sliderLayout)
@ -679,6 +742,8 @@ 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
) )
card.hoverChanged.connect(self._on_card_hovered)
card.focusChanged.connect(self._on_card_focused)
# Подключаем сигналы контекстного меню # Подключаем сигналы контекстного меню
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)
@ -988,6 +1053,7 @@ class MainWindow(QMainWindow):
# 6. Automatic fullscreen on gamepad connection # 6. Automatic fullscreen on gamepad connection
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.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE) self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
self.autoFullscreenGamepadTitle = QLabel(_("Auto Fullscreen on Gamepad connected:")) self.autoFullscreenGamepadTitle = QLabel(_("Auto Fullscreen on Gamepad connected:"))
@ -1492,7 +1558,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
@ -1723,6 +1789,8 @@ class MainWindow(QMainWindow):
elif not child_running: elif not child_running:
# Игра завершилась сбрасываем флаг, сбрасываем кнопку и останавливаем таймер # Игра завершилась сбрасываем флаг, сбрасываем кнопку и останавливаем таймер
self._gameLaunched = False self._gameLaunched = False
if hasattr(self, 'input_manager'):
self.input_manager.enable_gamepad_handling()
self.resetPlayButton() self.resetPlayButton()
#self._uninhibit_screensaver() #self._uninhibit_screensaver()
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None: if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
@ -1779,6 +1847,9 @@ class MainWindow(QMainWindow):
# Если игра уже запущена для этого exe останавливаем её по нажатию кнопки # Если игра уже запущена для этого exe останавливаем её по нажатию кнопки
if self.game_processes and self.target_exe == current_exe: if self.game_processes and self.target_exe == current_exe:
if hasattr(self, 'input_manager'):
self.input_manager.enable_gamepad_handling()
for proc in self.game_processes: for proc in self.game_processes:
try: try:
parent = psutil.Process(proc.pid) parent = psutil.Process(proc.pid)
@ -1818,6 +1889,11 @@ class MainWindow(QMainWindow):
self.target_exe = current_exe self.target_exe = current_exe
exe_name = os.path.splitext(current_exe)[0] exe_name = os.path.splitext(current_exe)[0]
env_vars = os.environ.copy() env_vars = os.environ.copy()
# Delay disabling gamepad handling to allow rumble to complete
if hasattr(self, 'input_manager'):
QTimer.singleShot(200, self.input_manager.disable_gamepad_handling)
if entry_exec_split[0] == "env" and len(entry_exec_split) > 1 and 'data/scripts/start.sh' in entry_exec_split[1]: if entry_exec_split[0] == "env" and len(entry_exec_split) > 1 and 'data/scripts/start.sh' in entry_exec_split[1]:
env_vars['START_FROM_STEAM'] = '1' env_vars['START_FROM_STEAM'] = '1'
elif entry_exec_split[0] == "flatpak": elif entry_exec_split[0] == "flatpak":
@ -1839,14 +1915,46 @@ class MainWindow(QMainWindow):
self.checkProcessTimer.start(500) self.checkProcessTimer.start(500)
def closeEvent(self, event): def closeEvent(self, event):
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
for proc in self.game_processes: for proc in self.game_processes:
try: try:
parent = psutil.Process(proc.pid)
children = parent.children(recursive=True)
for child in children:
try:
logger.debug(f"Terminating child process {child.pid}")
child.terminate()
except psutil.NoSuchProcess:
logger.debug(f"Child process {child.pid} already terminated")
psutil.wait_procs(children, timeout=5)
for child in children:
if child.is_running():
logger.debug(f"Killing child process {child.pid}")
child.kill()
logger.debug(f"Terminating process group {proc.pid}")
os.killpg(os.getpgid(proc.pid), signal.SIGTERM) os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except ProcessLookupError: except (psutil.NoSuchProcess, ProcessLookupError) as e:
pass # процесс уже завершился logger.debug(f"Process {proc.pid} already terminated: {e}")
self.game_processes = [] # Очищаем список процессов
# Сохраняем настройки окна
if not read_fullscreen_config(): if not read_fullscreen_config():
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
save_window_geometry(self.width(), self.height()) save_window_geometry(self.width(), self.height())
save_card_size(self.card_width) save_card_size(self.card_width)
# Очищаем таймеры и другие ресурсы
if hasattr(self, 'games_load_timer') and self.games_load_timer.isActive():
self.games_load_timer.stop()
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer.isActive():
self.settingsDebounceTimer.stop()
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer.isActive():
self.searchDebounceTimer.stop()
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer and self.checkProcessTimer.isActive():
self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None
QApplication.quit()
event.accept() event.accept()

View File

@ -1,10 +1,11 @@
import subprocess import subprocess
from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QMessageBox from PySide6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QMessageBox, QApplication, QWidget
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
import os import os
from portprotonqt.localization import _ from portprotonqt.localization import _
from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.theme_manager import ThemeManager
logger = get_logger(__name__) logger = get_logger(__name__)
@ -13,44 +14,68 @@ class SystemOverlay(QDialog):
def __init__(self, parent, theme): def __init__(self, parent, theme):
super().__init__(parent) super().__init__(parent)
self.theme = theme self.theme = theme
self.setWindowTitle(_("System Overlay")) self.setWindowTitle("System Overlay")
self.setModal(True) self.setModal(True)
self.setFixedSize(400, 300) self.setFixedSize(400, 300)
self.theme_manager = ThemeManager()
self.setStyleSheet(self.theme.OVERLAY_WINDOW_STYLE)
# Make window stay on top and frameless
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.Dialog |
Qt.WindowType.WindowStaysOnTopHint
)
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20) layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(10) layout.setSpacing(10)
# Reboot button # Reboot button
reboot_button = QPushButton(_("Reboot")) reboot_button = AutoSizeButton(
_("Reboot"),
icon=self.theme_manager.get_icon("reboot")
)
reboot_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) reboot_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
reboot_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) reboot_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
reboot_button.clicked.connect(self.reboot) reboot_button.clicked.connect(self.reboot)
layout.addWidget(reboot_button) layout.addWidget(reboot_button)
# Shutdown button # Shutdown button
shutdown_button = QPushButton(_("Shutdown")) shutdown_button = AutoSizeButton(
_("Shutdown"),
icon=self.theme_manager.get_icon("shutdown")
)
shutdown_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) shutdown_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
shutdown_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) shutdown_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
shutdown_button.clicked.connect(self.shutdown) shutdown_button.clicked.connect(self.shutdown)
layout.addWidget(shutdown_button) layout.addWidget(shutdown_button)
# Suspend button # Suspend button
suspend_button = QPushButton(_("Suspend")) suspend_button = AutoSizeButton(
_("Suspend"),
icon=self.theme_manager.get_icon("suspend")
)
suspend_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) suspend_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
suspend_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) suspend_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
suspend_button.clicked.connect(self.suspend) suspend_button.clicked.connect(self.suspend)
layout.addWidget(suspend_button) layout.addWidget(suspend_button)
# Exit application button # Exit application button
exit_button = QPushButton(_("Exit Application")) exit_button = AutoSizeButton(
_("Exit Application"),
icon=self.theme_manager.get_icon("exit")
)
exit_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) exit_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
exit_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) exit_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
exit_button.clicked.connect(self.exit_application) exit_button.clicked.connect(self.exit_application)
layout.addWidget(exit_button) layout.addWidget(exit_button)
# Return to Desktop button # Return to Desktop button
desktop_button = QPushButton(_("Return to Desktop")) desktop_button = AutoSizeButton(
_("Return to Desktop"),
icon=self.theme_manager.get_icon("desktop")
)
desktop_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) desktop_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
desktop_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) desktop_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
desktop_button.clicked.connect(self.return_to_desktop) desktop_button.clicked.connect(self.return_to_desktop)
@ -62,14 +87,31 @@ class SystemOverlay(QDialog):
layout.addWidget(desktop_button) layout.addWidget(desktop_button)
# Cancel button # Cancel button
cancel_button = QPushButton(_("Cancel")) cancel_button = AutoSizeButton(
_("Cancel"),
icon=self.theme_manager.get_icon("cancel")
)
cancel_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE) cancel_button.setStyleSheet(self.theme.OVERLAY_BUTTON_STYLE)
cancel_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus) cancel_button.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
cancel_button.clicked.connect(self.reject) cancel_button.clicked.connect(self.reject)
layout.addWidget(cancel_button) layout.addWidget(cancel_button)
# Set focus to the first button def showEvent(self, event):
reboot_button.setFocus() """Override showEvent to center window and set focus."""
super().showEvent(event)
# Center window relative to parent or screen
parent = self.parent()
if isinstance(parent, QWidget) and parent.isVisible():
self.move(parent.geometry().center() - self.rect().center())
else:
screen_geometry = QApplication.primaryScreen().availableGeometry()
self.move(screen_geometry.center() - self.rect().center())
# Set focus on first button
button = self.findChild(QPushButton)
if button is not None:
button.setFocus()
def reboot(self): def reboot(self):
try: try:

View File

@ -8,6 +8,76 @@ current_theme_name = read_theme_from_config()
favoriteLabelSize = 48, 48 favoriteLabelSize = 48, 48
pixmapsScaledSize = 60, 60 pixmapsScaledSize = 60, 60
GAME_CARD_ANIMATION = {
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
# Значение в пикселях.
"default_border_width": 2,
# Ширина обводки при наведении курсора.
# Увеличивает толщину рамки, когда курсор находится над карточкой.
# Значение в пикселях.
"hover_border_width": 8,
# Ширина обводки при фокусе (например, при выборе с клавиатуры).
# Увеличивает толщину рамки, когда карточка в фокусе.
# Значение в пикселях.
"focus_border_width": 12,
# Минимальная ширина обводки во время пульсирующей анимации.
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания").
# Значение в пикселях.
"pulse_min_border_width": 8,
# Максимальная ширина обводки во время пульсирующей анимации.
# Определяет максимальную толщину рамки при пульсации.
# Значение в пикселях.
"pulse_max_border_width": 10,
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе).
# Влияет на скорость перехода от одной ширины обводки к другой.
# Значение в миллисекундах.
"thickness_anim_duration": 300,
# Длительность одного цикла пульсирующей анимации.
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
# Значение в миллисекундах.
"pulse_anim_duration": 800,
# Длительность анимации вращения градиента.
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
# Значение в миллисекундах.
"gradient_anim_duration": 3000,
# Начальный угол градиента (в градусах).
# Определяет начальную точку вращения градиента при старте анимации.
"gradient_start_angle": 360,
# Конечный угол градиента (в градусах).
# Определяет конечную точку вращения градиента.
# Значение 0 означает полный поворот на 360 градусов.
"gradient_end_angle": 0,
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
"thickness_easing_curve": "OutBack",
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
# Влияет на "чувство" возврата к исходной ширине обводки.
"thickness_easing_curve_out": "InBack",
# Цвета градиента для анимированной обводки.
# Список словарей, где каждый словарь задает позицию (0.01.0) и цвет в формате hex.
# Влияет на внешний вид обводки при наведении или фокусе.
"gradient_colors": [
{"position": 0, "color": "#00fff5"}, # Начальный цвет (циан)
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
]
}
# СТИЛЬ ШАПКИ ГЛАВНОГО ОКНА # СТИЛЬ ШАПКИ ГЛАВНОГО ОКНА
MAIN_WINDOW_HEADER_STYLE = """ MAIN_WINDOW_HEADER_STYLE = """
QFrame { QFrame {
@ -416,6 +486,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;

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8 1c-3.8581 0-7 3.1419-7 7s3.1419 7 7 7 7-3.1419 7-7-3.1419-7-7-7zm0 1.3988c3.1014 0 5.6012 2.4998 5.6012 5.6012s-2.4998 5.6012-5.6012 5.6012-5.6012-2.4998-5.6012-5.6012 2.4998-5.6012 5.6012-5.6012zm-2.1002 3.501a0.70007 0.70007 0 0 0-0.69938 0.69938 0.70007 0.70007 0 0 0 0.69938 0.70144h0.0062a0.70007 0.70007 0 0 0 0.70144-0.70144 0.70007 0.70007 0 0 0-0.70144-0.69938zm4.2004 0a0.70007 0.70007 0 0 0-0.69938 0.69938 0.70007 0.70007 0 0 0 0.69938 0.70144h0.0062a0.70007 0.70007 0 0 0 0.70144-0.70144 0.70007 0.70007 0 0 0-0.70144-0.69938zm-2.1002 2.9452c-0.81784 0-1.6354 0.31214-2.2499 0.93935a0.70007 0.70007 0 0 0 0.01026 0.99062 0.70007 0.70007 0 0 0 0.98857-0.01026c0.69244-0.70672 1.8098-0.70672 2.5022 0a0.70007 0.70007 0 0 0 0.98857 0.01026 0.70007 0.70007 0 0 0 0.01026-0.99062c-0.61461-0.62721-1.4321-0.93935-2.2499-0.93935z" stop-color="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.0501"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8.4694 1c-0.38007 0-0.76931 0.12326-1.0795 0.39899-0.24769 0.22016-0.42499 0.54342-0.48662 0.90798-0.11341-0.02303-0.22892-0.03356-0.34306-0.03356-0.38003 0-0.76934 0.12329-1.0795 0.39899s-0.51086 0.71412-0.51086 1.1914v3.2534c-0.58842-0.47053-1.4218-0.53407-2.0788-0.13983-0.73301 0.43947-0.98585 1.3992-0.56679 2.1441 1.1968 2.1273 1.8635 3.2982 2.1124 3.6804h-0.0019c0.04201 0.06466 0.08444 0.12946 0.12678 0.1939 0.78928 1.1985 2.1179 1.8996 3.5443 1.9595a0.63645 0.63645 0 0 0 0.04475 0.044746h1.2734c2.4527 0 4.4541-2.0014 4.4541-4.4541v-5.4087c0-0.47729-0.20066-0.9175-0.51085-1.1932-0.31015-0.27573-0.69944-0.39899-1.0795-0.39899-0.11425 0-0.22955 0.010492-0.34306 0.03356-0.0619-0.36382-0.23934-0.68631-0.48662-0.90611-0.3102-0.27573-0.69944-0.39899-1.0795-0.39899-0.11414 0-0.22964 0.010534-0.34306 0.03356-0.06162-0.36456-0.23893-0.68781-0.48662-0.90798-0.3102-0.27573-0.69944-0.39899-1.0795-0.39899zm0 1.2734c0.09723 0 0.18528 0.033977 0.23305 0.076442 0.04778 0.042465 0.08576 0.081413 0.08576 0.24051v5.4106a0.63639 0.63639 0 0 0 0.63577 0.63577 0.63639 0.63639 0 0 0 0.63577-0.63577v-4.1353a0.63639 0.63639 0 0 0 0-0.00186c0-0.1591 0.038-0.19805 0.08576-0.24051 0.04778-0.042465 0.13583-0.078306 0.23305-0.078306 0.09723 0 0.18528 0.035841 0.23305 0.078306 0.04778 0.042465 0.08576 0.081413 0.08576 0.24051v4.1372a0.63639 0.63639 0 0 0 0.63577 0.63577 0.63639 0.63639 0 0 0 0.63577-0.63577v-2.8619a0.63639 0.63639 0 0 0 0-0.00186c0-0.1591 0.03799-0.19805 0.08576-0.24051 0.04777-0.042465 0.13583-0.078306 0.23305-0.078306 0.09723 0 0.18528 0.035841 0.23305 0.078306s0.08576 0.081413 0.08576 0.24051v5.4087c0 1.7649-1.4177 3.1826-3.1826 3.1826h-1.141c-1.0712 1.81e-4 -2.0676-0.53724-2.6568-1.4319-0.04114-0.06262-0.08223-0.12548-0.12305-0.18831-0.14817-0.22748-0.8766-1.4876-2.0714-3.6114-0.08788-0.1562-0.03999-0.33666 0.11373-0.42882a0.63645 0.63645 0 0 0 0.0019 0c0.2198-0.13189 0.4917-0.097253 0.67306 0.083899l0.92662 0.92476 0.0093 0.00932a0.63639 0.63639 0 0 0 0.01492 0.011187 0.63623 0.63623 0 0 0 0.04847 0.042882 0.63623 0.63623 0 0 0 0.06898 0.046611 0.63623 0.63623 0 0 0 0.07458 0.037289 0.63623 0.63623 0 0 0 0.07831 0.026102 0.63623 0.63623 0 0 0 0.07831 0.01678 0.63639 0.63639 0 0 0 0.0037 0 0.63623 0.63623 0 0 0 0.08203 0.00559 0.63623 0.63623 0 0 0 0.08203-0.00559 0.63639 0.63639 0 0 0 0.36729-0.18085 0.63623 0.63623 0 0 0 0.05407-0.063391 0.63639 0.63639 0 0 0 0.04661-0.068984 0.63623 0.63623 0 0 0 0.08576-0.31695v-4.7729c0-0.15914 0.03796-0.19802 0.08576-0.24051 0.04781-0.042493 0.13579-0.078306 0.23305-0.078306s0.18525 0.035813 0.23305 0.078306c0.04781 0.042493 0.08576 0.081374 0.08576 0.24051v4.1372a0.63623 0.63623 0 0 0 0.63577 0.63577 0.63623 0.63623 0 0 0 0.63577-0.63577v-4.1353a0.63639 0.63639 0 0 0 0-0.00186v-1.2734c0-0.1591 0.038-0.19805 0.08576-0.24051 0.04778-0.042465 0.13583-0.076442 0.23305-0.076442z" stop-color="#000000" stroke-width="0"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m4.499 1c-0.76498 0-1.3988 0.63379-1.3988 1.3988v1.4008c0 1.5752 0.61452 2.8449 1.5464 3.6733 0.22594 0.20084 0.46887 0.37639 0.724 0.5271-0.25512 0.15071-0.49805 0.32626-0.724 0.5271-0.93192 0.82837-1.5464 2.0982-1.5464 3.6733v1.4008c0 0.76498 0.63379 1.3988 1.3988 1.3988h7.002c0.76498 0 1.3988-0.63379 1.3988-1.3988v-1.4008c0-1.5752-0.61452-2.8449-1.5464-3.6733-0.22594-0.20084-0.46887-0.37639-0.724-0.5271 0.25513-0.15071 0.49805-0.32626 0.724-0.5271 0.93192-0.82837 1.5464-2.0982 1.5464-3.6733v-1.4008c0-0.76498-0.63379-1.3988-1.3988-1.3988zm0 1.3988h7.002v1.4008h-7.002zm0.23381 2.8016h6.5344c-0.18996 0.50569-0.4851 0.90658-0.845 1.2265-0.64324 0.57176-1.5277 0.87372-2.4222 0.87372s-1.779-0.30195-2.4222-0.87372c-0.3599-0.31991-0.65504-0.7208-0.845-1.2265zm3.2672 3.499c0.89453 0 1.779 0.30195 2.4222 0.87372 0.3599 0.31991 0.65504 0.7208 0.845 1.2265h-6.5344c0.18996-0.50569 0.4851-0.90658 0.845-1.2265 0.64324-0.57176 1.5277-0.87372 2.4222-0.87372zm-3.501 3.501h7.002v1.4008h-7.002z" stop-color="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.0501"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m9.3899 1a1.4351 1.4351 0 0 0-1.4349 1.4349 1.4351 1.4351 0 0 0 1.4349 1.4349 1.4351 1.4351 0 0 0 1.4349-1.4349 1.4351 1.4351 0 0 0-1.4349-1.4349zm-0.73373 3.5667a0.69575 0.69575 0 0 0-0.01427 0.00204 0.69568 0.69568 0 0 0-0.0591 0.00611 0.69575 0.69575 0 0 0-0.02446 0.00408 0.69568 0.69568 0 0 0-0.02038 0.00408l-3.4567 0.69093a0.69575 0.69575 0 0 0-0.56049 0.68278v2.0871a0.69568 0.69568 0 0 0 0.69704 0.69501 0.69568 0.69568 0 0 0 0.69501-0.69501v-1.5164l1.9322-0.38725-0.53196 3.1815a0.69575 0.69575 0 0 0 0.26904 0.67055l2.5049 1.8771v2.4356a0.69568 0.69568 0 0 0 0.69501 0.69501 0.69568 0.69568 0 0 0 0.69704-0.69501v-2.7821a0.69575 0.69575 0 0 0-0.27922-0.55641l-2.4437-1.8343 0.40355-2.4234 1.1312 1.1312a0.69575 0.69575 0 0 0 0.27107 0.16713l2.0871 0.69704a0.69568 0.69568 0 0 0 0.88048-0.44024 0.69568 0.69568 0 0 0-0.44024-0.88048l-1.9301-0.64405-1.9709-1.9709a0.69575 0.69575 0 0 0-0.05095-0.044839 0.69568 0.69568 0 0 0-0.0061-0.00408 0.69575 0.69575 0 0 0-0.04076-0.030572 0.69568 0.69568 0 0 0-0.05911-0.036687 0.69575 0.69575 0 0 0-0.04892-0.024458 0.69568 0.69568 0 0 0-0.0061-0.00204 0.69575 0.69575 0 0 0-0.06318-0.024458 0.69568 0.69568 0 0 0-0.10394-0.026496 0.69568 0.69568 0 0 0-0.0163-0.00204 0.69575 0.69575 0 0 0-0.13656-0.00611zm-1.5673 5.9127a0.69568 0.69568 0 0 0-0.58087 0.38317l-0.2833 0.56864-2.9573-0.59106a0.69568 0.69568 0 0 0-0.81933 0.54622 0.69568 0.69568 0 0 0 0.54622 0.8173l3.4771 0.69704a0.69575 0.69575 0 0 0 0.76023-0.37094l0.52176-1.0435a0.69568 0.69568 0 0 0-0.31184-0.93347 0.69568 0.69568 0 0 0-0.3526-0.07337z" stop-color="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.0435"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m11.896 0.99999a0.77558 0.77558 0 0 0-0.04089 0.0023c-2.9888 0.16296-5.5645 2.0598-6.7001 4.803-1.6574 0.22304-3.1671 1.0967-4.0123 2.5628a0.7755 0.7755 0 0 0-0.03181 0.2431 0.7755 0.7755 0 0 0-0.06589 0.052255 0.7755 0.7755 0 0 0 0.04998 0.063615 0.7755 0.7755 0 0 0 0.33171 0.69977 0.7755 0.7755 0 0 0 0.18176-0.047712 0.7755 0.7755 0 0 0 0.11587 0.14768c0.30901 0.036723 0.60932 0.09847 0.8997 0.18403-1.1944 1.1741-1.8021 2.8615-1.579 4.5735a0.77558 0.77558 0 0 0 0.67023 0.67023c1.712 0.22304 3.3994-0.38463 4.5735-1.579 0.08557 0.29041 0.1473 0.59066 0.18403 0.8997a0.77558 0.77558 0 0 0 1.1587 0.58163c1.4661-0.84521 2.3398-2.3549 2.5628-4.0123 2.7432-1.1355 4.64-3.7113 4.803-6.7001a0.77558 0.77558 0 0 0 0.0023-0.0409c0-1.704-1.3995-3.1035-3.1035-3.1035zm0.02045 1.5608c0.84612 0.01261 1.5096 0.67611 1.5222 1.5222-0.14636 2.4969-1.7648 4.6608-4.1259 5.4914a0.77558 0.77558 0 0 0-0.51801 0.70658c-0.03211 0.97216-0.51756 1.7993-1.1746 2.481-0.10516-0.31723-0.23158-0.62236-0.37715-0.91561a0.7755 0.7755 0 0 0-0.36352-0.64751c-0.53452-0.83732-1.2415-1.5443-2.0789-2.0789a0.7755 0.7755 0 0 0-0.64751-0.36352c-0.29324-0.14557-0.59838-0.27199-0.91561-0.37715 0.68166-0.65705 1.5088-1.1425 2.481-1.1746a0.77558 0.77558 0 0 0 0.70658-0.51801c0.83059-2.3611 2.9944-3.9795 5.4914-4.1259zm-1.5699 1.5427c-0.36621 0-0.74435 0.11713-1.0497 0.38851-0.3053 0.27138-0.50211 0.7086-0.50211 1.161s0.19681 0.89187 0.50211 1.1633c0.3053 0.27138 0.68345 0.38851 1.0497 0.38851 0.36621 0 0.74208-0.11713 1.0474-0.38851 0.3053-0.27138 0.50211-0.71088 0.50211-1.1633s-0.19681-0.8896-0.50211-1.161c-0.3053-0.27138-0.68117-0.38851-1.0474-0.38851zm-6.1843 6.3388c0.54645 0.37575 1.0192 0.84854 1.395 1.395-0.66921 0.88979-1.7235 1.346-2.8377 1.4427 0.09666-1.1142 0.55292-2.1685 1.4427-2.8377z" stop-color="#000000" stroke-width="0"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8 15q-1.4525 0-2.73-0.55125t-2.2225-1.4962-1.4962-2.2225-0.55125-2.73 0.55125-2.73 1.4962-2.2225 2.2225-1.4962 2.73-0.55125 2.73 0.55125 2.2225 1.4962 1.4962 2.2225 0.55125 2.73-0.55125 2.73-1.4962 2.2225-2.2225 1.4962-2.73 0.55125zm0-1.4q0.945 0 1.82-0.30625t1.61-0.88375l-7.84-7.84q-0.5775 0.735-0.88375 1.61t-0.30625 1.82q0 2.345 1.6275 3.9725t3.9725 1.6275zm4.41-2.17q0.5775-0.735 0.88375-1.61t0.30625-1.82q0-2.345-1.6275-3.9725t-3.9725-1.6275q-0.945 0-1.82 0.30625t-1.61 0.88375z" fill="#fff" stroke-width=".0175"/></svg>

After

Width:  |  Height:  |  Size: 655 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6.5845 11.474-3.2932-3.2932 0.82331-0.82331 2.4699 2.4699 5.3009-5.3009 0.8233 0.82331z" fill="#fff" stroke-width=".014444"/></svg>

After

Width:  |  Height:  |  Size: 260 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m1 15v-1.5556h14v1.5556zm1.9091-2.3333q-0.525 0-0.89886-0.45694-0.37386-0.45694-0.37386-1.0986v-8.5556q0-0.64167 0.37386-1.0986 0.37386-0.45694 0.89886-0.45694h10.182q0.525 0 0.89886 0.45694 0.37386 0.45694 0.37386 1.0986v8.5556q0 0.64167-0.37386 1.0986-0.37386 0.45694-0.89886 0.45694zm0-1.5556h10.182v-8.5556h-10.182zm0 0v-8.5556z" fill="#fff" stroke-width=".017588"/></svg>

After

Width:  |  Height:  |  Size: 504 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8 1v1.5547h5.4453v10.891h-5.4453v1.5547h5.4453c0.42777-7e-6 0.79303-0.15241 1.0977-0.45703 0.30463-0.30462 0.45703-0.66988 0.45703-1.0977v-10.891c0-0.42777-0.1524-0.79303-0.45703-1.0977-0.30462-0.30463-0.66988-0.45703-1.0977-0.45703h-5.4453zm-3.1113 3.1113-3.8887 3.8887 3.8887 3.8887 1.0703-1.127-1.9844-1.9844h6.3594v-1.5547h-6.3594l1.9844-1.9844-1.0703-1.127z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8 1c-3.86 0-7 3.1403-7 7s3.14 7 7 7 7-3.1403 7-7-3.14-7-7-7zm-4.0784 3.3478h1.7655c0.26874 0 0.48684 0.21779 0.48684 0.48684v2.9828c0 0.26904-0.21779 0.48684-0.48684 0.48684h-2.2523v-0.6087h1.9479c0.10074 0 0.18249-0.08175 0.18249-0.18249v-2.3742c0-0.10074-0.08175-0.18249-0.18249-0.18249h-1.1568c-0.10074 0-0.18249 0.08175-0.18249 0.18249v1.1568c0 0.10074 0.08175 0.18249 0.18249 0.18249h0.88273v0.6087h-1.1871c-0.26874 0-0.48684-0.2178-0.48684-0.48684v-1.7655c0-0.26904 0.21779-0.48684 0.48684-0.48684zm3.1957 0h1.7655c0.26843 0 0.48684 0.2184 0.48684 0.48684v1.7655c0 0.26844-0.2184 0.48684-0.48684 0.48684h-1.7655c-0.26843 0-0.48684-0.2184-0.48684-0.48684v-1.7655c0-0.26844 0.2184-0.48684 0.48684-0.48684zm3.1957 0h1.7655c0.26874 0 0.48684 0.21779 0.48684 0.48684v2.9828c0 0.26904-0.21779 0.48684-0.48684 0.48684h-2.2523v-0.6087h1.9479c0.10074 0 0.18249-0.08175 0.18249-0.18249v-2.3742c0-0.10074-0.08175-0.18249-0.18249-0.18249h-1.1568c-0.10074 0-0.18249 0.08175-0.18249 0.18249v1.1568c0 0.10074 0.08175 0.18249 0.18249 0.18249h0.88273v0.6087h-1.1871c-0.26874 0-0.48684-0.2178-0.48684-0.48684v-1.7655c0-0.26904 0.21779-0.48684 0.48684-0.48684zm-2.8913 0.6087c-0.10074 0-0.18249 0.08175-0.18249 0.18249v1.1568c0 0.10074 0.08175 0.18249 0.18249 0.18249h1.1568c0.10074 0 0.18249-0.08175 0.18249-0.18249v-1.1568c0-0.10074-0.08175-0.18249-0.18249-0.18249zm-3.5 3.9565h1.7958v0.6087h-1.4914c-0.10074 0-0.18249 0.08175-0.18249 0.18249v1.1568c0 0.10074 0.08175 0.18249 0.18249 0.18249h1.4914v0.6087h-1.7958c-0.26874 0-0.48684-0.21779-0.48684-0.48684v-1.7655c0-0.26874 0.21779-0.48684 0.48684-0.48684zm2.7391 0h1.7655c0.26844 0 0.48684 0.2184 0.48684 0.48684v1.7655c0 0.26844-0.2184 0.48684-0.48684 0.48684h-1.7655c-0.26844 0-0.48684-0.2184-0.48684-0.48684v-1.7655c0-0.26844 0.2184-0.48684 0.48684-0.48684zm3.1957 0h2.7088v2.7391h-0.6087v-2.1304h-0.50229c-0.10074 0-0.18249 0.08175-0.18249 0.18249v1.9479h-0.6087v-2.1304h-0.50229c-0.10074 0-0.18249 0.08175-0.18249 0.18249v1.9479h-0.6087v-2.2523c0-0.26874 0.21779-0.48684 0.48684-0.48684zm-2.8913 0.6087c-0.10074 0-0.18249 0.08175-0.18249 0.18249v1.1568c0 0.10074 0.08175 0.18249 0.18249 0.18249h1.1568c0.10074 0 0.18249-0.08175 0.18249-0.18249v-1.1568c0-0.10074-0.08175-0.18249-0.18249-0.18249z" fill="#fff" stroke-width=".30435"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8 15v-1.5556h5.4444v-10.889h-5.4444v-1.5556h5.4444q0.64167 0 1.0986 0.45694 0.45694 0.45694 0.45694 1.0986v10.889q0 0.64167-0.45694 1.0986t-1.0986 0.45694zm-1.5556-3.1111-1.0694-1.1278 1.9833-1.9833h-6.3583v-1.5556h6.3583l-1.9833-1.9833 1.0694-1.1278 3.8889 3.8889z" fill="#fff" stroke-width=".019444"/></svg>

After

Width:  |  Height:  |  Size: 438 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8 15q-1.4583 0-2.7319-0.55417-1.2735-0.55417-2.2167-1.4972-0.94313-0.94306-1.4972-2.2167t-0.55417-2.7319q-7.699e-5 -1.4583 0.55417-2.7319 0.55424-1.2736 1.4972-2.2167 0.94298-0.94306 2.2167-1.4972 1.2737-0.55417 2.7319-0.55417 1.5944 0 3.0237 0.68056 1.4292 0.68056 2.4208 1.925v-1.8278h1.5556v4.6667h-4.6667v-1.5556h2.1389q-0.79722-1.0889-1.9639-1.7111-1.1667-0.62222-2.5083-0.62222-2.275 0-3.8596 1.5848-1.5846 1.5848-1.5848 3.8596-1.55e-4 2.2748 1.5848 3.8596 1.585 1.5848 3.8596 1.5848 2.0417 0 3.5681-1.3222t1.7985-3.3444h1.5944q-0.29167 2.6639-2.2848 4.443-1.9931 1.7791-4.6763 1.7792z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 741 B

View File

@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8 15q-1.4525 0-2.73-0.55125t-2.2225-1.4962-1.4962-2.2225-0.55125-2.73q0-1.47 0.55125-2.7388t1.4962-2.2138l0.98 0.98q-0.77 0.77-1.1988 1.785t-0.42875 2.1875q0 2.345 1.6275 3.9725t3.9725 1.6275 3.9725-1.6275 1.6275-3.9725q0-1.1725-0.42875-2.1875t-1.1988-1.785l0.98-0.98q0.945 0.945 1.4962 2.2138t0.55125 2.7388q0 1.4525-0.55125 2.73t-1.4962 2.2225-2.2225 1.4962-2.73 0.55125zm-0.7-6.3v-7.7h1.4v7.7z" fill="#fff" stroke-width=".0175"/></svg>

After

Width:  |  Height:  |  Size: 567 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -8,13 +8,79 @@ current_theme_name = read_theme_from_config()
favoriteLabelSize = 48, 48 favoriteLabelSize = 48, 48
pixmapsScaledSize = 60, 60 pixmapsScaledSize = 60, 60
GAME_CARD_ANIMATION = {
# Ширина обводки карточки в состоянии покоя (без наведения или фокуса).
# Влияет на толщину рамки вокруг карточки, когда она не выделена.
# Значение в пикселях.
"default_border_width": 2,
# Ширина обводки при наведении курсора.
# Увеличивает толщину рамки, когда курсор находится над карточкой.
# Значение в пикселях.
"hover_border_width": 8,
# Ширина обводки при фокусе (например, при выборе с клавиатуры).
# Увеличивает толщину рамки, когда карточка в фокусе.
# Значение в пикселях.
"focus_border_width": 12,
# Минимальная ширина обводки во время пульсирующей анимации.
# Определяет минимальную толщину рамки при пульсации (анимация "дыхания").
# Значение в пикселях.
"pulse_min_border_width": 8,
# Максимальная ширина обводки во время пульсирующей анимации.
# Определяет максимальную толщину рамки при пульсации.
# Значение в пикселях.
"pulse_max_border_width": 10,
# Длительность анимации изменения толщины обводки (например, при наведении или фокусе).
# Влияет на скорость перехода от одной ширины обводки к другой.
# Значение в миллисекундах.
"thickness_anim_duration": 300,
# Длительность одного цикла пульсирующей анимации.
# Определяет, как быстро рамка "пульсирует" между min и max значениями.
# Значение в миллисекундах.
"pulse_anim_duration": 800,
# Длительность анимации вращения градиента.
# Влияет на скорость, с которой градиентная обводка вращается вокруг карточки.
# Значение в миллисекундах.
"gradient_anim_duration": 3000,
# Начальный угол градиента (в градусах).
# Определяет начальную точку вращения градиента при старте анимации.
"gradient_start_angle": 360,
# Конечный угол градиента (в градусах).
# Определяет конечную точку вращения градиента.
# Значение 0 означает полный поворот на 360 градусов.
"gradient_end_angle": 0,
# Тип кривой сглаживания для анимации увеличения обводки (при наведении/фокусе).
# Влияет на "чувство" анимации (например, плавное ускорение или замедление).
# Возможные значения: строки, соответствующие QEasingCurve.Type (например, "OutBack", "InOutQuad").
"thickness_easing_curve": "OutBack",
# Тип кривой сглаживания для анимации уменьшения обводки (при уходе курсора/потере фокуса).
# Влияет на "чувство" возврата к исходной ширине обводки.
"thickness_easing_curve_out": "InBack",
# Цвета градиента для анимированной обводки.
# Список словарей, где каждый словарь задает позицию (0.01.0) и цвет в формате hex.
# Влияет на внешний вид обводки при наведении или фокусе.
"gradient_colors": [
{"position": 0, "color": "#00fff5"}, # Начальный цвет (циан)
{"position": 0.33, "color": "#FF5733"}, # Цвет на 33% (оранжевый)
{"position": 0.66, "color": "#9B59B6"}, # Цвет на 66% (пурпурный)
{"position": 1, "color": "#00fff5"} # Конечный цвет (возвращение к циану)
]
}
CONTEXT_MENU_STYLE = """ CONTEXT_MENU_STYLE = """
QMenu { QMenu {
background: qlineargradient(x1:0, y1:0, x2:1, y2:0, background: #282a33;;
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; color: #ffffff;
font-family: 'Play'; font-family: 'Play';
font-size: 16px; font-size: 16px;
@ -27,12 +93,12 @@ CONTEXT_MENU_STYLE = """
color: #ffffff; color: #ffffff;
} }
QMenu::item:selected { QMenu::item:selected {
background: #282a33; background: #409EFF;
color: #09bec8; color: #ffffff;
} }
QMenu::item:hover { QMenu::item:hover {
background: #282a33; background: #409EFF;
color: #09bec8; color: #ffffff;
} }
QMenu::item:focus { QMenu::item:focus {
background: #409EFF; background: #409EFF;
@ -67,7 +133,7 @@ TITLE_LABEL_STYLE = """
# СТИЛЬ ОБЛАСТИ НАВИГАЦИИ (КНОПКИ ВКЛАДОК) # СТИЛЬ ОБЛАСТИ НАВИГАЦИИ (КНОПКИ ВКЛАДОК)
NAV_WIDGET_STYLE = """ NAV_WIDGET_STYLE = """
QWidget { QWidget {
background: none; background: #282a33;
border: 0px solid; border: 0px solid;
} }
""" """
@ -82,19 +148,19 @@ NAV_BUTTON_STYLE = """
font-family: 'Play'; font-family: 'Play';
font-size: 16px; font-size: 16px;
text-transform: uppercase; text-transform: uppercase;
border: none; border: #409EFF;
border-radius: 15px; border-radius: 15px;
} }
NavLabel[checked = true] { NavLabel[checked = true] {
background: rgba(0,122,255,0); background: rgba(0,122,255,0);
color: #09bec8; color: #409EFF;
font-weight: normal; font-weight: normal;
text-decoration: underline; text-decoration: underline;
border-radius: 15px; border-radius: 15px;
} }
NavLabel:hover { NavLabel:hover {
background: none; background: none;
color: #09bec8; color: #409EFF;
} }
""" """
@ -120,7 +186,7 @@ SEARCH_EDIT_STYLE = """
color: #ffffff; color: #ffffff;
} }
QLineEdit:focus { QLineEdit:focus {
border: 1px solid #09bec8; border: 1px solid #409EFF;
} }
""" """
@ -228,7 +294,7 @@ INSTALLED_TAB_TITLE_STYLE = "font-family: 'Play'; font-size: 24px; color: #fffff
ACTION_BUTTON_STYLE = """ ACTION_BUTTON_STYLE = """
QPushButton { QPushButton {
background: #3f424d; background: #3f424d;
border: 1px solid rgba(255, 255, 255, 0.20); border: 2px solid rgba(255, 255, 255, 0.01);
border-radius: 10px; border-radius: 10px;
color: #ffffff; color: #ffffff;
font-size: 16px; font-size: 16px;
@ -236,36 +302,40 @@ ACTION_BUTTON_STYLE = """
padding: 8px 16px; padding: 8px 16px;
} }
QPushButton:hover { QPushButton:hover {
background: #282a33; background: #409EFF;
border: 2px solid #409EFF;
} }
QPushButton:pressed { QPushButton:pressed {
background: #282a33; background: #282a33;
} }
QPushButton:focus { QPushButton:focus {
border: 2px solid #409EFF; border: 2px solid #409EFF;
background-color: #404554; background-color: #409EFF;
} }
""" """
# СТИЛЬ КНОПОК ОВЕРЛЕЯ # СТИЛЬ ОВЕРЛЕЯ
OVERLAY_WINDOW_STYLE = "background: #282a33;"
OVERLAY_BUTTON_STYLE = """ OVERLAY_BUTTON_STYLE = """
QPushButton { QPushButton {
background: #3f424d; background: #3f424d;
border: 1px solid rgba(255, 255, 255, 0.20); border: 2px solid rgba(255, 255, 255, 0.01);
border-radius: 10px; border-radius: 10px;
color: #ffffff; color: #ffffff;
font-size: 16px; font-size: 16px;
font-family: 'Play'; font-family: 'Play';
padding: 8px 16px;
} }
QPushButton:hover { QPushButton:hover {
background: #282a33; background: #409EFF;
border: 2px solid #409EFF;
} }
QPushButton:pressed { QPushButton:pressed {
background: #282a33; background: #282a33;
} }
QPushButton:focus { QPushButton:focus {
border: 2px solid #409EFF; border: 2px solid #409EFF;
background-color: #404554; background-color: #409EFF;
} }
""" """
@ -331,10 +401,10 @@ ADDGAME_BACK_BUTTON_STYLE = """
padding: 8px 16px; padding: 8px 16px;
} }
QPushButton:hover { QPushButton:hover {
background: #09bec8; background: #409EFF;
} }
QPushButton:pressed { QPushButton:pressed {
background: #09bec8; background: #409EFF;
} }
""" """
@ -388,10 +458,10 @@ PLAY_BUTTON_STYLE = """
min-height: 40px; min-height: 40px;
} }
QPushButton:hover { QPushButton:hover {
background: #09bec8; background: #409EFF;
} }
QPushButton:pressed { QPushButton:pressed {
background: #09bec8; background: #409EFF;
} }
""" """
@ -416,6 +486,40 @@ DIALOG_BROWSE_BUTTON_STYLE = """
} }
""" """
ADDGAME_INPUT_STYLE = """
QLineEdit {
background: #3f424d;
border: 2px solid rgba(255, 255, 255, 0.01);
border-radius: 10px;
height: 34px;
padding-left: 12px;
color: #ffffff;
font-family: 'Play';
font-size: 16px;
}
QLineEdit:hover {
background: #3f424d;
border: 2px solid #409EFF;
}
QLineEdit:focus {
border: 2px solid #409EFF;
background-color: #404554;
}
QMenu {
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 5px 10px;
background: #32343d;
}
QMenu::item {
padding: 0px 10px;
border: 10px solid transparent; /* reserve space for selection border */
}
QMenu::item:selected {
background: #3f424d;
border-radius: 10px;
}
"""
# СТИЛЬ КАРТОЧКИ ИГРЫ (GAMECARD) # СТИЛЬ КАРТОЧКИ ИГРЫ (GAMECARD)
GAME_CARD_WINDOW_STYLE = """ GAME_CARD_WINDOW_STYLE = """
QFrame { QFrame {
@ -478,6 +582,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;
@ -532,7 +657,7 @@ PARAMS_TITLE_STYLE = "color: #ffffff; font-family: 'Play'; font-size: 16px; padd
PROXY_INPUT_STYLE = """ PROXY_INPUT_STYLE = """
QLineEdit { QLineEdit {
background: #282a33; background: #282a33;
border: 0px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.01);
border-radius: 10px; border-radius: 10px;
height: 34px; height: 34px;
padding-left: 12px; padding-left: 12px;
@ -540,8 +665,13 @@ PROXY_INPUT_STYLE = """
font-family: 'Play'; font-family: 'Play';
font-size: 16px; font-size: 16px;
} }
QLineEdit:hover {
background: #3f424d;
border: 2px solid #409EFF;
}
QLineEdit:focus { QLineEdit:focus {
border: 1px solid rgba(255, 255, 255, 0.2); border: 2px solid #409EFF;
background-color: #404554;
} }
QMenu { QMenu {
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
@ -561,7 +691,7 @@ PROXY_INPUT_STYLE = """
SETTINGS_COMBO_STYLE = f""" SETTINGS_COMBO_STYLE = f"""
QComboBox {{ QComboBox {{
background: #3f424d; background: #3f424d;
border: 0px solid rgba(255, 255, 255, 0.2); border: 2px solid rgba(255, 255, 255, 0.01);
border-radius: 10px; border-radius: 10px;
height: 34px; height: 34px;
padding-left: 12px; padding-left: 12px;
@ -573,19 +703,20 @@ SETTINGS_COMBO_STYLE = f"""
}} }}
QComboBox:on {{ QComboBox:on {{
background: #373a43; background: #373a43;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid #409EFF;
border-top-left-radius: 10px; border-top-left-radius: 10px;
border-top-right-radius: 10px; border-top-right-radius: 10px;
border-bottom-left-radius: 0px; border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px; border-bottom-right-radius: 0px;
}} }}
QComboBox:hover {{ QComboBox:hover {{
border: 1px solid rgba(255, 255, 255, 0.2); border: 2px solid #409EFF;
background: #409EFF;
}} }}
/* Состояние фокуса */ /* Состояние фокуса */
QComboBox:focus {{ QComboBox:focus {{
border: 2px solid #409EFF; border: 2px solid #409EFF;
background-color: #404554; background-color: #409EFF;
}} }}
QComboBox::drop-down {{ QComboBox::drop-down {{
subcontrol-origin: padding; subcontrol-origin: padding;
@ -610,7 +741,7 @@ SETTINGS_COMBO_STYLE = f"""
/* Список при открытом комбобоксе */ /* Список при открытом комбобоксе */
QComboBox QAbstractItemView {{ QComboBox QAbstractItemView {{
outline: none; outline: none;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid #409EFF;
border-top-style: none; border-top-style: none;
}} }}
QListView {{ QListView {{
@ -634,6 +765,30 @@ SETTINGS_COMBO_STYLE = f"""
}} }}
""" """
SETTINGS_CHECKBOX_STYLE = f"""
QCheckBox {{
height: 34px;
}}
QCheckBox::indicator {{
width: 24px;
height: 24px;
border: 2px solid rgba(255, 255, 255, 0.01);
border-radius: 10px;
background: #282a33;
}}
QCheckBox::indicator:hover {{
background: #3f424d;
border: 2px solid #409EFF;
}}
QCheckBox::indicator:focus {{
border: 2px solid #409EFF;
}}
QCheckBox::indicator:checked {{
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
border: 2px solid #409EFF;
}}
"""
# ФУНКЦИЯ ДЛЯ ДИНАМИЧЕСКОГО ГРАДИЕНТА (ДЕТАЛИ ИГР) # ФУНКЦИЯ ДЛЯ ДИНАМИЧЕСКОГО ГРАДИЕНТА (ДЕТАЛИ ИГР)
# Функции из этой темы срабатывает всегда вне зависимости от выбранной темы, функции из других тем работают только в этих темах # Функции из этой темы срабатывает всегда вне зависимости от выбранной темы, функции из других тем работают только в этих темах

View File

@ -7,12 +7,13 @@ from portprotonqt.config_utils import read_theme_from_config
class SystemTray: class SystemTray:
def __init__(self, app, theme=None): def __init__(self, app, theme=None):
self.app = app
self.theme_manager = ThemeManager() self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else default_styles self.theme = theme if theme is not None else default_styles
self.current_theme_name = read_theme_from_config() self.current_theme_name = read_theme_from_config()
self.tray = QSystemTrayIcon() self.tray = QSystemTrayIcon()
self.tray.setIcon(cast(QIcon, self.theme_manager.get_icon("ppqt-tray", self.current_theme_name))) self.tray.setIcon(cast(QIcon, self.theme_manager.get_icon("ppqt-tray", self.current_theme_name)))
self.tray.setToolTip("PortProton QT") self.tray.setToolTip("PortProtonQt")
self.tray.setVisible(True) self.tray.setVisible(True)
# Создаём меню # Создаём меню
@ -32,4 +33,17 @@ class SystemTray:
def hide_tray(self): def hide_tray(self):
"""Скрыть иконку трея""" """Скрыть иконку трея"""
self.tray.hide() if self.tray:
self.tray.setVisible(False)
if self.menu:
self.menu.deleteLater()
self.menu = None
def cleanup(self):
"""Очистка ресурсов трея"""
if self.tray:
self.tray.setVisible(False)
self.tray = None
if self.menu:
self.menu.deleteLater()
self.menu = None

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "portprotonqt" name = "portprotonqt"
version = "0.1.1" version = "0.1.2"
description = "A project to rewrite PortProton (PortWINE) using PySide" description = "A project to rewrite PortProton (PortWINE) using PySide"
readme = "README.md" readme = "README.md"
license = { text = "GPL-3.0" } license = { text = "GPL-3.0" }

26
renovate.json Normal file
View File

@ -0,0 +1,26 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:best-practices"],
"rebaseWhen": "never",
"lockFileMaintenance": {
"enabled": true
},
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
},
{
"automerge": true,
"matchUpdateTypes": ["pin", "pinDigest"]
},
{
"enabled": false,
"matchFileNames": [".gitea/workflows/**.yaml", ".gitea/workflows/**.yml"]
},
{
"enabled": false,
"matchFileNames": [".python-version"]
}
]
}

795
uv.lock generated

File diff suppressed because it is too large Load Diff