88 Commits

Author SHA1 Message Date
464ad0fe9c chore: optimize and clean code
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-17 13:09:02 +05:00
cde92885d4 feat(virtual_keybord): added gamepad hint
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-17 00:04:47 +05:00
120c7b319c fix: improve gamepad detection using udev ID_INPUT_JOYSTICK property 2025-10-16 23:20:48 +05:00
596aed0077 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 14:54:30 +05:00
6fc6cb1e02 feat: added minimize to tray
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 14:53:08 +05:00
186e28a19b fix(gamepad): resolve MonitorObserver blocking issue causing application hang
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 14:43:49 +05:00
28e4d1e77c Revert "chore: broke autorelease for tasting purpose"
This reverts commit fff1f888c4.
2025-10-16 14:11:36 +05:00
fff1f888c4 chore: broke autorelease for tasting purpose
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 12:48:21 +05:00
fdd5a0a3d5 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 10:44:30 +05:00
792e52d981 feat(dialogs): added controller hints
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 10:39:24 +05:00
84d5e46a74 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 22:53:08 +05:00
4bc764d568 partially revert b1047ba18e
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 22:31:35 +05:00
9a18aa037e feat(autoinstall): no restart on autoinstall finished
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 21:58:40 +05:00
ed62d2d1c4 fix: resolve lambda variable capture issue in switchTab method
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 21:47:14 +05:00
accc9b18b6 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 15:31:56 +05:00
82249d7eab feat(settings): Added Gamepad type settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 15:30:31 +05:00
476c896940 chore(TODO): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 12:44:01 +05:00
b1047ba18e fix: fix card overlap on display_filter change
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-13 12:14:54 +05:00
987199d8e6 chore(release): enable node experimental-fetch
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-13 11:52:43 +05:00
Renovate Bot
ef1acd4581 chore(deps): update archlinux:base-devel docker digest to 06ab929 2025-10-12 17:46:27 +00:00
96f884904c chore: bump ver to v0.1.7
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:33:56 +05:00
b856a2afae chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:33:12 +05:00
55ef0030e6 feat: added version and commit on WindowTitle
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:31:23 +05:00
8aaeaa4824 chore(localization): add localization for auto-install progress status message
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:14:06 +05:00
f55372b480 fix(autoinstall): fix scrollbar sticking to the right edge
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:10:44 +05:00
4d6f32f053 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 15:25:04 +05:00
a2f5141b20 chore localization update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 15:21:14 +05:00
e3cb2857e7 fix(pyright): fix pyright errors
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 15:14:02 +05:00
efe8a35832 feat(autoinstall): rework gamepad navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 14:57:43 +05:00
61fae97dad fix(autoinstall): fix virtual keyboard open
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 14:45:52 +05:00
5442100f64 feat: use GameCard on autonstall tab
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 13:56:18 +05:00
2d6ef84798 chore: rename metadata to use pw_create_unique_exe
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 12:14:31 +05:00
Renovate Bot
f4aee15b5d chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.0 2025-10-12 00:01:35 +00:00
87a65108a5 feat(autoinstall): added covers
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 00:48:09 +05:00
bb617708ac feat: initial add of autoinstall tab
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-11 19:19:47 +05:00
1cf332cd87 feat(winetab): added progress bar
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-11 13:24:58 +05:00
577ad4d3a3 feat: adapt WineTab to new cli
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-10 23:07:48 +05:00
ef3f2d6e96 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 21:01:30 +05:00
657d7728a6 fix(gamepad): exit fullscreen on disconnect only if auto-fullscreen enabled and fullscreen disabled
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 20:59:51 +05:00
9452bfda2e chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
7eb2db0d68 chore localization update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
6ef7a03366 feat: added search to controller hints
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
e5af354b56 fix(virtual-keyboard): turn off caps lock when disabling shift while caps is enabled
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
e6e5f6c8ea feat(virtual_keyboard): make keyboard bigger
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
84306bb31b feat(virtual_keyboard): added dpad reapeat movement
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
60af4d1482 feat(virtual_keyboard): press X to backspace
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
692e11b21d chore(virtual_keyboard): move styles to style.py
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
b1a804811e chore(keyboard): drop connect_keyboard_to_lineedit
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
9a30cfaea7 chore(keyboard): drop unneded key events
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
5dd2f71f5e feat: added virtual keyboard
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 11:41:53 +00:00
dba172361b fix(ui): resolve layout issues during search filtering
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 12:52:34 +05:00
a9c70b8818 chore(winetricks): use curl for download
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 08:59:03 +05:00
135ace732f chore(deps): added Winetricks deps copied from upstream control
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 08:47:22 +05:00
8b727f64e1 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-08 19:26:21 +05:00
a8eb591da5 fix: update ControlHints and NavButtons together
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-08 19:23:58 +05:00
fe4ca1ee87 fix: revert signals to pyside 6.9.1
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-08 19:12:37 +05:00
ffe3e9d3d6 chore(deps): revert Pyside6 to 6.9.1
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-08 19:04:45 +05:00
49d39b5d61 chore(pyright): fix code for new version
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-08 18:37:31 +05:00
Renovate Bot
03566da704 fix(deps): lock file maintenance python dependencies 2025-10-08 18:21:53 +05:00
Renovate Bot
7f996ab6a0 chore(deps): update archlinux:base-devel docker digest to b380991 2025-10-08 12:09:42 +00:00
Renovate Bot
9e17978155 chore(deps): update ghcr.io/renovatebot/renovate:latest docker digest to 17c8966 2025-10-08 12:05:09 +00:00
5d0185b1b4 feat(winetricks): added preloader to tabble
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-08 16:41:32 +05:00
5c134be04e chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-07 15:54:05 +05:00
8c66695192 chore(winetricks): fix typo on translate and added forget icon
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-07 15:49:48 +05:00
7a141d8e46 fix(winetricks): resolve QProcess channel mode warning in install handler
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-07 15:43:16 +05:00
abb2377fb7 fix(winetricks): remove duplicate entries in winetricks.log
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-07 15:37:41 +05:00
75f4f346de chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-07 15:27:31 +05:00
87a9f85272 feat(wine settings): make winetricks work with gamepad
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-07 15:18:48 +05:00
240f685ece feat(wine settings): make winetricks work
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-07 12:06:35 +05:00
af4e3e95bb chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-06 17:57:52 +05:00
017d9a42cf feat(wine settings): make prefix and wine delete work
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-06 17:55:24 +05:00
18b7c4054b chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-06 17:47:45 +05:00
dd7f71b70a feat(wine settings): make pfx_backup work
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-06 17:29:06 +05:00
8fd44c575b fix: expose gamesListWidget from GameLibraryManager to fix gamepad navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-06 13:21:58 +05:00
65b43c1572 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-06 00:04:55 +05:00
f35276abfe fix: reject candidate if normalized name equals "game"
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-06 00:02:06 +05:00
6fea9a9a7e chore(wine settings): rework layout
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-05 20:01:00 +05:00
5189474631 feat(wine settings): initial introdouce
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-05 16:19:06 +05:00
Renovate Bot
416cc6a268 chore(deps): update archlinux:base-devel docker digest to 5d95edc 2025-10-05 08:20:07 +00:00
Renovate Bot
3b44ed5252 chore(deps): update ghcr.io/renovatebot/renovate:latest docker digest to e459af1 2025-10-05 00:01:07 +00:00
c8c45dda06 chore(readme): drop Those Awesome Guys
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-04 20:54:57 +05:00
3f9f794e6f hint icons 2025-10-04 22:12:10 +07:00
ba9d8b76d8 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-02 16:31:01 +05:00
e99c71c1f8 feat: optimize search
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-02 16:29:18 +05:00
baec62d1cb chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-01 11:19:52 +05:00
cb76961e4f feat: optimize add and remove game
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-01 11:19:37 +05:00
Gitea Actions
081cd07253 chore: update steam apps list 2025-10-01T00:01:41Z 2025-10-01 00:01:42 +00:00
b5efee29ea chore: cleanup MainWindow class
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-09-30 15:43:11 +05:00
104 changed files with 17639 additions and 2042 deletions

View File

@@ -94,7 +94,7 @@ jobs:
name: Build Arch Package name: Build Arch Package
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: container:
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64 image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
volumes: volumes:
- /usr:/usr-host - /usr:/usr-host
- /opt:/opt-host - /opt:/opt-host

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: 0.1.6 VERSION: 0.1.7
PKGDEST: "/tmp/portprotonqt" PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt" PACKAGE: "portprotonqt"
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
@@ -180,6 +180,8 @@ jobs:
- name: Release - name: Release
uses: https://gitea.com/actions/gitea-release-action@v1 uses: https://gitea.com/actions/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with: with:
body_path: changelog.txt body_path: changelog.txt
token: ${{ env.GITEA_TOKEN }} token: ${{ env.GITEA_TOKEN }}

View File

@@ -138,7 +138,7 @@ jobs:
needs: changes needs: changes
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch' if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
container: container:
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64 image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
volumes: volumes:
- /usr:/usr-host - /usr:/usr-host
- /opt:/opt-host - /opt:/opt-host

View File

@@ -8,7 +8,7 @@ on:
jobs: jobs:
renovate: renovate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:latest@sha256:dd5721b9a686a40d81687643e4b71b82a0ca31fb653fd727538af69104fd388d container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
steps: steps:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 - uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5

View File

@@ -16,7 +16,7 @@ repos:
- id: uv-lock - id: uv-lock
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.2 rev: v0.14.0
hooks: hooks:
- id: ruff-check - id: ruff-check

View File

@@ -6,13 +6,48 @@
## [Unreleased] ## [Unreleased]
### Added ### Added
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
### Changed ### Changed
- При завершении автоустановки приложение больше не перезапускается
### Fixed
- Исправлено наложение карточек при смене фильтра игр
### Contributors
- @Vector_null
---
## [0.1.7] - 2025-10-12
### Added
- Возможность скроллинга библиотеки мышью или пальцем
- Импорт и экспорт бекапа префикса
- Диалог для управление Winetricks
- Кнопки для удаления префикса, wine или proton
- Все настройки Wine с оригинального PortProton
- Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке и автоустановках
- Вкладка автоустановок
- В заголовке окна теперь отображается версия приложения и хеш коммита если запуск идёт с гита
### Changed
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
- В диалог выбора файлов в режиме directory_only (при выборе куда сохранить бекап префикса) добавлена кнопка ./ обозначающая нынешнюю папку
### Fixed ### Fixed
- Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений - Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений
- Исправлено зависание при добавлении или удалении игры в Wayland
- Исправлено зависание при поиске игр
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
- Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада
- Исправлен выход из полноэкранного режима при отключении геймпада подключённого по USB даже если настройка "Режим полноэкранного отображения приложения при подключении геймпада" выключена
- При сохранении настроек теперь не меняется размер окна
### Contributors ### Contributors
- @wmigor (Igor Akulov)
- @Vector_null
--- ---
@@ -29,12 +64,13 @@
### Changed ### Changed
- Управления с геймпада теперь перехватывается только если окно в фокусе - Управления с геймпада теперь перехватывается только если окно в фокусе
### Fixed ### Fixed
- Исправлена проблема с устаревшими кэш-файлами, вызывающими несоответствия при обновлении JSON - Исправлена проблема с устаревшими кэш-файлами, вызывающими несоответствия при обновлении JSON
- Исправлено переключение в полноэкранный режим при нажатии кнопки "Select во время запущенной игры - Исправлено переключение в полноэкранный режим при нажатии кнопки "Select во время запущенной игры
### Contributors ### Contributors
- @wmigor (Igor Akulov)
- @Vector_null
--- ---

View File

@@ -54,8 +54,6 @@ PortProtonQt использует код и зависимости от след
- [Legendary](https://github.com/derrod/legendary) — инструмент для работы с Epic Games Store, лицензия [GPL-3.0](https://github.com/derrod/legendary/blob/master/LICENSE). - [Legendary](https://github.com/derrod/legendary) — инструмент для работы с Epic Games Store, лицензия [GPL-3.0](https://github.com/derrod/legendary/blob/master/LICENSE).
- [Icoextract](https://github.com/jlu5/icoextract) — библиотека для извлечения иконок, лицензия [MIT](https://github.com/jlu5/icoextract/blob/master/LICENSE). - [Icoextract](https://github.com/jlu5/icoextract) — библиотека для извлечения иконок, лицензия [MIT](https://github.com/jlu5/icoextract/blob/master/LICENSE).
- [HowLongToBeat Python API](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI) — библиотека для взаимодействия с HowLongToBeat, лицензия [MIT](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI/blob/master/LICENSE.md). - [HowLongToBeat Python API](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI) — библиотека для взаимодействия с HowLongToBeat, лицензия [MIT](https://github.com/ScrappyCocco/HowLongToBeat-PythonAPI/blob/master/LICENSE.md).
- [Those Awesome Guys: Gamepad prompts images](https://thoseawesomeguys.com/prompts/) - Набор подсказок для геймпада и клавиатур, лицензия [CC0](https://creativecommons.org/public-domain/cc0/)
Полный текст лицензий см. в файле [LICENSE](LICENSE). Полный текст лицензий см. в файле [LICENSE](LICENSE).
> [!WARNING] > [!WARNING]

15
TODO.md
View File

@@ -1,6 +1,6 @@
- [X] Адаптировать структуру проекта для поддержки инструментов сборки - [X] Адаптировать структуру проекта для поддержки инструментов сборки
- [X] Добавить возможность управления с геймпада - [X] Добавить возможность управления с геймпада
- [ ] Добавить возможность управления с тачскрина - [X] Добавить возможность управления с тачскрина (Формально и так есть)
- [X] Добавить возможность управления с мыши и клавиатуры - [X] Добавить возможность управления с мыши и клавиатуры
- [X] Добавить систему тем [Документация](documentation/theme_guide) - [X] Добавить систему тем [Документация](documentation/theme_guide)
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено) - [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
@@ -11,18 +11,18 @@
- [ ] Разработать адаптивный дизайн (за эталон берётся 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
- [X] Получать обложки для игр из SteamGridDB или CDN Steam - [X] Получать обложки для игр из CDN Steam
- [X] Оптимизировать работу со Steam API для ускорения времени запуска - [X] Оптимизировать работу со Steam API для ускорения времени запуска
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley) - [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода - [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки) - [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
- [X] Избавиться от вызовов yad - [X] Избавиться от вызовов yad
- [X] Реализовать собственный системный трей вместо использования трея PortProton - [X] Реализовать собственный системный трей вместо использования трея PortProton
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.) - [X] Добавить экранную клавиатуру в поиск
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту) - [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
- [X] Добавить индикацию запуска приложения - [X] Добавить индикацию запуска приложения
- [X] Достигнуть паритета функциональности с Ingame - [X] Достигнуть паритета функциональности с Ingame
- [ ] Достигнуть паритета функциональности с PortProton - [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов)
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}` - [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/) - [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
- [X] Добавить переводы в переопределения - [X] Добавить переводы в переопределения
@@ -49,7 +49,7 @@
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter) - [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
- [X] Добавить систему избранного для карточек - [X] Добавить систему избранного для карточек
- [X] Заменить все `print` на `logging` - [X] Заменить все `print` на `logging`
- [ ] Привести все логи к единому языку - [X] Привести все логи к единому языку
- [X] Уменьшить количество подстановок в переводах - [X] Уменьшить количество подстановок в переводах
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog) - [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py` - [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
@@ -62,7 +62,6 @@
- [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))
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры - [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
- [ ] Доделать светлую тему - [X] Добавить подсказки к управлению с геймпада
- [ ] Добавить подсказки к управлению с геймпада
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры - [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры - [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры

View File

@@ -36,7 +36,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.6 version: 0.1.7
exec: usr/bin/python3 exec: usr/bin/python3
exec_args: "-m portprotonqt.app $@" exec_args: "-m portprotonqt.app $@"
apt: apt:
@@ -54,6 +54,11 @@ AppDir:
- libxcb-cursor0 - libxcb-cursor0
- libimage-exiftool-perl - libimage-exiftool-perl
- xdg-utils - xdg-utils
- cabextract
- curl
- 7zip
- unzip
- unrar
exclude: exclude:
- "*-doc" - "*-doc"
- "*-man" - "*-man"

View File

@@ -1,12 +1,12 @@
pkgname=portprotonqt pkgname=portprotonqt
pkgver=0.1.6 pkgver=0.1.7
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')
url="https://git.linux-gaming.ru/Boria138/PortProtonQt" url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
license=('GPL-3.0') license=('GPL-3.0')
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client') 'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
makedepends=('python-'{'build','installer','setuptools','wheel'}) makedepends=('python-'{'build','installer','setuptools','wheel'})
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver") source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
sha256sums=('SKIP') sha256sums=('SKIP')

View File

@@ -6,7 +6,7 @@ arch=('any')
url="https://git.linux-gaming.ru/Boria138/PortProtonQt" url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
license=('GPL-3.0') license=('GPL-3.0')
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client') 'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
makedepends=('python-'{'build','installer','setuptools','wheel'}) makedepends=('python-'{'build','installer','setuptools','wheel'})
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git") source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
sha256sums=('SKIP') sha256sums=('SKIP')

View File

@@ -46,6 +46,11 @@ Requires: python3-pillow
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils
Requires: python3-beautifulsoup4 Requires: python3-beautifulsoup4
Requires: cabextract
Requires: gzip
Requires: unzip
Requires: curl
Requires: unrar
%description -n python3-%{pypi_name}-git %description -n python3-%{pypi_name}-git
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup. This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.

View File

@@ -1,5 +1,5 @@
%global pypi_name portprotonqt %global pypi_name portprotonqt
%global pypi_version 0.1.6 %global pypi_version 0.1.7
%global oname PortProtonQt %global oname PortProtonQt
%global _python_no_extras_requires 1 %global _python_no_extras_requires 1
@@ -43,6 +43,11 @@ Requires: python3-pillow
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils
Requires: python3-beautifulsoup4 Requires: python3-beautifulsoup4
Requires: cabextract
Requires: gzip
Requires: unzip
Requires: curl
Requires: unrar
%description -n python3-%{pypi_name} %description -n python3-%{pypi_name}
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup. This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.

View File

@@ -217,7 +217,7 @@
}, },
{ {
"normalized_name": "watch_dogs 2", "normalized_name": "watch_dogs 2",
"status": "Broken" "status": "Running"
}, },
{ {
"normalized_name": "zero hour", "normalized_name": "zero hour",

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,96 @@
[ [
{
"normalized_title": "dirt rally 2.0 game of the year",
"slug": "dirt-rally-2-0-game-of-the-year-edition"
},
{
"normalized_title": "deus ex human revolution directors cut",
"slug": "deus-ex-human-revolution-director-s-cut"
},
{
"normalized_title": "freelancer",
"slug": "freelancer"
},
{
"normalized_title": "everspace",
"slug": "everspace"
},
{
"normalized_title": "blades of time limited",
"slug": "blades-of-time-limited-edition"
},
{
"normalized_title": "chorus",
"slug": "chorus"
},
{
"normalized_title": "tom clancy's splinter cell pandora tomorrow",
"slug": "tom-clancys-splinter-cell-pandora-tomorrow"
},
{
"normalized_title": "the alters",
"slug": "the-alters"
},
{
"normalized_title": "hard reset redux",
"slug": "hard-reset-redux"
},
{
"normalized_title": "far cry 5",
"slug": "far-cry-5"
},
{
"normalized_title": "metal eden",
"slug": "metal-eden"
},
{
"normalized_title": "indiana jones and the great circle",
"slug": "indiana-jones-and-the-great-circle"
},
{
"normalized_title": "old world",
"slug": "old-world"
},
{
"normalized_title": "witchfire",
"slug": "witchfire"
},
{
"normalized_title": "prototype",
"slug": "prototype"
},
{
"normalized_title": "mandragora whispers of the witch tree",
"slug": "mandragora-whispers-of-the-witch-tree"
},
{
"normalized_title": "grand theft auto v (gta 5)",
"slug": "grand-theft-auto-v-gta-5"
},
{
"normalized_title": "lifeless planet premier",
"slug": "lifeless-planet-premier-edition"
},
{
"normalized_title": "warcraft iii the frozen throne",
"slug": "warcraft-iii-the-frozen-throne"
},
{
"normalized_title": "star wars republic commando",
"slug": "star-wars-republic-commando"
},
{
"normalized_title": "hollow knight silksong",
"slug": "hollow-knight-silksong"
},
{
"normalized_title": "arma reforger",
"slug": "arma-reforger"
},
{
"normalized_title": "arma 3",
"slug": "arma-3"
},
{ {
"normalized_title": "astroneer", "normalized_title": "astroneer",
"slug": "astroneer" "slug": "astroneer"
@@ -195,10 +287,6 @@
"normalized_title": "slitterhead", "normalized_title": "slitterhead",
"slug": "slitterhead" "slug": "slitterhead"
}, },
{
"normalized_title": "indiana jones and the great circle",
"slug": "indiana-jones-and-the-great-circle"
},
{ {
"normalized_title": "crossout", "normalized_title": "crossout",
"slug": "crossout" "slug": "crossout"

Binary file not shown.

View File

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

View File

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

View File

@@ -1,17 +1,41 @@
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
from portprotonqt.main_window import MainWindow from portprotonqt.main_window import MainWindow
from portprotonqt.config_utils import save_fullscreen_config from portprotonqt.config_utils import save_fullscreen_config, get_portproton_location
from portprotonqt.logger import get_logger, setup_logger from portprotonqt.logger import get_logger, setup_logger
from portprotonqt.cli import parse_args from portprotonqt.cli import parse_args
__app_id__ = "ru.linux_gaming.PortProtonQt" __app_id__ = "ru.linux_gaming.PortProtonQt"
__app_name__ = "PortProtonQt" __app_name__ = "PortProtonQt"
__app_version__ = "0.1.6" __app_version__ = "0.1.7"
def get_version():
try:
commit = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'],
stderr=subprocess.DEVNULL
).decode('utf-8').strip()
return f"{__app_version__} ({commit})"
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return __app_version__
def main(): def main():
os.environ['PW_CLI'] = '1'
os.environ['PROCESS_LOG'] = '1'
os.environ['START_FROM_STEAM'] = '1'
portproton_path = get_portproton_location()
if portproton_path is None:
return
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
subprocess.run([script_path, 'cli', '--initial'])
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setWindowIcon(QIcon.fromTheme(__app_id__)) app.setWindowIcon(QIcon.fromTheme(__app_id__))
app.setDesktopFileName(__app_id__) app.setDesktopFileName(__app_id__)
@@ -34,7 +58,8 @@ def main():
else: else:
logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language") logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language")
window = MainWindow(app_name=__app_name__) version = get_version()
window = MainWindow(app_name=__app_name__, version=version)
if args.fullscreen: if args.fullscreen:
logger.info("Launching in fullscreen mode due to --fullscreen flag") logger.info("Launching in fullscreen mode due to --fullscreen flag")

View File

@@ -259,6 +259,25 @@ def save_rumble_config(rumble_enabled):
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
def read_gamepad_type():
"""Reads the gamepad type from the [Gamepad] section.
Returns 'xbox' if the parameter is missing.
"""
cp = read_config_safely(CONFIG_FILE)
if cp is None or not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "type"):
save_gamepad_type("xbox")
return "xbox"
return cp.get("Gamepad", "type", fallback="xbox").lower()
def save_gamepad_type(gpad_type):
"""Saves the gamepad type to the [Gamepad] section."""
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
if "Gamepad" not in cp:
cp["Gamepad"] = {}
cp["Gamepad"]["type"] = gpad_type
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
def ensure_default_proxy_config(): def ensure_default_proxy_config():
"""Ensures the [Proxy] section exists in the configuration file. """Ensures the [Proxy] section exists in the configuration file.
Creates it with empty values if missing. Creates it with empty values if missing.
@@ -408,3 +427,22 @@ def save_favorite_folders(folders):
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"' cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile: with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile) cp.write(configfile)
def read_minimize_to_tray():
"""Reads the minimize-to-tray setting from the [Display] section.
Returns True if the parameter is missing (default: minimize to tray).
"""
cp = read_config_safely(CONFIG_FILE)
if cp is None or not cp.has_section("Display") or not cp.has_option("Display", "minimize_to_tray"):
save_minimize_to_tray(True)
return True
return cp.getboolean("Display", "minimize_to_tray", fallback=True)
def save_minimize_to_tray(minimize_to_tray):
"""Saves the minimize-to-tray setting to the [Display] section."""
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
if "Display" not in cp:
cp["Display"] = {}
cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)

View File

@@ -29,7 +29,7 @@ class ContextMenuSignals(QObject):
class ContextMenuManager: class ContextMenuManager:
"""Manages context menu actions for game management in PortProtonQt.""" """Manages context menu actions for game management in PortProtonQt."""
def __init__(self, parent, portproton_location, theme, load_games_callback, update_game_grid_callback): def __init__(self, parent, portproton_location, theme, load_games_callback, game_library_manager):
""" """
Initialize the ContextMenuManager. Initialize the ContextMenuManager.
@@ -45,7 +45,8 @@ class ContextMenuManager:
self.theme = theme self.theme = theme
self.theme_manager = ThemeManager() self.theme_manager = ThemeManager()
self.load_games = load_games_callback self.load_games = load_games_callback
self.update_game_grid = update_game_grid_callback self.game_library_manager = game_library_manager
self.update_game_grid = game_library_manager.update_game_grid
self.legendary_path = os.path.join( self.legendary_path = os.path.join(
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")), os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
"PortProtonQt", "legendary_cache", "legendary" "PortProtonQt", "legendary_cache", "legendary"
@@ -859,9 +860,16 @@ Icon={icon_path}
_("Failed to delete custom data: {error}").format(error=str(e)) _("Failed to delete custom data: {error}").format(error=str(e))
) )
# Reload games list and update grid self.update_game_grid = self.game_library_manager.remove_game_incremental
self.load_games() self.game_library_manager.remove_game_incremental(game_name, exec_line)
self.update_game_grid()
def add_game_incremental(self, game_data: tuple):
"""Add game after .desktop creation."""
if not self._check_portproton():
return
# Assume game_data is built from new .desktop (name, desc, cover, etc.)
self.game_library_manager.add_game_incremental(game_data)
self._show_status_message(_("Added '{game_name}' successfully").format(game_name=game_data[0]))
def add_to_menu(self, game_name, exec_line): def add_to_menu(self, game_name, exec_line):
"""Copy the .desktop file to ~/.local/share/applications.""" """Copy the .desktop file to ~/.local/share/applications."""

View File

Before

Width:  |  Height:  |  Size: 634 KiB

After

Width:  |  Height:  |  Size: 634 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

Before

Width:  |  Height:  |  Size: 978 KiB

After

Width:  |  Height:  |  Size: 978 KiB

View File

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

View File

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 391 KiB

View File

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 710 KiB

View File

Before

Width:  |  Height:  |  Size: 627 KiB

After

Width:  |  Height:  |  Size: 627 KiB

View File

@@ -126,7 +126,21 @@ class FlowLayout(QLayout):
return True return True
def heightForWidth(self, width): def heightForWidth(self, width):
return self.doLayout(QRect(0, 0, width, 0), True) # Аналогично фильтруем видимые для тестового расчёта высоты
visible_items = []
nat_sizes = np.empty((0, 2), dtype=np.int32)
for item in self.itemList:
if item.widget() and item.widget().isVisible():
visible_items.append(item)
s = item.sizeHint()
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
if len(visible_items) == 0:
return 0
_, total_height = compute_layout(nat_sizes, width, self._spacing, self._max_scale)
return total_height
def setGeometry(self, rect): def setGeometry(self, rect):
super().setGeometry(rect) super().setGeometry(rect)
@@ -145,26 +159,46 @@ class FlowLayout(QLayout):
return size return size
def doLayout(self, rect, testOnly): def doLayout(self, rect, testOnly):
N = len(self.itemList) N_total = len(self.itemList)
if N == 0: if N_total == 0:
return 0 return 0
nat_sizes = np.empty((N, 2), dtype=np.int32) # Фильтруем только видимые элементы
visible_items = []
visible_indices = [] # Индексы в оригинальном itemList для установки геометрии
nat_sizes = np.empty((0, 2), dtype=np.int32)
for i, item in enumerate(self.itemList): for i, item in enumerate(self.itemList):
s = item.sizeHint() if item.widget() and item.widget().isVisible():
nat_sizes[i, 0] = s.width() visible_items.append(item)
nat_sizes[i, 1] = s.height() visible_indices.append(i)
s = item.sizeHint()
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
N = len(visible_items)
if N == 0:
# Если все скрыты, устанавливаем нулевые геометрии для всех
if not testOnly:
for item in self.itemList:
item.setGeometry(QRect())
return 0
geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale) geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale)
if not testOnly: if not testOnly:
for i, item in enumerate(self.itemList): # Устанавливаем геометрии только для видимых
x = geom_array[i, 0] + rect.x() for idx, (_vis_idx, item) in enumerate(zip(visible_indices, visible_items, strict=True)):
y = geom_array[i, 1] + rect.y() x = geom_array[idx, 0] + rect.x()
w = geom_array[i, 2] y = geom_array[idx, 1] + rect.y()
h = geom_array[i, 3] w = geom_array[idx, 2]
h = geom_array[idx, 3]
item.setGeometry(QRect(QPoint(x, y), QSize(w, h))) item.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
# Для невидимых — нулевая геометрия
for i in range(N_total):
if i not in visible_indices:
self.itemList[i].setGeometry(QRect())
return total_height return total_height
class ClickableLabel(QLabel): class ClickableLabel(QLabel):

View File

@@ -2,11 +2,13 @@ import os
import tempfile import tempfile
import re import re
from typing import cast, TYPE_CHECKING from typing import cast, TYPE_CHECKING
from PySide6.QtGui import QPixmap, QIcon from PySide6.QtGui import QPixmap, QIcon, QTextCursor
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller QDialog, QFormLayout, QHBoxLayout, QLabel, QVBoxLayout, QListWidget, QScrollArea, QWidget, QListWidgetItem, QSizePolicy, QApplication, QProgressBar, QScroller,
QTabWidget, QTableWidget, QHeaderView, QMessageBox, QTableWidgetItem, QTextEdit, QAbstractItemView, QStackedWidget
) )
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
from icoextract import IconExtractor, IconExtractorError from icoextract import IconExtractor, IconExtractorError
from PIL import Image from PIL import Image
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config
@@ -15,6 +17,8 @@ from portprotonqt.logger import get_logger
from portprotonqt.theme_manager import ThemeManager from portprotonqt.theme_manager import ThemeManager
from portprotonqt.custom_widgets import AutoSizeButton from portprotonqt.custom_widgets import AutoSizeButton
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
from portprotonqt.virtual_keyboard import VirtualKeyboard
from portprotonqt.preloader import Preloader
import psutil import psutil
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -87,6 +91,130 @@ def generate_thumbnail(inputfile, outfile, size=128, force_resize=True):
logger.error(f"Ошибка при сохранении миниатюры: {e}") logger.error(f"Ошибка при сохранении миниатюры: {e}")
return False return False
def create_dialog_hints_widget(theme, main_window, input_manager, context='default'):
"""
Common function to create hints widget for all dialogs.
Uses main_window for get_button_icon/get_nav_icon, input_manager for gamepad detection.
"""
theme_manager = ThemeManager()
current_theme_name = read_theme_from_config()
hintsWidget = QWidget()
hintsWidget.setStyleSheet(theme.STATUS_BAR_STYLE)
hintsLayout = QHBoxLayout(hintsWidget)
hintsLayout.setContentsMargins(10, 0, 10, 0)
hintsLayout.setSpacing(20)
dialog_actions = []
# Context-specific actions (gamepad only, no keyboard)
if context == 'file_explorer':
dialog_actions = [
("confirm", _("Open")), # A / Cross
("add_game", _("Select Dir")), # X / Triangle
("prev_dir", _("Prev Dir")), # Y / Square
("back", _("Cancel")), # B / Circle
("context_menu", _("Menu")), # Start / Options
]
elif context == 'winetricks':
dialog_actions = [
("confirm", _("Toggle")), # A / Cross
("add_game", _("Install")), # X / Triangle
("prev_dir", _("Force Install")), # Y / Square
("back", _("Cancel")), # B / Circle
("prev_tab", _("Prev Tab")), # LB / L1
("next_tab", _("Next Tab")), # RB / R1
]
hints_labels = [] # Store for updates (returned for class storage)
def make_hint(icon_name, text, action=None):
container = QWidget()
hlayout = QHBoxLayout(container)
hlayout.setContentsMargins(0, 5, 0, 0)
hlayout.setSpacing(6)
icon_label = QLabel()
icon_label.setFixedSize(26, 26)
icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
pixmap = QPixmap()
icon_path = theme_manager.get_theme_image(icon_name, current_theme_name)
if icon_path:
pixmap.load(str(icon_path))
if not pixmap.isNull():
icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
hlayout.addWidget(icon_label)
text_label = QLabel(text)
text_label.setStyleSheet(theme.LAST_LAUNCH_VALUE_STYLE)
text_label.setAlignment(Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft)
hlayout.addWidget(text_label)
# Initially hidden; show only if gamepad connected
container.setVisible(False)
hints_labels.append((container, icon_label, action))
hintsLayout.addWidget(container)
# Add gamepad hints only
for action, text in dialog_actions:
make_hint("placeholder", text, action)
hintsLayout.addStretch()
# Return widget and labels for class storage
return hintsWidget, hints_labels
def update_dialog_hints(hints_labels, main_window, input_manager, theme_manager, current_theme_name):
"""
Common function to update hints for any dialog.
"""
if not input_manager or not main_window:
# Hide all if no input_manager or main_window
for container, _, _ in hints_labels:
container.setVisible(False)
return
is_gamepad = input_manager.gamepad is not None
if not is_gamepad:
# Hide all hints if no gamepad
for container, _, _ in hints_labels:
container.setVisible(False)
return
gtype = input_manager.gamepad_type
gamepad_actions = ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir', 'prev_tab', 'next_tab']
for container, icon_label, action in hints_labels:
if action and action in gamepad_actions:
container.setVisible(True)
# Update icon using main_window methods
if action in ['confirm', 'back', 'context_menu', 'add_game', 'prev_dir']:
icon_name = main_window.get_button_icon(action, gtype)
else: # only prev_tab/next_tab (treat as nav)
direction = 'left' if action == 'prev_tab' else 'right'
icon_name = main_window.get_nav_icon(direction, gtype)
icon_path = theme_manager.get_theme_image(icon_name, current_theme_name)
pixmap = QPixmap()
if icon_path:
pixmap.load(str(icon_path))
if not pixmap.isNull():
icon_label.setPixmap(pixmap.scaled(
26, 26,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
))
else:
# Fallback to placeholder
placeholder = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder:
pixmap.load(str(placeholder))
icon_label.setPixmap(pixmap.scaled(26, 26, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
else:
container.setVisible(False)
class FileSelectedSignal(QObject): class FileSelectedSignal(QObject):
file_selected = Signal(str) # Сигнал с путем к выбранному файлу file_selected = Signal(str) # Сигнал с путем к выбранному файлу
@@ -181,6 +309,7 @@ class FileExplorer(QDialog):
self.initial_path = initial_path # Store initial path if provided self.initial_path = initial_path # Store initial path if provided
self.thumbnail_cache = {} # Cache for loaded thumbnails self.thumbnail_cache = {} # Cache for loaded thumbnails
self.pending_thumbnails = set() # Track files pending thumbnail loading self.pending_thumbnails = set() # Track files pending thumbnail loading
self.main_window = None # Add reference to MainWindow
self.setup_ui() self.setup_ui()
# Window settings # Window settings
@@ -194,6 +323,7 @@ class FileExplorer(QDialog):
while parent: while parent:
if hasattr(parent, 'input_manager'): if hasattr(parent, 'input_manager'):
self.input_manager = cast("MainWindow", parent).input_manager self.input_manager = cast("MainWindow", parent).input_manager
self.main_window = parent
if hasattr(parent, 'context_menu_manager'): if hasattr(parent, 'context_menu_manager'):
self.context_menu_manager = cast("MainWindow", parent).context_menu_manager self.context_menu_manager = cast("MainWindow", parent).context_menu_manager
parent = parent.parent() parent = parent.parent()
@@ -210,6 +340,17 @@ class FileExplorer(QDialog):
self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid self.current_path = os.path.expanduser("~") # Fallback to home if initial path is invalid
self.update_file_list() self.update_file_list()
# Create hints widget using common function
self.current_theme_name = read_theme_from_config()
self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='file_explorer')
self.main_layout.addWidget(self.hints_widget)
# Connect signals
if self.input_manager:
self.input_manager.button_event.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name))
self.input_manager.dpad_moved.connect(lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name))
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
class ThumbnailLoader(QRunnable): class ThumbnailLoader(QRunnable):
"""Class for asynchronous thumbnail loading in a separate thread.""" """Class for asynchronous thumbnail loading in a separate thread."""
class Signals(QObject): class Signals(QObject):
@@ -507,8 +648,8 @@ class FileExplorer(QDialog):
"""Update the list of mounted drives and favorite folders.""" """Update the list of mounted drives and favorite folders."""
for i in reversed(range(self.drives_layout.count())): for i in reversed(range(self.drives_layout.count())):
item = self.drives_layout.itemAt(i) item = self.drives_layout.itemAt(i)
if item and item.widget(): widget = item.widget() if item else None
widget = item.widget() if widget:
self.drives_layout.removeWidget(widget) self.drives_layout.removeWidget(widget)
widget.deleteLater() widget.deleteLater()
@@ -597,6 +738,16 @@ class FileExplorer(QDialog):
self.thumbnail_cache.clear() # Clear cache when changing directories self.thumbnail_cache.clear() # Clear cache when changing directories
self.pending_thumbnails.clear() # Clear pending thumbnails self.pending_thumbnails.clear() # Clear pending thumbnails
try: try:
if self.directory_only:
item = QListWidgetItem("./")
folder_icon = theme_manager.get_icon("folder")
# Ensure the icon is a QIcon
if isinstance(folder_icon, str) and os.path.isfile(folder_icon):
folder_icon = QIcon(folder_icon)
elif not isinstance(folder_icon, QIcon):
folder_icon = QIcon() # Fallback to empty icon
item.setIcon(folder_icon)
self.file_list.addItem(item)
if self.current_path != "/": if self.current_path != "/":
item = QListWidgetItem("../") item = QListWidgetItem("../")
folder_icon = theme_manager.get_icon("folder") folder_icon = theme_manager.get_icon("folder")
@@ -804,6 +955,60 @@ class AddGameDialog(QDialog):
if edit_mode: if edit_mode:
self.updatePreview() self.updatePreview()
# Инициализация клавиатуры (отдельным методом вроде лучше)
self.init_keyboard()
# Устанавливаем фокус на первое поле при открытии
QTimer.singleShot(0, self.nameEdit.setFocus)
def init_keyboard(self):
"""Инициализация виртуальной клавиатуры"""
self.keyboard = VirtualKeyboard(self, theme=self.theme, button_width=40)
self.keyboard.hide()
def show_keyboard_for_widget(self, widget):
"""Показывает клавиатуру для указанного виджета"""
if not widget or not widget.isVisible():
return
# Устанавливаем текущий виджет ввода
self.keyboard.current_input_widget = widget
# Позиционирование клавиатуры
keyboard_height = 220
self.keyboard.setFixedWidth(self.width())
self.keyboard.setFixedHeight(keyboard_height)
self.keyboard.move(0, self.height() - keyboard_height)
# Показываем и поднимаем клавиатуру
self.keyboard.setParent(self)
self.keyboard.show()
self.keyboard.raise_()
# TODO: доработать.
# Устанавливаем фокус на первую кнопку клавиатуры
first_button = self.keyboard.findFirstFocusableButton()
if first_button:
QTimer.singleShot(50, lambda: first_button.setFocus())
def closeEvent(self, event):
"""Обработчик закрытия окна"""
if hasattr(self, 'keyboard'):
self.keyboard.hide()
super().closeEvent(event)
def reject(self):
"""Обработчик кнопки Cancel"""
if hasattr(self, 'keyboard'):
self.keyboard.hide()
super().reject()
def accept(self):
"""Обработчик кнопки Apply"""
if hasattr(self, 'keyboard'):
self.keyboard.hide()
super().accept()
def browseExe(self): def browseExe(self):
"""Открывает файловый менеджер для выбора exe-файла""" """Открывает файловый менеджер для выбора exe-файла"""
try: try:
@@ -967,3 +1172,505 @@ Icon={icon_path}
""" """
return desktop_entry, desktop_path return desktop_entry, desktop_path
class WinetricksDialog(QDialog):
def __init__(self, parent=None, theme=None, prefix_path: str | None = None, wine_use: str | None = None):
super().__init__(parent)
self.theme = theme if theme else theme_manager.apply_theme(read_theme_from_config())
self.prefix_path: str | None = prefix_path
self.wine_use: str | None = wine_use
self.portproton_path = get_portproton_location()
if self.portproton_path is None:
logger.error("PortProton location not found")
return
self.tmp_path = os.path.join(self.portproton_path, "data", "tmp")
os.makedirs(self.tmp_path, exist_ok=True)
self.winetricks_path = os.path.join(self.tmp_path, "winetricks")
if self.prefix_path is None:
logger.error("Prefix path not provided")
return
self.log_path = os.path.join(self.prefix_path, "winetricks.log")
os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
if not os.path.exists(self.log_path):
open(self.log_path, 'a').close()
self.downloader = Downloader(max_workers=4)
self.apply_process: QProcess | None = None
self.setWindowTitle(_("Prefix Manager"))
self.setModal(True)
self.resize(700, 700)
self.setStyleSheet(self.theme.MAIN_WINDOW_STYLE + self.theme.MESSAGE_BOX_STYLE)
self.update_winetricks()
self.setup_ui()
self.load_lists()
# Find input_manager and main_window
self.input_manager = None
self.main_window = None
parent = self.parent()
while parent:
if hasattr(parent, 'input_manager'):
self.input_manager = cast("MainWindow", parent).input_manager
self.main_window = parent
parent = parent.parent()
self.current_theme_name = read_theme_from_config()
# Enable Winetricks-specific mode
if self.input_manager:
self.input_manager.enable_winetricks_mode(self)
# Create hints widget using common function
self.hints_widget, self.hints_labels = create_dialog_hints_widget(self.theme, self.main_window, self.input_manager, context='winetricks')
self.main_layout.addWidget(self.hints_widget)
# Connect signals (use self.theme_manager)
if self.input_manager:
self.input_manager.button_event.connect(
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
)
self.input_manager.dpad_moved.connect(
lambda *args: update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
)
update_dialog_hints(self.hints_labels, self.main_window, self.input_manager, theme_manager, self.current_theme_name)
def update_winetricks(self):
"""Update the winetricks script."""
if not self.downloader.has_internet():
logger.warning("No internet connection, skipping winetricks update")
return
url = "https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks"
temp_path = os.path.join(self.tmp_path, "winetricks_temp")
try:
self.downloader.download(url, temp_path)
with open(temp_path) as f:
ext_content = f.read()
ext_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', ext_content)
ext_ver = ext_ver_match.group(1) if ext_ver_match else None
logger.info(f"External winetricks version: {ext_ver}")
except Exception as e:
logger.error(f"Failed to get external version: {e}")
ext_ver = None
if os.path.exists(temp_path):
os.remove(temp_path)
return
int_ver = None
if os.path.exists(self.winetricks_path):
try:
with open(self.winetricks_path) as f:
int_content = f.read()
int_ver_match = re.search(r'WINETRICKS_VERSION\s*=\s*[\'"]?([^\'"\s]+)', int_content)
int_ver = int_ver_match.group(1) if int_ver_match else None
logger.info(f"Internal winetricks version: {int_ver}")
except Exception as e:
logger.error(f"Failed to read internal winetricks version: {e}")
update_needed = not os.path.exists(self.winetricks_path) or (int_ver != ext_ver and ext_ver)
if update_needed:
try:
self.downloader.download(url, self.winetricks_path)
os.chmod(self.winetricks_path, 0o755)
logger.info(f"Winetricks updated to version {ext_ver}")
self.apply_modifications(self.winetricks_path)
except Exception as e:
logger.error(f"Failed to update winetricks: {e}")
elif os.path.exists(self.winetricks_path):
self.apply_modifications(self.winetricks_path)
if os.path.exists(temp_path):
os.remove(temp_path)
def apply_modifications(self, file_path):
"""Apply custom modifications to the winetricks script."""
if not os.path.exists(file_path):
return
try:
with open(file_path) as f:
content = f.read()
# Apply sed-like replacements
content = re.sub(r'w_metadata vcrun2015 dlls \\', r'w_metadata !dont_use_2015! dlls \\', content)
content = re.sub(r'w_metadata vcrun2017 dlls \\', r'w_metadata !dont_use_2017! dlls \\', content)
content = re.sub(r'w_metadata vcrun2019 dlls \\', r'w_metadata !dont_use_2019! dlls \\', content)
content = re.sub(r'w_set_winver win2k3', r'w_set_winver win7', content)
with open(file_path, 'w') as f:
f.write(content)
logger.info("Winetricks modifications applied")
except Exception as e:
logger.error(f"Error applying modifications to winetricks: {e}")
def setup_ui(self):
"""Set up the user interface with tabs and tables."""
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(10, 10, 10, 10)
self.main_layout.setSpacing(10)
# Log output
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setStyleSheet(self.theme.WINETRICKS_LOG_STYLE)
self.main_layout.addWidget(self.log_output)
# Tab widget
self.tab_widget = QTabWidget()
self.tab_widget.setStyleSheet(self.theme.WINETRICKS_TAB_STYLE)
table_base_style = self.theme.WINETRICKS_TABBLE_STYLE
# DLLs tab
self.dll_table = QTableWidget()
self.dll_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.dll_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.dll_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.dll_table.setColumnCount(3)
self.dll_table.setHorizontalHeaderLabels([_("Set"), _("Libraries"), _("Information")])
self.dll_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
self.dll_table.horizontalHeader().resizeSection(0, 50)
self.dll_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.dll_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.dll_table.setStyleSheet(table_base_style)
self.dll_preloader = Preloader()
dll_preloader_container = QWidget()
dll_preloader_layout = QVBoxLayout(dll_preloader_container)
dll_preloader_layout.addStretch()
dll_preloader_hlayout = QHBoxLayout()
dll_preloader_hlayout.addStretch()
dll_preloader_hlayout.addWidget(self.dll_preloader)
dll_preloader_hlayout.addStretch()
dll_preloader_layout.addLayout(dll_preloader_hlayout)
dll_preloader_layout.addStretch()
dll_preloader_layout.setContentsMargins(0, 0, 0, 0)
dll_preloader_layout.setSpacing(0)
self.dll_container = QStackedWidget()
self.dll_container.addWidget(dll_preloader_container)
self.dll_container.addWidget(self.dll_table)
self.tab_widget.addTab(self.dll_container, "DLLs")
# Fonts tab
self.fonts_table = QTableWidget()
self.fonts_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.fonts_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.fonts_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.fonts_table.setColumnCount(3)
self.fonts_table.setHorizontalHeaderLabels([_("Set"), _("Fonts"), _("Information")])
self.fonts_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
self.fonts_table.horizontalHeader().resizeSection(0, 50)
self.fonts_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.fonts_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.fonts_table.setStyleSheet(table_base_style)
self.fonts_preloader = Preloader()
fonts_preloader_container = QWidget()
fonts_preloader_layout = QVBoxLayout(fonts_preloader_container)
fonts_preloader_layout.addStretch()
fonts_preloader_hlayout = QHBoxLayout()
fonts_preloader_hlayout.addStretch()
fonts_preloader_hlayout.addWidget(self.fonts_preloader)
fonts_preloader_hlayout.addStretch()
fonts_preloader_layout.addLayout(fonts_preloader_hlayout)
fonts_preloader_layout.addStretch()
fonts_preloader_layout.setContentsMargins(0, 0, 0, 0)
fonts_preloader_layout.setSpacing(0)
self.fonts_container = QStackedWidget()
self.fonts_container.addWidget(fonts_preloader_container)
self.fonts_container.addWidget(self.fonts_table)
self.tab_widget.addTab(self.fonts_container, _("Fonts"))
# Settings tab
self.settings_table = QTableWidget()
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.settings_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.settings_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.settings_table.setColumnCount(3)
self.settings_table.setHorizontalHeaderLabels([_("Set"), _("Settings"), _("Information")])
self.settings_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
self.settings_table.horizontalHeader().resizeSection(0, 50)
self.settings_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.settings_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.settings_table.setStyleSheet(table_base_style)
self.settings_preloader = Preloader()
settings_preloader_container = QWidget()
settings_preloader_layout = QVBoxLayout(settings_preloader_container)
settings_preloader_layout.addStretch()
settings_preloader_hlayout = QHBoxLayout()
settings_preloader_hlayout.addStretch()
settings_preloader_hlayout.addWidget(self.settings_preloader)
settings_preloader_hlayout.addStretch()
settings_preloader_layout.addLayout(settings_preloader_hlayout)
settings_preloader_layout.addStretch()
settings_preloader_layout.setContentsMargins(0, 0, 0, 0)
settings_preloader_layout.setSpacing(0)
self.settings_container = QStackedWidget()
self.settings_container.addWidget(settings_preloader_container)
self.settings_container.addWidget(self.settings_table)
self.tab_widget.addTab(self.settings_container, _("Settings"))
self.containers = {
"dlls": self.dll_container,
"fonts": self.fonts_container,
"settings": self.settings_container
}
self.main_layout.addWidget(self.tab_widget)
# Buttons
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
self.cancel_button = AutoSizeButton(_("Cancel"), icon=theme_manager.get_icon("cancel"))
self.force_button = AutoSizeButton(_("Force Install"), icon=theme_manager.get_icon("apply"))
self.install_button = AutoSizeButton(_("Install"), icon=theme_manager.get_icon("apply"))
self.cancel_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.force_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
self.install_button.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
button_layout.addWidget(self.cancel_button)
button_layout.addWidget(self.force_button)
button_layout.addWidget(self.install_button)
self.main_layout.addLayout(button_layout)
self.cancel_button.clicked.connect(self.reject)
self.force_button.clicked.connect(lambda: self.install_selected(force=True))
self.install_button.clicked.connect(lambda: self.install_selected(force=False))
def load_lists(self):
"""Load and populate the lists for DLLs, Fonts, and Settings"""
if not os.path.exists(self.winetricks_path):
QMessageBox.warning(self, _("Error"), _("Winetricks not found. Please try again."))
self.reject()
return
assert self.prefix_path is not None
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
env.insert("WINETRICKS_DOWNLOADER", "curl")
if self.wine_use is not None:
env.insert("WINE", self.wine_use)
cwd = os.path.dirname(self.winetricks_path)
# DLLs
self.containers["dlls"].setCurrentIndex(0)
self._start_list_process("dlls", self.dll_table, self.get_dll_exclusions(), env, cwd)
# Fonts
self.containers["fonts"].setCurrentIndex(0)
self._start_list_process("fonts", self.fonts_table, self.get_fonts_exclusions(), env, cwd)
# Settings
self.containers["settings"].setCurrentIndex(0)
self._start_list_process("settings", self.settings_table, self.get_settings_exclusions(), env, cwd)
def _start_list_process(self, category, table, exclusion_pattern, env, cwd):
"""Запускает QProcess для списка."""
process = QProcess(self)
process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
process.setProcessEnvironment(env)
process.finished.connect(lambda exit_code, exit_status: self._on_list_finished(category, table, exclusion_pattern, process, exit_code, exit_status))
process.start(self.winetricks_path, [category, "list"])
def _on_list_finished(self, category, table, exclusion_pattern, process: QProcess | None, exit_code, exit_status):
"""Обработчик завершения списка."""
if process is None:
logger.error(f"Process is None for {category}")
self.containers[category].setCurrentIndex(1)
return
output = bytes(process.readAllStandardOutput().data()).decode('utf-8', 'ignore')
if exit_code == 0 and exit_status == QProcess.ExitStatus.NormalExit:
self.populate_table(table, output, exclusion_pattern, self.log_path)
# Restore focus after populating
if table.rowCount() > 0:
table.setCurrentCell(0, 0)
table.setFocus(Qt.FocusReason.OtherFocusReason)
else:
error_output = bytes(process.readAllStandardError().data()).decode('utf-8', 'ignore')
logger.error(f"Failed to list {category}: {error_output}")
self.containers[category].setCurrentIndex(1)
def get_dll_exclusions(self):
"""Get regex pattern for DLL exclusions."""
return r'(d3d|directx9|dont_use|dxvk|vkd3d|galliumnine|faudio1|Foundation)'
def get_fonts_exclusions(self):
"""Get regex pattern for Fonts exclusions."""
return r'dont_use'
def get_settings_exclusions(self):
"""Get regex pattern for Settings exclusions."""
return r'(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home)'
def populate_table(self, table, output, exclusion_pattern, log_path):
"""Populate the table with items from output, checking installation status."""
table.setRowCount(0)
table.verticalHeader().setVisible(False)
lines = output.strip().split('\n')
installed = set()
if os.path.exists(log_path):
with open(log_path) as f:
for line in f:
installed.add(line.strip())
# regex-парсинг (имя - первое слово, остальное - описание)
line_re = re.compile(r"^\s*(?:\[(.)]\s+)?([^\s]+)\s*(.*)")
for line in lines:
line = line.strip()
if not line or re.search(exclusion_pattern, line, re.I):
continue
line = line.split('(', 1)[0].strip()
match = line_re.match(line)
if not match:
continue
_status, name, info = match.groups()
# Очищаем info от мусора
info = re.sub(r'\[.*?\]', '', info).strip() # Удаляем [скачивания] и т.п.
# To match bash desc extraction: after name, substr(2) to trim leading space
if info.startswith(' '):
info = info[1:].lstrip()
# Фильтр служебных строк
if '/' in name or '\\' in name or name.lower() in ('executing', 'using', 'warning:') or name.endswith(':'):
continue
checked = Qt.CheckState.Checked if name in installed else Qt.CheckState.Unchecked
row = table.rowCount()
table.insertRow(row)
# Checkbox
checkbox = QTableWidgetItem()
checkbox.setCheckState(checked)
table.setItem(row, 0, checkbox)
# Name
name_item = QTableWidgetItem(name)
table.setItem(row, 1, name_item)
# Info
info_item = QTableWidgetItem(info)
table.setItem(row, 2, info_item)
def install_selected(self, force=False):
"""Install selected components."""
selected = []
for table in [self.dll_table, self.fonts_table, self.settings_table]:
for row in range(table.rowCount()):
checkbox = table.item(row, 0)
if checkbox is not None and checkbox.checkState() == Qt.CheckState.Checked:
name_item = table.item(row, 1)
if name_item is not None:
name = name_item.text()
if name and name not in selected:
selected.append(name)
# Load installed
installed = set()
if os.path.exists(self.log_path):
with open(self.log_path) as f:
for line in f:
installed.add(line.strip())
# Filter to new selected
new_selected = [name for name in selected if name not in installed]
if not new_selected:
QMessageBox.information(self, _("Warning"), _("No components selected."))
return
self.install_button.setEnabled(False)
self.force_button.setEnabled(False)
self.cancel_button.setEnabled(False)
self._start_install_process(new_selected, force)
def _start_install_process(self, selected, force):
"""Запускает QProcess для установки."""
assert self.prefix_path is not None
env = QProcessEnvironment.systemEnvironment()
env.insert("WINEPREFIX", self.prefix_path)
if self.wine_use is not None:
env.insert("WINE", self.wine_use)
self.apply_process = QProcess(self)
self.apply_process.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
self.apply_process.setProcessEnvironment(env)
self.apply_process.readyReadStandardOutput.connect(self._on_ready_read)
self.apply_process.finished.connect(lambda exit_code, exit_status: self._on_install_finished(exit_code, exit_status, selected))
args = ["--unattended"] + (["--force"] if force else []) + selected
self.apply_process.start(self.winetricks_path, args)
def _on_ready_read(self):
"""Handle ready read for install process."""
if self.apply_process is None:
return
data = self.apply_process.readAllStandardOutput().data()
message = bytes(data).decode('utf-8', 'ignore').strip()
self._log(message)
def _on_install_finished(self, exit_code, exit_status, selected):
"""Обработчик завершения установки."""
error_message = ""
if self.apply_process is not None:
# Читаем вывод в зависимости от режима каналов
if self.apply_process.processChannelMode() == QProcess.ProcessChannelMode.MergedChannels:
# Если каналы объединены, читаем из StandardOutput
output_data = self.apply_process.readAllStandardOutput().data()
error_message = bytes(output_data).decode('utf-8', 'ignore')
else:
# Если каналы разделены, читаем из StandardError
error_data = self.apply_process.readAllStandardError().data()
error_message = bytes(error_data).decode('utf-8', 'ignore')
if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
logger.error(f"Winetricks install failed: {error_message}")
QMessageBox.warning(self, _("Error"), _("Installation failed. Check logs."))
else:
if os.path.exists(self.log_path):
with open(self.log_path) as f:
existing = {line.strip() for line in f if line.strip()}
else:
existing = set()
with open(self.log_path, 'a') as f:
for name in selected:
if name not in existing:
f.write(f"{name}\n")
logger.info("Winetricks installation completed successfully.")
QMessageBox.information(self, _("Success"), _("Components installed successfully."))
self.load_lists()
# Разблокировка
self.install_button.setEnabled(True)
self.force_button.setEnabled(True)
self.cancel_button.setEnabled(True)
def _log(self, message):
"""Добавляет в лог."""
self.log_output.append(message)
self.log_output.moveCursor(QTextCursor.MoveOperation.End)
def closeEvent(self, event):
"""Disable mode on close."""
if self.input_manager:
self.input_manager.disable_winetricks_mode()
super().closeEvent(event)
def reject(self):
"""Disable mode on reject."""
if self.input_manager:
self.input_manager.disable_winetricks_mode()
super().reject()

View File

@@ -12,6 +12,7 @@ from portprotonqt.downloader import Downloader
from portprotonqt.animations import GameCardAnimations from portprotonqt.animations import GameCardAnimations
from typing import cast from typing import cast
class GameCard(QFrame): class GameCard(QFrame):
borderWidthChanged = Signal() borderWidthChanged = Signal()
gradientAngleChanged = Signal() gradientAngleChanged = Signal()
@@ -447,6 +448,7 @@ class GameCard(QFrame):
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged)) gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged)) scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged))
def paintEvent(self, event): def paintEvent(self, event):
super().paintEvent(event) super().paintEvent(event)
self.animations.paint_border(QPainter(self)) self.animations.paint_border(QPainter(self))

View File

@@ -0,0 +1,467 @@
from typing import Protocol
from portprotonqt.game_card import GameCard
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller
from PySide6.QtCore import Qt, QTimer
from portprotonqt.custom_widgets import FlowLayout
from portprotonqt.config_utils import read_favorites, read_sort_method, read_card_size, save_card_size
from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.context_menu_manager import ContextMenuManager, CustomLineEdit
from collections import deque
class MainWindowProtocol(Protocol):
"""Protocol defining the interface that MainWindow must implement for GameLibraryManager."""
def openGameDetailPage(
self,
name: str,
description: str,
cover_path: str | None = None,
appid: str = "",
exec_line: str = "",
controller_support: str = "",
last_launch: str = "",
formatted_playtime: str = "",
protondb_tier: str = "",
game_source: str = "",
anticheat_status: str = "",
) -> None: ...
def createSearchWidget(self) -> tuple[QWidget, CustomLineEdit]: ...
def on_slider_released(self) -> None: ...
# Required attributes
searchEdit: CustomLineEdit
_last_card_width: int
current_hovered_card: GameCard | None
current_focused_card: GameCard | None
gamesListWidget: QWidget | None
class GameLibraryManager:
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
self.main_window = main_window
self.theme = theme
self.context_menu_manager: ContextMenuManager | None = context_menu_manager
self.games: list[tuple] = []
self.filtered_games: list[tuple] = []
self.game_card_cache = {}
self.pending_images = {}
self.card_width = read_card_size()
self.gamesListWidget: QWidget | None = None
self.gamesListLayout: FlowLayout | None = None
self.sizeSlider: QSlider | None = None
self._update_timer: QTimer | None = None
self._pending_update = False
self.pending_deletions = deque()
self.is_filtering = False
self.dirty = False
def create_games_library_widget(self):
"""Creates the games library widget with search, grid, and slider."""
self.gamesLibraryWidget = QWidget()
self.gamesLibraryWidget.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE)
layout = QVBoxLayout(self.gamesLibraryWidget)
layout.setSpacing(15)
# Search widget
searchWidget, self.searchEdit = self.main_window.createSearchWidget()
layout.addWidget(searchWidget)
# Scroll area for game grid
scrollArea = QScrollArea()
scrollArea.setWidgetResizable(True)
scrollArea.setStyleSheet(self.theme.SCROLL_AREA_STYLE)
QScroller.grabGesture(scrollArea.viewport(), QScroller.ScrollerGestureType.LeftMouseButtonGesture)
self.gamesListWidget = QWidget()
self.gamesListWidget.setStyleSheet(self.theme.LIST_WIDGET_STYLE)
self.gamesListLayout = FlowLayout(self.gamesListWidget)
self.gamesListWidget.setLayout(self.gamesListLayout)
scrollArea.setWidget(self.gamesListWidget)
layout.addWidget(scrollArea)
# Slider for card size
sliderLayout = QHBoxLayout()
sliderLayout.addStretch()
self.sizeSlider = QSlider(Qt.Orientation.Horizontal)
self.sizeSlider.setMinimum(200)
self.sizeSlider.setMaximum(250)
self.sizeSlider.setValue(self.card_width)
self.sizeSlider.setTickInterval(10)
self.sizeSlider.setFixedWidth(150)
self.sizeSlider.setToolTip(f"{self.card_width} px")
self.sizeSlider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
self.sizeSlider.sliderReleased.connect(self.main_window.on_slider_released)
sliderLayout.addWidget(self.sizeSlider)
layout.addLayout(sliderLayout)
# Initialize update timer
self._update_timer = QTimer()
self._update_timer.setSingleShot(True)
self._update_timer.setInterval(100) # 100ms debounce
self._update_timer.timeout.connect(self._perform_update)
# Calculate initial card width
def calculate_card_width():
if self.gamesListLayout is None:
return
available_width = scrollArea.width() - 20
spacing = self.gamesListLayout._spacing
target_cards_per_row = 8
calculated_width = (available_width - spacing * (target_cards_per_row - 1)) // target_cards_per_row
calculated_width = max(200, min(calculated_width, 250))
QTimer.singleShot(0, calculate_card_width)
# Connect scroll event for lazy loading
scrollArea.verticalScrollBar().valueChanged.connect(self.load_visible_images)
return self.gamesLibraryWidget
def on_slider_released(self):
"""Handles slider release to update card size."""
if self.sizeSlider is None:
return
self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{self.card_width} px")
save_card_size(self.card_width)
for card in self.game_card_cache.values():
card.update_card_size(self.card_width)
self.update_game_grid()
def load_visible_images(self):
"""Loads images for visible game cards."""
if self.gamesListWidget is None:
return
visible_region = self.gamesListWidget.visibleRegion()
max_concurrent_loads = 5
loaded_count = 0
for card_key, card in self.game_card_cache.items():
if card_key in self.pending_images and visible_region.contains(card.pos()) and loaded_count < max_concurrent_loads:
cover_path, width, height, callback = self.pending_images.pop(card_key)
load_pixmap_async(cover_path, width, height, callback)
loaded_count += 1
def _on_card_focused(self, game_name: str, is_focused: bool):
"""Handles card focus events."""
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.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
self.main_window.current_hovered_card._hovered = False
self.main_window.current_hovered_card.leaveEvent(None)
self.main_window.current_hovered_card = None
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
self.main_window.current_focused_card._focused = False
self.main_window.current_focused_card.clearFocus()
self.main_window.current_focused_card = card
else:
if self.main_window.current_focused_card == card:
self.main_window.current_focused_card = None
def _on_card_hovered(self, game_name: str, is_hovered: bool):
"""Handles card hover events."""
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.main_window.current_focused_card and self.main_window.current_focused_card != card:
self.main_window.current_focused_card._focused = False
self.main_window.current_focused_card.clearFocus()
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
self.main_window.current_hovered_card._hovered = False
self.main_window.current_hovered_card.leaveEvent(None)
self.main_window.current_hovered_card = card
else:
if self.main_window.current_hovered_card == card:
self.main_window.current_hovered_card = None
def _perform_update(self):
"""Performs the actual grid update."""
if not self._pending_update:
return
self._pending_update = False
self._update_game_grid_immediate()
def update_game_grid(self, games_list: list[tuple] | None = None, is_filter: bool = False):
"""Schedules a game grid update with debouncing."""
if not is_filter:
if games_list is not None:
self.filtered_games = games_list
self.dirty = True # Full rebuild only for non-filter
self.is_filtering = is_filter
self._pending_update = True
if self._update_timer is not None:
self._update_timer.start()
else:
self._update_game_grid_immediate()
def force_update_cards_library(self):
if self.gamesListWidget and self.gamesListLayout:
self.gamesListLayout.invalidate()
self.gamesListWidget.updateGeometry()
widget = self.gamesListWidget
QTimer.singleShot(0, lambda: (
widget.adjustSize(),
widget.updateGeometry()
))
def _update_game_grid_immediate(self):
"""Updates the game grid with the provided or current game list."""
if self.gamesListLayout is None or self.gamesListWidget is None:
return
search_text = self.main_window.searchEdit.text().strip().lower()
if self.is_filtering:
# Filter mode: do not change layout, only hide/show cards
self._apply_filter_visibility(search_text)
else:
# Full update: sorting, removal/addition, reorganization
games_list = self.filtered_games if self.filtered_games else self.games
favorites = read_favorites()
sort_method = read_sort_method()
# Batch layout updates (extended scope)
self.gamesListWidget.setUpdatesEnabled(False)
if self.gamesListLayout is not None:
self.gamesListLayout.setEnabled(False) # Disable layout during batch
try:
# Optimized sorting: Partition favorites first, then sort subgroups
def partition_sort_key(game):
name = game[0]
is_fav = name in favorites
fav_order = 0 if is_fav else 1
if sort_method == "playtime":
return (fav_order, -game[11] if game[11] else 0, -game[10] if game[10] else 0)
elif sort_method == "alphabetical":
return (fav_order, name.lower())
elif sort_method == "favorites":
return (fav_order,)
else:
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
# Quick partition: Sort favorites and non-favorites separately, then merge
fav_games = [g for g in games_list if g[0] in favorites]
non_fav_games = [g for g in games_list if g[0] not in favorites]
sorted_fav = sorted(fav_games, key=partition_sort_key)
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
sorted_games = sorted_fav + sorted_non_fav
# Build set of current game keys for faster lookup
current_game_keys = {(game[0], game[4]) for game in sorted_games}
# Remove cards that no longer exist (batch)
cards_to_remove = []
for card_key in list(self.game_card_cache.keys()):
if card_key not in current_game_keys:
cards_to_remove.append(card_key)
for card_key in cards_to_remove:
card = self.game_card_cache.pop(card_key)
if self.gamesListLayout is not None:
self.gamesListLayout.removeWidget(card)
self.pending_deletions.append(card) # Defer
if card_key in self.pending_images:
del self.pending_images[card_key]
# Track current layout order (only if dirty/full update needed)
if self.dirty and self.gamesListLayout is not None:
current_layout_order = []
for i in range(self.gamesListLayout.count()):
item = self.gamesListLayout.itemAt(i)
if item is not None:
widget = item.widget()
if widget:
for key, card in self.game_card_cache.items():
if card == widget:
current_layout_order.append(key)
break
else:
current_layout_order = None # Skip reorg if not dirty
new_card_order = []
cards_to_add = []
for game_data in sorted_games:
game_name = game_data[0]
exec_line = game_data[4]
game_key = (game_name, exec_line)
should_be_visible = not search_text or search_text in game_name.lower()
if game_key in self.game_card_cache:
card = self.game_card_cache[game_key]
if card.isVisible() != should_be_visible:
card.setVisible(should_be_visible)
new_card_order.append(game_key)
else:
if self.context_menu_manager is None:
continue
card = self._create_game_card(game_data)
self.game_card_cache[game_key] = card
card.setVisible(should_be_visible)
new_card_order.append(game_key)
cards_to_add.append((game_key, card))
# Only reorganize if order changed AND dirty
if self.dirty and self.gamesListLayout is not None and (current_layout_order is None or new_card_order != current_layout_order):
# Remove all widgets from layout (batch)
while self.gamesListLayout.count():
self.gamesListLayout.takeAt(0)
# Add widgets in new order (batch)
for game_key in new_card_order:
card = self.game_card_cache[game_key]
self.gamesListLayout.addWidget(card)
self.dirty = False # Reset flag
# Deferred deletions (run in timer to avoid stack overflow)
if self.pending_deletions:
QTimer.singleShot(0, lambda: self._flush_deletions())
# Load visible images for new cards only
if cards_to_add:
self.load_visible_images()
finally:
if self.gamesListLayout is not None:
self.gamesListLayout.setEnabled(True)
self.gamesListWidget.setUpdatesEnabled(True)
if self.gamesListLayout is not None:
self.gamesListLayout.update()
self.gamesListWidget.updateGeometry()
self.main_window._last_card_width = self.card_width
self.force_update_cards_library()
self.is_filtering = False # Reset flag in any case
def _apply_filter_visibility(self, search_text: str):
"""Applies visibility to cards based on search, without changing the layout."""
visible_count = 0
for game_key, card in self.game_card_cache.items():
game_name = card.name # Assume GameCard has 'name' attribute
should_be_visible = not search_text or search_text in game_name.lower()
if card.isVisible() != should_be_visible:
card.setVisible(should_be_visible)
if should_be_visible:
visible_count += 1
# Load image only for newly visible cards
if game_key in self.pending_images:
cover_path, width, height, callback = self.pending_images.pop(game_key)
load_pixmap_async(cover_path, width, height, callback)
# Force full relayout after visibility changes
if self.gamesListLayout is not None:
self.gamesListLayout.invalidate() # Принудительно инвалидируем для пересчёта
self.gamesListLayout.update()
if self.gamesListWidget is not None:
self.gamesListWidget.updateGeometry()
self.main_window._last_card_width = self.card_width
# If search is empty, load images for visible ones
if not search_text:
self.load_visible_images()
def _create_game_card(self, game_data: tuple) -> GameCard:
"""Creates a new game card with all necessary connections."""
card = GameCard(
*game_data,
select_callback=self.main_window.openGameDetailPage,
theme=self.theme,
card_width=self.card_width,
context_menu_manager=self.context_menu_manager
)
card.hoverChanged.connect(self._on_card_hovered)
card.focusChanged.connect(self._on_card_focused)
if self.context_menu_manager:
card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
card.addToMenuRequested.connect(self.context_menu_manager.add_to_menu)
card.removeFromMenuRequested.connect(self.context_menu_manager.remove_from_menu)
card.addToDesktopRequested.connect(self.context_menu_manager.add_to_desktop)
card.removeFromDesktopRequested.connect(self.context_menu_manager.remove_from_desktop)
card.addToSteamRequested.connect(self.context_menu_manager.add_to_steam)
card.removeFromSteamRequested.connect(self.context_menu_manager.remove_from_steam)
card.openGameFolderRequested.connect(self.context_menu_manager.open_game_folder)
return card
def _flush_deletions(self):
"""Delete pending widgets off the main update cycle."""
for card in list(self.pending_deletions):
card.deleteLater()
self.pending_deletions.remove(card)
def clear_layout(self, layout):
"""Clears all widgets from the layout."""
if layout is None:
return
while layout.count():
child = layout.takeAt(0)
if child.widget():
widget = child.widget()
for key, card in list(self.game_card_cache.items()):
if card == widget:
del self.game_card_cache[key]
if key in self.pending_images:
del self.pending_images[key]
widget.deleteLater()
def set_games(self, games: list[tuple]):
"""Sets the games list and updates the filtered games."""
self.games = games
self.filtered_games = self.games
self.dirty = True # Full resort needed
self.update_game_grid()
def add_game_incremental(self, game_data: tuple):
"""Add a single game without full reload."""
self.games.append(game_data)
self.filtered_games.append(game_data) # Assume no filter active; adjust if needed
self.dirty = True
self.update_game_grid()
def remove_game_incremental(self, game_name: str, exec_line: str):
"""Remove a single game without full reload."""
key = (game_name, exec_line)
self.games = [g for g in self.games if (g[0], g[4]) != key]
self.filtered_games = [g for g in self.filtered_games if (g[0], g[4]) != key]
if key in self.game_card_cache and self.gamesListLayout is not None:
card = self.game_card_cache.pop(key)
self.gamesListLayout.removeWidget(card)
self.pending_deletions.append(card) # Defer deleteLater
if key in self.pending_images:
del self.pending_images[key]
self.dirty = True
self.update_game_grid()
def filter_games_delayed(self):
"""Filters games based on search text and updates the grid."""
self.update_game_grid(is_filter=True)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
# keyboard_layouts.py
keyboard_layouts = {
'en': {
'normal': [
['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='],
['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'],
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"],
['', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/']
],
'shift': [
['~', '!', '@', '#', '$', '%', '^', '&&', '*', '(', ')', '_', '+'],
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'],
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"'],
['', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?']
]
},
'ru': {
'normal': [
['ё', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='],
['TAB', 'й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', '\\'],
['CAPS', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'],
['', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', '.']
],
'shift': [
['Ё', '!', '"', '', ';', '%', ':', '?', '*', '(', ')', '_', '+'],
['TAB', 'Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', '/'],
['CAPS', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж', 'Э'],
['', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю', ',']
]
},
'fr': {
'normal': [
['²', '&', 'é', '"', "'", '(', '-', 'è', '_', 'ç', 'à', ')', '='],
['TAB', 'a', 'z', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '^', '$', '*'],
['CAPS', 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'ù'],
['', 'w', 'x', 'c', 'v', 'b', 'n', ',', ';', ':', '!']
],
'shift': [
['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '°', '+'],
['TAB', 'A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '¨', '£', 'µ'],
['CAPS', 'Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', '%'],
['', 'W', 'X', 'C', 'V', 'B', 'N', '?', '.', '/', '§']
]
},
'es': {
'normal': [
['º', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', "'", '¡'],
['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '`', '+', '\\'],
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ñ', "'", 'ç'],
['', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-']
],
'shift': [
['ª', '!', '"', '·', '$', '%', '&', '/', '(', ')', '=', '?', '¿'],
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '^', '*', '|'],
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ñ', '"', 'Ç'],
['', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_']
]
},
'de': {
'normal': [
['^', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß', '´'],
['TAB', 'q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü', '+', '#'],
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
['', '<', 'y', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-']
],
'shift': [
['°', '!', '"', '§', '$', '%', '&', '/', '(', ')', '=', '?', '`'],
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Z', 'U', 'I', 'O', 'P', 'Ü', '*', '\''],
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ö', 'Ä'],
['', '>', 'Y', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_']
]
}
}

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-09-23 22:23+0500\n" "POT-Creation-Date: 2025-10-16 14:54+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"
@@ -191,6 +191,10 @@ msgstr ""
msgid "Failed to delete custom data: {error}" msgid "Failed to delete custom data: {error}"
msgstr "" msgstr ""
#, python-brace-format
msgid "Added '{game_name}' successfully"
msgstr ""
msgid "Game name and executable path are required" msgid "Game name and executable path are required"
msgstr "" msgstr ""
@@ -248,13 +252,37 @@ msgstr ""
msgid "Select All" msgid "Select All"
msgstr "" msgstr ""
#, python-brace-format msgid "Open"
msgid "Launching {0}" msgstr ""
msgid "Select Dir"
msgstr ""
msgid "Prev Dir"
msgstr "" msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
msgid "Toggle"
msgstr ""
msgid "Install"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Prev Tab"
msgstr ""
msgid "Next Tab"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgstr ""
msgid "File Explorer" msgid "File Explorer"
msgstr "" msgstr ""
@@ -304,6 +332,39 @@ msgstr ""
msgid "No cover selected" msgid "No cover selected"
msgstr "" msgstr ""
msgid "Prefix Manager"
msgstr ""
msgid "Set"
msgstr ""
msgid "Libraries"
msgstr ""
msgid "Information"
msgstr ""
msgid "Fonts"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Winetricks not found. Please try again."
msgstr ""
msgid "Warning"
msgstr ""
msgid "No components selected."
msgstr ""
msgid "Installation failed. Check logs."
msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Loading Epic Games Store games..." msgid "Loading Epic Games Store games..."
msgstr "" msgstr ""
@@ -352,9 +413,6 @@ msgstr ""
msgid "Auto Install" msgid "Auto Install"
msgstr "" msgstr ""
msgid "Emulators"
msgstr ""
msgid "Wine Settings" msgid "Wine Settings"
msgstr "" msgstr ""
@@ -370,6 +428,28 @@ msgstr ""
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
msgid "Search"
msgstr ""
msgid "Installation already in progress."
msgstr ""
msgid "Failed to start installation."
msgstr ""
#, python-brace-format
msgid "Processed {} installation..."
msgstr ""
msgid "Installation completed successfully."
msgstr ""
msgid "Installation failed."
msgstr ""
msgid "Installation error."
msgstr ""
msgid "Loading Steam games..." msgid "Loading Steam games..."
msgstr "" msgstr ""
@@ -382,13 +462,106 @@ msgstr ""
msgid "Find Games ..." msgid "Find Games ..."
msgstr "" msgstr ""
msgid "Here you can configure automatic game installation..." #, python-brace-format
msgid "Added '{name}'"
msgstr "" msgstr ""
msgid "List of available emulators and their configuration..." msgid "Compatibility tool:"
msgstr "" msgstr ""
msgid "Various Wine parameters and versions..." msgid "Prefix:"
msgstr ""
msgid "Wine Configuration"
msgstr ""
msgid "Registry Editor"
msgstr ""
msgid "Command Prompt"
msgstr ""
msgid "Uninstaller"
msgstr ""
msgid "Create Prefix Backup"
msgstr ""
msgid "Load Prefix Backup"
msgstr ""
msgid "Delete Compatibility Tool"
msgstr ""
msgid "Delete Prefix"
msgstr ""
msgid "Clear Prefix"
msgstr ""
msgid "Launching tool..."
msgstr ""
msgid "Failed to start process."
msgstr ""
msgid "Confirm Clear"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgstr ""
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgstr ""
msgid "Failed to start backup process."
msgstr ""
msgid "Failed to start restore process."
msgstr ""
msgid "Prefix backup completed."
msgstr ""
msgid "Prefix backup failed."
msgstr ""
msgid "Prefix restore completed."
msgstr ""
msgid "Prefix restore failed."
msgstr ""
#, python-brace-format
msgid "Are you sure you want to delete prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' deleted."
msgstr ""
#, python-brace-format
msgid "Failed to delete prefix: {}"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to delete compatibility tool '{}'?"
msgstr ""
#, python-brace-format
msgid "Compatibility tool '{}' deleted."
msgstr ""
#, python-brace-format
msgid "Failed to delete compatibility tool: {}"
msgstr "" msgstr ""
msgid "Main PortProton parameters..." msgid "Main PortProton parameters..."
@@ -424,6 +597,9 @@ msgstr ""
msgid "Games Display Filter:" msgid "Games Display Filter:"
msgstr "" msgstr ""
msgid "Gamepad Type:"
msgstr ""
msgid "Proxy URL" msgid "Proxy URL"
msgstr "" msgstr ""
@@ -448,6 +624,12 @@ msgstr ""
msgid "Application Fullscreen Mode:" msgid "Application Fullscreen Mode:"
msgstr "" msgstr ""
msgid "Minimize to tray on close"
msgstr ""
msgid "Application Close Mode:"
msgstr ""
msgid "Auto Fullscreen on Gamepad connected" msgid "Auto Fullscreen on Gamepad connected"
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-09-23 22:23+0500\n" "POT-Creation-Date: 2025-10-16 14:54+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"
@@ -191,6 +191,10 @@ msgstr ""
msgid "Failed to delete custom data: {error}" msgid "Failed to delete custom data: {error}"
msgstr "" msgstr ""
#, python-brace-format
msgid "Added '{game_name}' successfully"
msgstr ""
msgid "Game name and executable path are required" msgid "Game name and executable path are required"
msgstr "" msgstr ""
@@ -248,13 +252,37 @@ msgstr ""
msgid "Select All" msgid "Select All"
msgstr "" msgstr ""
#, python-brace-format msgid "Open"
msgid "Launching {0}" msgstr ""
msgid "Select Dir"
msgstr ""
msgid "Prev Dir"
msgstr "" msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
msgid "Toggle"
msgstr ""
msgid "Install"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Prev Tab"
msgstr ""
msgid "Next Tab"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgstr ""
msgid "File Explorer" msgid "File Explorer"
msgstr "" msgstr ""
@@ -304,6 +332,39 @@ msgstr ""
msgid "No cover selected" msgid "No cover selected"
msgstr "" msgstr ""
msgid "Prefix Manager"
msgstr ""
msgid "Set"
msgstr ""
msgid "Libraries"
msgstr ""
msgid "Information"
msgstr ""
msgid "Fonts"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Winetricks not found. Please try again."
msgstr ""
msgid "Warning"
msgstr ""
msgid "No components selected."
msgstr ""
msgid "Installation failed. Check logs."
msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Loading Epic Games Store games..." msgid "Loading Epic Games Store games..."
msgstr "" msgstr ""
@@ -352,9 +413,6 @@ msgstr ""
msgid "Auto Install" msgid "Auto Install"
msgstr "" msgstr ""
msgid "Emulators"
msgstr ""
msgid "Wine Settings" msgid "Wine Settings"
msgstr "" msgstr ""
@@ -370,6 +428,28 @@ msgstr ""
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
msgid "Search"
msgstr ""
msgid "Installation already in progress."
msgstr ""
msgid "Failed to start installation."
msgstr ""
#, python-brace-format
msgid "Processed {} installation..."
msgstr ""
msgid "Installation completed successfully."
msgstr ""
msgid "Installation failed."
msgstr ""
msgid "Installation error."
msgstr ""
msgid "Loading Steam games..." msgid "Loading Steam games..."
msgstr "" msgstr ""
@@ -382,13 +462,106 @@ msgstr ""
msgid "Find Games ..." msgid "Find Games ..."
msgstr "" msgstr ""
msgid "Here you can configure automatic game installation..." #, python-brace-format
msgid "Added '{name}'"
msgstr "" msgstr ""
msgid "List of available emulators and their configuration..." msgid "Compatibility tool:"
msgstr "" msgstr ""
msgid "Various Wine parameters and versions..." msgid "Prefix:"
msgstr ""
msgid "Wine Configuration"
msgstr ""
msgid "Registry Editor"
msgstr ""
msgid "Command Prompt"
msgstr ""
msgid "Uninstaller"
msgstr ""
msgid "Create Prefix Backup"
msgstr ""
msgid "Load Prefix Backup"
msgstr ""
msgid "Delete Compatibility Tool"
msgstr ""
msgid "Delete Prefix"
msgstr ""
msgid "Clear Prefix"
msgstr ""
msgid "Launching tool..."
msgstr ""
msgid "Failed to start process."
msgstr ""
msgid "Confirm Clear"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgstr ""
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgstr ""
msgid "Failed to start backup process."
msgstr ""
msgid "Failed to start restore process."
msgstr ""
msgid "Prefix backup completed."
msgstr ""
msgid "Prefix backup failed."
msgstr ""
msgid "Prefix restore completed."
msgstr ""
msgid "Prefix restore failed."
msgstr ""
#, python-brace-format
msgid "Are you sure you want to delete prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' deleted."
msgstr ""
#, python-brace-format
msgid "Failed to delete prefix: {}"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to delete compatibility tool '{}'?"
msgstr ""
#, python-brace-format
msgid "Compatibility tool '{}' deleted."
msgstr ""
#, python-brace-format
msgid "Failed to delete compatibility tool: {}"
msgstr "" msgstr ""
msgid "Main PortProton parameters..." msgid "Main PortProton parameters..."
@@ -424,6 +597,9 @@ msgstr ""
msgid "Games Display Filter:" msgid "Games Display Filter:"
msgstr "" msgstr ""
msgid "Gamepad Type:"
msgstr ""
msgid "Proxy URL" msgid "Proxy URL"
msgstr "" msgstr ""
@@ -448,6 +624,12 @@ msgstr ""
msgid "Application Fullscreen Mode:" msgid "Application Fullscreen Mode:"
msgstr "" msgstr ""
msgid "Minimize to tray on close"
msgstr ""
msgid "Application Close Mode:"
msgstr ""
msgid "Auto Fullscreen on Gamepad connected" msgid "Auto Fullscreen on Gamepad connected"
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-09-23 22:23+0500\n" "POT-Creation-Date: 2025-10-16 14:54+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"
@@ -189,6 +189,10 @@ msgstr ""
msgid "Failed to delete custom data: {error}" msgid "Failed to delete custom data: {error}"
msgstr "" msgstr ""
#, python-brace-format
msgid "Added '{game_name}' successfully"
msgstr ""
msgid "Game name and executable path are required" msgid "Game name and executable path are required"
msgstr "" msgstr ""
@@ -246,13 +250,37 @@ msgstr ""
msgid "Select All" msgid "Select All"
msgstr "" msgstr ""
#, python-brace-format msgid "Open"
msgid "Launching {0}" msgstr ""
msgid "Select Dir"
msgstr ""
msgid "Prev Dir"
msgstr "" msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr ""
msgid "Toggle"
msgstr ""
msgid "Install"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Prev Tab"
msgstr ""
msgid "Next Tab"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgstr ""
msgid "File Explorer" msgid "File Explorer"
msgstr "" msgstr ""
@@ -302,6 +330,39 @@ msgstr ""
msgid "No cover selected" msgid "No cover selected"
msgstr "" msgstr ""
msgid "Prefix Manager"
msgstr ""
msgid "Set"
msgstr ""
msgid "Libraries"
msgstr ""
msgid "Information"
msgstr ""
msgid "Fonts"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Winetricks not found. Please try again."
msgstr ""
msgid "Warning"
msgstr ""
msgid "No components selected."
msgstr ""
msgid "Installation failed. Check logs."
msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Loading Epic Games Store games..." msgid "Loading Epic Games Store games..."
msgstr "" msgstr ""
@@ -350,9 +411,6 @@ msgstr ""
msgid "Auto Install" msgid "Auto Install"
msgstr "" msgstr ""
msgid "Emulators"
msgstr ""
msgid "Wine Settings" msgid "Wine Settings"
msgstr "" msgstr ""
@@ -368,6 +426,28 @@ msgstr ""
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
msgid "Search"
msgstr ""
msgid "Installation already in progress."
msgstr ""
msgid "Failed to start installation."
msgstr ""
#, python-brace-format
msgid "Processed {} installation..."
msgstr ""
msgid "Installation completed successfully."
msgstr ""
msgid "Installation failed."
msgstr ""
msgid "Installation error."
msgstr ""
msgid "Loading Steam games..." msgid "Loading Steam games..."
msgstr "" msgstr ""
@@ -380,13 +460,106 @@ msgstr ""
msgid "Find Games ..." msgid "Find Games ..."
msgstr "" msgstr ""
msgid "Here you can configure automatic game installation..." #, python-brace-format
msgid "Added '{name}'"
msgstr "" msgstr ""
msgid "List of available emulators and their configuration..." msgid "Compatibility tool:"
msgstr "" msgstr ""
msgid "Various Wine parameters and versions..." msgid "Prefix:"
msgstr ""
msgid "Wine Configuration"
msgstr ""
msgid "Registry Editor"
msgstr ""
msgid "Command Prompt"
msgstr ""
msgid "Uninstaller"
msgstr ""
msgid "Create Prefix Backup"
msgstr ""
msgid "Load Prefix Backup"
msgstr ""
msgid "Delete Compatibility Tool"
msgstr ""
msgid "Delete Prefix"
msgstr ""
msgid "Clear Prefix"
msgstr ""
msgid "Launching tool..."
msgstr ""
msgid "Failed to start process."
msgstr ""
msgid "Confirm Clear"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgstr ""
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgstr ""
msgid "Failed to start backup process."
msgstr ""
msgid "Failed to start restore process."
msgstr ""
msgid "Prefix backup completed."
msgstr ""
msgid "Prefix backup failed."
msgstr ""
msgid "Prefix restore completed."
msgstr ""
msgid "Prefix restore failed."
msgstr ""
#, python-brace-format
msgid "Are you sure you want to delete prefix '{}'?"
msgstr ""
#, python-brace-format
msgid "Prefix '{}' deleted."
msgstr ""
#, python-brace-format
msgid "Failed to delete prefix: {}"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to delete compatibility tool '{}'?"
msgstr ""
#, python-brace-format
msgid "Compatibility tool '{}' deleted."
msgstr ""
#, python-brace-format
msgid "Failed to delete compatibility tool: {}"
msgstr "" msgstr ""
msgid "Main PortProton parameters..." msgid "Main PortProton parameters..."
@@ -422,6 +595,9 @@ msgstr ""
msgid "Games Display Filter:" msgid "Games Display Filter:"
msgstr "" msgstr ""
msgid "Gamepad Type:"
msgstr ""
msgid "Proxy URL" msgid "Proxy URL"
msgstr "" msgstr ""
@@ -446,6 +622,12 @@ msgstr ""
msgid "Application Fullscreen Mode:" msgid "Application Fullscreen Mode:"
msgstr "" msgstr ""
msgid "Minimize to tray on close"
msgstr ""
msgid "Application Close Mode:"
msgstr ""
msgid "Auto Fullscreen on Gamepad connected" msgid "Auto Fullscreen on Gamepad connected"
msgstr "" msgstr ""

View File

@@ -9,18 +9,17 @@ 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-09-23 22:23+0500\n" "POT-Creation-Date: 2025-10-16 14:54+0500\n"
"PO-Revision-Date: 2025-09-23 22:23+0500\n" "PO-Revision-Date: 2025-10-16 14:54+0500\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: ru_RU <LL@li.org>\n"
"Language: ru_RU\n" "Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 "
"&& (n%100<10 || n%100>=20) ? 1 : 2);\n"
"Generated-By: Babel 2.17.0\n" "Generated-By: Babel 2.17.0\n"
"X-Generator: Poedit 3.6\n"
msgid "Error" msgid "Error"
msgstr "Ошибка" msgstr "Ошибка"
@@ -87,11 +86,11 @@ msgstr "Успешно"
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"'{game_name}' was added to Steam. Please restart Steam for changes to take " "'{game_name}' was added to Steam. Please restart Steam for changes to "
"effect." "take effect."
msgstr "" msgstr ""
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите Steam, " "'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите "
"чтобы изменения вступили в силу." "Steam, чтобы изменения вступили в силу."
#, python-brace-format #, python-brace-format
msgid "Executable not found for game: {game_name}" msgid "Executable not found for game: {game_name}"
@@ -179,11 +178,11 @@ msgstr "Подтвердите удаление"
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"Are you sure you want to delete '{game_name}'? This will remove the .desktop " "Are you sure you want to delete '{game_name}'? This will remove the "
"file and custom data." ".desktop file and custom data."
msgstr "" msgstr ""
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению файла ." "Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению "
"desktop и пользовательских данных." "файла .desktop и пользовательских данных."
#, python-brace-format #, python-brace-format
msgid "Failed to delete .desktop file: {error}" msgid "Failed to delete .desktop file: {error}"
@@ -197,6 +196,10 @@ msgstr "'{game_name}' был(а) успешно удалён(а)"
msgid "Failed to delete custom data: {error}" msgid "Failed to delete custom data: {error}"
msgstr "Не удалось удалить пользовательские данные: {error}" msgstr "Не удалось удалить пользовательские данные: {error}"
#, python-brace-format
msgid "Added '{game_name}' successfully"
msgstr "'{game_name}' успешно добавлен(а)"
msgid "Game name and executable path are required" msgid "Game name and executable path are required"
msgstr "Требуются название игры и путь к исполняемому файлу" msgstr "Требуются название игры и путь к исполняемому файлу"
@@ -225,11 +228,11 @@ msgstr "Не удалось добавить '{game_name}' в Steam: {error}"
#, python-brace-format #, python-brace-format
msgid "" msgid ""
"'{game_name}' was removed from Steam. Please restart Steam for changes to take " "'{game_name}' was removed from Steam. Please restart Steam for changes to"
"effect." " take effect."
msgstr "" msgstr ""
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam, чтобы " "'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam,"
"изменения вступили в силу." " чтобы изменения вступили в силу."
#, python-brace-format #, python-brace-format
msgid "Failed to remove game '{game_name}' from Steam: {error}" msgid "Failed to remove game '{game_name}' from Steam: {error}"
@@ -256,13 +259,37 @@ msgstr "Удалить"
msgid "Select All" msgid "Select All"
msgstr "Выбрать всё" msgstr "Выбрать всё"
#, python-brace-format msgid "Open"
msgid "Launching {0}" msgstr "Открыть"
msgstr "Идёт запуск {0}"
msgid "Select Dir"
msgstr "Выбрать папку"
msgid "Prev Dir"
msgstr "Предыдущий каталог"
msgid "Cancel" msgid "Cancel"
msgstr "Отмена" msgstr "Отмена"
msgid "Toggle"
msgstr "Переключить"
msgid "Install"
msgstr "Установить"
msgid "Force Install"
msgstr "Принудительно установить"
msgid "Prev Tab"
msgstr "Предыдущая вкладка"
msgid "Next Tab"
msgstr "Следующая вкладка"
#, python-brace-format
msgid "Launching {0}"
msgstr "Идёт запуск {0}"
msgid "File Explorer" msgid "File Explorer"
msgstr "Проводник" msgstr "Проводник"
@@ -274,7 +301,7 @@ msgstr "Путь: "
#, python-format #, python-format
msgid "Access denied: %s" msgid "Access denied: %s"
msgstr "Доступ запрещен: %s" msgstr "Доступ запрещён: %s"
msgid "Edit Game" msgid "Edit Game"
msgstr "Редактировать игру" msgstr "Редактировать игру"
@@ -312,6 +339,39 @@ msgstr "Скачивание обложки..."
msgid "No cover selected" msgid "No cover selected"
msgstr "Обложка не выбрана" msgstr "Обложка не выбрана"
msgid "Prefix Manager"
msgstr "Менеджер префиксов"
msgid "Set"
msgstr "Выбор"
msgid "Libraries"
msgstr "Библиотеки"
msgid "Information"
msgstr "Описание"
msgid "Fonts"
msgstr "Шрифты"
msgid "Settings"
msgstr "Настройки"
msgid "Winetricks not found. Please try again."
msgstr "Winetricks не найден. Повторите попытку."
msgid "Warning"
msgstr "Предупреждение"
msgid "No components selected."
msgstr "Не выбрано ни одного компонента."
msgid "Installation failed. Check logs."
msgstr "Установка не удалась. Проверьте журналы."
msgid "Components installed successfully."
msgstr "Компоненты успешно установлены."
msgid "Loading Epic Games Store games..." msgid "Loading Epic Games Store games..."
msgstr "Загрузка игр из Epic Games Store..." msgstr "Загрузка игр из Epic Games Store..."
@@ -360,9 +420,6 @@ msgstr "Библиотека"
msgid "Auto Install" msgid "Auto Install"
msgstr "Автоустановка" msgstr "Автоустановка"
msgid "Emulators"
msgstr "Эмуляторы"
msgid "Wine Settings" msgid "Wine Settings"
msgstr "Настройки wine" msgstr "Настройки wine"
@@ -378,6 +435,28 @@ msgstr "Назад"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Полный экран" msgstr "Полный экран"
msgid "Search"
msgstr "Поиск"
msgid "Installation already in progress."
msgstr "Установка уже выполняется."
msgid "Failed to start installation."
msgstr "Не удалось запустить установку."
#, python-brace-format
msgid "Processed {} installation..."
msgstr "В процессе установки {}..."
msgid "Installation completed successfully."
msgstr "Установка завершена успешно."
msgid "Installation failed."
msgstr "Установка не удалась."
msgid "Installation error."
msgstr "Ошибка установки."
msgid "Loading Steam games..." msgid "Loading Steam games..."
msgstr "Загрузка игр из Steam..." msgstr "Загрузка игр из Steam..."
@@ -390,14 +469,109 @@ msgstr "Игровая библиотека"
msgid "Find Games ..." msgid "Find Games ..."
msgstr "Найти игры..." msgstr "Найти игры..."
msgid "Here you can configure automatic game installation..." #, python-brace-format
msgstr "Здесь можно настроить автоматическую установку игр..." msgid "Added '{name}'"
msgstr "'{name}' добавлен(а)"
msgid "List of available emulators and their configuration..." msgid "Compatibility tool:"
msgstr "Список доступных эмуляторов и их настройка..." msgstr "Инструмент совместимости:"
msgid "Various Wine parameters and versions..." msgid "Prefix:"
msgstr "Различные параметры и версии wine..." msgstr "Префикс:"
msgid "Wine Configuration"
msgstr "Конфигурация Wine"
msgid "Registry Editor"
msgstr "Редактор реестра"
msgid "Command Prompt"
msgstr "Командная строка"
msgid "Uninstaller"
msgstr "Удаление программ"
msgid "Create Prefix Backup"
msgstr "Создать резервную копию префикса"
msgid "Load Prefix Backup"
msgstr "Загрузить резервную копию префикса"
msgid "Delete Compatibility Tool"
msgstr "Удалить Инструмент совместимости"
msgid "Delete Prefix"
msgstr "Удалить Префикс"
msgid "Clear Prefix"
msgstr "Очистить Префикс"
msgid "Launching tool..."
msgstr "Запуск инструмента..."
msgid "Failed to start process."
msgstr "Не удалось запустить процесс."
msgid "Confirm Clear"
msgstr "Подтвердите очистку"
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
#, python-brace-format
msgid "Prefix '{}' cleared successfully."
msgstr "Префикс '{}' успешно удален."
#, python-brace-format
msgid ""
"Prefix '{}' cleared with errors:\n"
"{}"
msgstr ""
"Префикс '{}' очищен с ошибками:\n"
"{}"
msgid "Failed to start backup process."
msgstr "Не удалось запустить процесс резервного копирования."
msgid "Failed to start restore process."
msgstr "Не удалось запустить процесс восстановления."
msgid "Prefix backup completed."
msgstr "Резервное копирование префикса завершено."
msgid "Prefix backup failed."
msgstr "Сбой резервного копирования префикса."
msgid "Prefix restore completed."
msgstr "Восстановление префикса завершено."
msgid "Prefix restore failed."
msgstr "Восстановление префикса не удалось."
#, python-brace-format
msgid "Are you sure you want to delete prefix '{}'?"
msgstr "Вы уверены, что хотите удалить префикс «{}»?"
#, python-brace-format
msgid "Prefix '{}' deleted."
msgstr "Префикс «{}» удален."
#, python-brace-format
msgid "Failed to delete prefix: {}"
msgstr "Не удалось удалить префикс: {}"
#, python-brace-format
msgid "Are you sure you want to delete compatibility tool '{}'?"
msgstr "Вы уверены, что хотите удалить инструмент совместимости «{}»?"
#, python-brace-format
msgid "Compatibility tool '{}' deleted."
msgstr "Инструмент совместимости «{}» удален."
#, python-brace-format
msgid "Failed to delete compatibility tool: {}"
msgstr "Не удалось удалить инструмент совместимости: {}"
msgid "Main PortProton parameters..." msgid "Main PortProton parameters..."
msgstr "Основные параметры PortProton..." msgstr "Основные параметры PortProton..."
@@ -432,6 +606,9 @@ msgstr "все"
msgid "Games Display Filter:" msgid "Games Display Filter:"
msgstr "Фильтр игр:" msgstr "Фильтр игр:"
msgid "Gamepad Type:"
msgstr "Тип геймпада:"
msgid "Proxy URL" msgid "Proxy URL"
msgstr "Адрес прокси" msgstr "Адрес прокси"
@@ -456,6 +633,12 @@ msgstr "Запуск приложения в полноэкранном режи
msgid "Application Fullscreen Mode:" msgid "Application Fullscreen Mode:"
msgstr "Режим полноэкранного отображения приложения:" msgstr "Режим полноэкранного отображения приложения:"
msgid "Minimize to tray on close"
msgstr "Сворачивать в трей при закрытии"
msgid "Application Close Mode:"
msgstr "Режим закрытия приложения:"
msgid "Auto Fullscreen on Gamepad connected" msgid "Auto Fullscreen on Gamepad connected"
msgstr "Режим полноэкранного отображения приложения при подключении геймпада" msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
@@ -482,7 +665,8 @@ msgstr "Подтвердите удаление"
msgid "Are you sure you want to reset all settings? This action cannot be undone." msgid "Are you sure you want to reset all settings? This action cannot be undone."
msgstr "" msgstr ""
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя отменить." "Вы уверены, что хотите сбросить все настройки? Это действие нельзя "
"отменить."
msgid "Settings reset. Restarting..." msgid "Settings reset. Restarting..."
msgstr "Настройки сброшены. Перезапуск..." msgstr "Настройки сброшены. Перезапуск..."
@@ -654,3 +838,4 @@ msgstr "Нет избранных"
msgid "No recent games" msgid "No recent games"
msgstr "Нет недавних игр" msgstr "Нет недавних игр"

File diff suppressed because it is too large Load Diff

View File

@@ -4,9 +4,12 @@ import orjson
import requests import requests
import urllib.parse import urllib.parse
import time import time
import glob
import re
from collections.abc import Callable from collections.abc import Callable
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.config_utils import get_portproton_location
logger = get_logger(__name__) logger = get_logger(__name__)
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
@@ -52,6 +55,9 @@ class PortProtonAPI:
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share")) self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data") self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
os.makedirs(self.custom_data_dir, exist_ok=True) os.makedirs(self.custom_data_dir, exist_ok=True)
self.portproton_location = get_portproton_location()
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
self._topics_data = None self._topics_data = None
def _get_game_dir(self, exe_name: str) -> str: def _get_game_dir(self, exe_name: str) -> str:
@@ -68,40 +74,6 @@ class PortProtonAPI:
logger.debug(f"Failed to check file at {url}: {e}") logger.debug(f"Failed to check file at {url}: {e}")
return False return False
def download_game_assets(self, exe_name: str, timeout: int = 5) -> dict[str, str | None]:
game_dir = self._get_game_dir(exe_name)
results: dict[str, str | None] = {"cover": None, "metadata": None}
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
cover_url_base = f"{self.base_url}/{exe_name}/cover"
metadata_url = f"{self.base_url}/{exe_name}/metadata.txt"
for ext in cover_extensions:
cover_url = f"{cover_url_base}{ext}"
if self._check_file_exists(cover_url, timeout):
local_cover_path = os.path.join(game_dir, f"cover{ext}")
result = self.downloader.download(cover_url, local_cover_path, timeout=timeout)
if result:
results["cover"] = result
logger.info(f"Downloaded cover for {exe_name} to {result}")
break
else:
logger.error(f"Failed to download cover for {exe_name} from {cover_url}")
else:
logger.debug(f"No cover found for {exe_name} with extension {ext}")
if self._check_file_exists(metadata_url, timeout):
local_metadata_path = os.path.join(game_dir, "metadata.txt")
result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout)
if result:
results["metadata"] = result
logger.info(f"Downloaded metadata for {exe_name} to {result}")
else:
logger.error(f"Failed to download metadata for {exe_name} from {metadata_url}")
else:
logger.debug(f"No metadata found for {exe_name}")
return results
def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None: def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None:
game_dir = self._get_game_dir(exe_name) game_dir = self._get_game_dir(exe_name)
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"] cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
@@ -163,6 +135,164 @@ class PortProtonAPI:
if callback: if callback:
callback(results) callback(results)
def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None:
"""Download only autoinstall cover image (PNG only, no metadata)."""
xdg_data_home = os.getenv("XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share"))
autoinstall_root = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
user_game_folder = os.path.join(autoinstall_root, exe_name)
if not os.path.isdir(user_game_folder):
try:
os.mkdir(user_game_folder)
except FileExistsError:
pass
cover_url = f"{self.base_url}/{exe_name}/cover.png"
local_cover_path = os.path.join(user_game_folder, "cover.png")
def on_cover_downloaded(local_path: str | None):
if local_path:
logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}")
else:
logger.debug(f"No autoinstall cover downloaded for {exe_name}")
if callback:
callback(local_path)
if self._check_file_exists(cover_url, timeout):
self.downloader.download_async(
cover_url,
local_cover_path,
timeout=timeout,
callback=on_cover_downloaded
)
else:
logger.debug(f"No autoinstall cover found for {exe_name}")
if callback:
callback(None)
def parse_autoinstall_script(self, file_path: str) -> tuple[str | None, str | None]:
"""Extract display_name from # name comment and exe_name from autoinstall bash script."""
try:
with open(file_path, encoding='utf-8') as f:
content = f.read()
# Skip emulators
if re.search(r'#\s*type\s*:\s*emulators', content, re.IGNORECASE):
return None, None
display_name = None
exe_name = None
# Extract display_name from "# name:" comment
name_match = re.search(r'#\s*name\s*:\s*(.+)', content, re.IGNORECASE)
if name_match:
display_name = name_match.group(1).strip()
# --- pw_create_unique_exe ---
pw_match = re.search(r'pw_create_unique_exe(?:\s+["\']([^"\']+)["\'])?', content)
if pw_match:
arg = pw_match.group(1)
if arg:
exe_name = arg.strip()
if not exe_name.lower().endswith(".exe"):
exe_name += ".exe"
else:
export_match = re.search(
r'export\s+PORTWINE_CREATE_SHORTCUT_NAME\s*=\s*["\']([^"\']+)["\']',
content, re.IGNORECASE)
if export_match:
exe_name = f"{export_match.group(1).strip()}.exe"
else:
portwine_match = None
for line in content.splitlines():
stripped = line.strip()
if stripped.startswith("#"):
continue
if "portwine_exe" in stripped and "=" in stripped:
portwine_match = stripped
break
if portwine_match:
exe_expr = portwine_match.split("=", 1)[1].strip().strip("'\" ")
exe_candidates = re.findall(r'[-\w\s/\\\.]+\.exe', exe_expr)
if exe_candidates:
exe_name = os.path.basename(exe_candidates[-1].strip())
# Fallback
if not display_name and exe_name:
display_name = exe_name
return display_name, exe_name
except Exception as e:
logger.error(f"Failed to parse {file_path}: {e}")
return None, None
def get_autoinstall_games_async(self, callback: Callable[[list[tuple]], None]) -> None:
"""Load auto-install games with user/builtin covers (no async download here)."""
games = []
auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall") if self.portproton_location else ""
if not os.path.exists(auto_dir):
callback(games)
return
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
if not scripts:
callback(games)
return
xdg_data_home = os.getenv("XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share"))
base_autoinstall_dir = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
os.makedirs(base_autoinstall_dir, exist_ok=True)
for script_path in scripts:
display_name, exe_name = self.parse_autoinstall_script(script_path)
script_name = os.path.splitext(os.path.basename(script_path))[0]
if not (display_name and exe_name):
continue
exe_name = os.path.splitext(exe_name)[0] # Без .exe
user_game_folder = os.path.join(base_autoinstall_dir, exe_name)
os.makedirs(user_game_folder, exist_ok=True)
# Поиск обложки
cover_path = ""
user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set()
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
candidate = f"cover{ext}"
if candidate in user_files:
cover_path = os.path.join(user_game_folder, candidate)
break
if not cover_path:
logger.debug(f"No local cover found for autoinstall {exe_name}")
# Формируем кортеж игры (добавлен exe_name в конец)
game_tuple = (
display_name, # name
"", # description
cover_path, # cover
"", # appid
f"autoinstall:{script_name}", # exec_line
"", # controller_support
"Never", # last_launch
"0h 0m", # formatted_playtime
"", # protondb_tier
"", # anticheat_status
0, # last_played
0, # playtime_seconds
"autoinstall", # game_source
exe_name # exe_name
)
games.append(game_tuple)
callback(games)
def _load_topics_data(self): def _load_topics_data(self):
"""Load and cache linux_gaming_topics_min.json from the archive.""" """Load and cache linux_gaming_topics_min.json from the archive."""
if self._topics_data is not None: if self._topics_data is not None:

49
portprotonqt/preloader.py Normal file
View File

@@ -0,0 +1,49 @@
import time
from PySide6.QtCore import QRect
from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient
from PySide6.QtWidgets import QWidget
class Preloader(QWidget):
def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None):
super().__init__(parent)
self.setFixedSize(150, 150)
self._speed = speed
self._line_width = line_line_width
self._color1 = color
self._color2 = QColor(color.red(), color.green(), color.blue(), 0)
self._start_time = time.time()
def showEvent(self, event):
self._start_time = time.time()
def paintEvent(self, event):
rect = self._get_preloader_rect()
center = rect.center()
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setPen(self._get_pen())
painter.translate(center)
painter.rotate(self._get_angle())
painter.translate(-center)
painter.drawArc(rect, 0, 270 * 16)
self.update()
def _get_pen(self) -> QPen:
gradient = QConicalGradient()
gradient.setCenter(self.rect().center())
gradient.setColorAt(0, self._color1)
gradient.setColorAt(1, self._color2)
pen = QPen(QBrush(gradient), self._line_width)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
return pen
def _get_angle(self) -> float:
duration = time.time() - self._start_time
return (self._speed * duration) % 360.0
def _get_preloader_rect(self) -> QRect:
size = self._line_width // 2
rect = self.rect()
rect.adjust(size, size, -size, -size)
return rect

View File

@@ -211,14 +211,28 @@ def normalize_name(s):
def is_valid_candidate(candidate): def is_valid_candidate(candidate):
""" """
Checks if a candidate contains forbidden substrings: Determines whether a given candidate string is valid for use as a game name.
- win32
- win64 The function performs the following checks:
- gamelauncher 1. Normalizes the candidate using `normalize_name()`.
Additionally checks the string without spaces. 2. Rejects the candidate if the normalized name is exactly "game"
Returns True if the candidate is valid, otherwise False. (to avoid overly generic names).
3. Removes spaces and checks for forbidden substrings:
- "win32"
- "win64"
- "gamelauncher"
These are checked in the space-free version of the string.
4. Returns True only if none of the forbidden conditions are met.
Args:
candidate (str): The candidate string to validate.
Returns:
bool: True if the candidate is valid, False otherwise.
""" """
normalized_candidate = normalize_name(candidate) normalized_candidate = normalize_name(candidate)
if normalized_candidate == "game":
return False
normalized_no_space = normalized_candidate.replace(" ", "") normalized_no_space = normalized_candidate.replace(" ", "")
forbidden = ["win32", "win64", "gamelauncher"] forbidden = ["win32", "win64", "gamelauncher"]
for token in forbidden: for token in forbidden:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g><rect x="1" y="6" width="46" height="36" rx="5" ry="5" fill="#3f424d" stroke-width="1.1506"/><rect x="4.2329" y="8.5301" width="39.534" height="30.94" rx="4.2972" ry="4.2972" fill="#fff" stroke-width=".98888"/><path d="m23.24 22.785c-0.67917 0.69059-0.67818 1.807 0 2.4913l8.0309 8.1037c1.8756 1.8787 4.6892-0.93962 2.8136-2.8183l-3.5038-3.5097c-0.58434-0.58533-0.39618-1.0598 0.44066-1.0598h9.6139c1.0992 0 1.9895-0.89179 1.9895-1.9928 0-1.1005-0.89028-1.9928-1.9895-1.9928h-9.6139c-0.82771 0-1.0277-0.47176-0.44066-1.0597l3.5038-3.5093c1.8756-1.8787-0.93803-4.6971-2.8136-2.8183z" fill="#3f424d" fill-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="48"
height="48"
version="1.1"
viewBox="0 0 48 48"
xml:space="preserve"
id="svg2"
sodipodi:docname="key_context.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs2" /><sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="8.6915209"
inkscape:cx="72.311855"
inkscape:cy="22.780823"
inkscape:window-width="2560"
inkscape:window-height="1406"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" /><path
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#686e7e;fill-opacity:1;stroke-width:0.554217;enable-background:accumulate;stop-color:#000000"
d="m 17.400964,38.281601 -0.04068,-15.381724 c -0.0087,-3.288656 2.401967,-6.020242 5.542168,-6.550475 V 7.4098472 C 11.174091,7.9874382 1.8422139,17.678792 1.8422139,29.550445 v 8.911269 c 3.429133,2.844892 11.5678151,2.890776 15.5587501,-0.180113 z"
id="path10"
sodipodi:nodetypes="csccscc" /><path
fill="#000000"
d="m 23.956256,40.5905 h -9e-6 c -2.438553,0 -4.433731,-1.995178 -4.433731,-4.43373 V 25.072424 c 0,-2.438552 1.995178,-4.433731 4.433731,-4.433731 h 9e-6 c 2.438552,0 4.43373,1.995179 4.43373,4.433731 V 36.15677 c 0,2.438552 -1.995178,4.43373 -4.43373,4.43373 z"
id="path2"
style="fill:#686e7e;fill-opacity:1;stroke-width:0.554217" /><g
id="g15"
transform="matrix(0.97480136,0,0,0.99852328,1.4840752,1.6593149)"><path
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#ffffff;stroke-width:0.95333;enable-background:accumulate;stop-color:#000000"
d="m 30.231637,35.990171 0.03878,-14.663865 c 0.0083,-3.135176 -2.289868,-5.73928 -5.283518,-6.244767 V 6.5591888 C 36.167905,7.1098239 45.209208,16.349815 45.064267,27.666494 l -0.109685,8.563937 c -3.269097,2.712122 -10.918265,2.687312 -14.722945,-0.24026 z"
id="path14" /><path
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#686e7e;fill-opacity:1;stroke-width:0.95333;enable-background:accumulate;stop-color:#000000"
d="m 24.224126,5.7586892 v 9.9671448 l 0.634933,0.107994 c 2.632815,0.444559 4.656653,2.729598 4.649348,5.490959 l -0.04096,15.03916 0.299778,0.230885 c 2.097287,1.613791 5.093143,2.357986 8.017658,2.392636 2.924514,0.03465 5.796042,-0.625772 7.656435,-2.169199 l 0.271848,-0.2253 0.113581,-8.91699 C 45.976953,15.94787 36.604257,6.3680498 25.024774,5.7977906 Z m 1.524956,1.6795 C 36.150995,8.3658717 44.437912,17.028984 44.301786,27.65736 l -0.104271,8.114479 c -1.445908,1.069255 -3.851487,1.720797 -6.394017,1.690673 -2.543438,-0.03013 -5.090881,-0.734663 -6.807375,-1.934591 l 0.03724,-14.199409 c 0.0087,-3.271088 -2.263607,-5.953645 -5.284281,-6.771998 z"
id="path15" /></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 874 B

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m17.977 16.26h11.807v2.6476h-8.086v3.554h7.2989v2.6476h-7.2989v3.9834h8.3245v2.6476h-12.046z" fill="#3f424d" stroke-width=".4977" aria-label="E"/></svg>

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6 6h36c2.77 0 5 2.23 5 5v26c0 2.77-2.23 5-5 5h-36c-2.77 0-5-2.23-5-5v-26c0-2.77 2.23-5 5-5z" fill="#3f424d" stroke-width="1.1506"/><path d="m8.5301 8.5301h30.94c2.3806 0 4.2972 1.9166 4.2972 4.2972v22.346c0 2.3806-1.9166 4.2972-4.2972 4.2972h-30.94c-2.3806 0-4.2972-1.9166-4.2972-4.2972v-22.346c0-2.3806 1.9166-4.2972 4.2972-4.2972z" fill="#fff" stroke-width=".98888"/><path d="m8.2952 18.538h8.3321v1.8684h-5.7063v2.5081h5.1508v1.8684h-5.1508v2.811h5.8746v1.8684h-8.5005zm10.268 0h2.6596l5.2854 7.4568v-7.4568h2.3397v10.924h-2.6596l-5.2854-7.5747v7.5747h-2.3397zm15.166 1.8684h-3.3665v-1.8684h9.3421v1.8684h-3.3497v9.0559h-2.6259z" fill="#3f424d" stroke-width=".35123" aria-label="ENT"/></svg>

After

Width:  |  Height:  |  Size: 823 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 B

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m11.139 18.538h8.5005v1.8684h-5.8746v2.6764h5.3191v1.8684h-5.3191v4.5111h-2.6259zm13.5 2.5754-2.9794 1.6496v-2.0704l3.4507-2.1546h1.9862v10.924h-2.4576zm9.7629 0-2.9794 1.6496v-2.0704l3.4507-2.1546h1.9862v10.924h-2.4576z" fill="#3f424d" stroke-width=".35123" aria-label="F11"/></svg>

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 B

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m26.619 34a1.9874 1.9874 0 0 1-1.3812-0.55623l-7.5143-7.2497a3.0457 3.0457 0 0 1 0-4.3873l7.5143-7.2497a1.9882 1.9882 0 0 1 2.7603 2.8624l-6.8226 6.581 6.8226 6.581a1.9874 1.9874 0 0 1-1.3791 3.4186z" fill="#3f424d" stroke-width=".20832"/></svg>

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 956 B

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m20.778 34a1.9874 1.9874 0 0 0 1.3812-0.55623l7.5143-7.2497a3.0457 3.0457 0 0 0 0-4.3873l-7.5143-7.2497a1.9882 1.9882 0 0 0-2.7603 2.8624l6.8226 6.581-6.8226 6.581a1.9874 1.9874 0 0 0 1.3791 3.4186z" fill="#3f424d" stroke-width=".20832"/></svg>

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m24 13.476c-5.7918 0-10.524 4.7162-10.524 10.524 0 5.7918 4.7162 10.524 10.524 10.524 5.7918 0 10.524-4.7162 10.524-10.524 0-5.7918-4.7162-10.524-10.524-10.524zm0 18.037c-4.137 0-7.5128-3.3758-7.5128-7.5128s3.3758-7.5128 7.5128-7.5128 7.5128 3.3758 7.5128 7.5128-3.3592 7.5128-7.5128 7.5128z" fill="#3f424d" stroke-width="1.6548"/></svg>

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m34.076 13.91c-0.57906-0.57906-1.5387-0.57906-2.1177 0l-7.958 7.958-7.958-7.958c-0.57906-0.57906-1.5387-0.57906-2.1177 0-0.57906 0.57906-0.57906 1.5387 0 2.1177l7.958 7.958-7.958 7.958c-0.57906 0.57906-0.57906 1.5387 0 2.1177 0.2978 0.2978 0.67833 0.44671 1.0589 0.44671 0.38053 0 0.76106-0.1489 1.0589-0.44671l7.958-7.9415 7.958 7.958c0.2978 0.2978 0.67833 0.44671 1.0589 0.44671s0.76106-0.1489 1.0589-0.44671c0.57906-0.57906 0.57906-1.5387 0-2.1177l-7.958-7.958 7.958-7.958c0.57906-0.59561 0.57906-1.5387 0-2.1343z" fill="#3f424d" stroke-width="1.6545"/></svg>

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m10.465 39.437c4.1391 1.4258 20.596 4.9156 31.79 2.551 2.7034-0.57104 4.7508-3.32 4.744-6.0831l-0.057386-23.467c-0.009676-3.9677-4.6895-7.2319-7.5124-7.2255-12.075 0.0276-22.278-0.0068827-33.557 1.5493-2.7371 0.37765-4.8753 4.0033-4.8727 6.7663l0.016807 17.988c0.00451 4.8315 6.0288 6.743 9.4485 7.921z" fill="#3f424d" stroke-width="1.1477"/><path d="m12.394 37.236c3.5492 1.2226 17.661 4.2149 27.259 2.1874 2.3181-0.48964 4.0736-2.8468 4.0678-5.216l-0.049207-20.123c-0.008279-3.4022-4.0211-6.2011-6.4416-6.1956-10.354 0.023666-19.103-0.0059052-28.774 1.3285-2.347 0.32383-4.1804 3.4327-4.1782 5.802l0.014412 15.424c0.00387 4.1428 5.1694 5.7819 8.1017 6.792z" fill="#fff" stroke-width=".98413"/><path d="m13.833 16.812h3.4556v11.917h7.0662v2.4588h-10.522zm17.101 3.3891-3.9207 2.1708v-2.7246l4.541-2.8353h2.6138v14.376h-3.234z" fill="#3f424d" stroke-width=".4622" aria-label="L1"/></svg>

After

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m18.047 46.216-2.1e-5 -5e-6c-5.4306-1.4551-8.6833-7.089-7.2282-12.52l6.6143-24.685c1.4551-5.4306 7.089-8.6833 12.52-7.2282l2.1e-5 5.5e-6c5.4306 1.4551 8.6833 7.089 7.2282 12.52l-6.6143 24.685c-1.4551 5.4306-7.089 8.6833-12.52 7.2282z" fill="#3f424d" stroke-width="1.2778"/><path d="m19.229 41.807-1.7e-5 -4e-6c-4.3529-1.1664-6.9601-5.6821-5.7937-10.035l5.3016-19.786c1.1664-4.3529 5.6821-6.9601 10.035-5.7937l1.7e-5 4.4e-6c4.3529 1.1664 6.9601 5.6821 5.7937 10.035l-5.3016 19.786c-1.1664 4.3529-5.6821 6.9601-10.035 5.7937z" fill="#fff" stroke-width="1.0242"/><path d="m19.502 18.291c-0.854 0-1.547 0.49867-1.547 1.114 0 0.6153 0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114 0-0.6153-0.69187-1.114-1.5459-1.114zm0 4.595c-0.854 0-1.547 0.49867-1.547 1.114 0 0.6153 0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114 0-0.6153-0.69187-1.114-1.5459-1.114zm0 4.595c-0.854 0-1.547 0.49867-1.547 1.114s0.69299 1.114 1.547 1.114h8.997c0.854 0 1.5459-0.49867 1.5459-1.114s-0.69187-1.114-1.5459-1.114z" fill="#3f424d" fill-rule="evenodd" stroke-width=".11455"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m37.535 39.437c-4.1391 1.4258-20.596 4.9156-31.79 2.551-2.7034-0.57104-4.7508-3.32-4.744-6.0831l0.057386-23.467c0.00968-3.9677 4.6895-7.2319 7.5124-7.2255 12.075 0.0276 22.278-0.00688 33.557 1.5493 2.7371 0.37765 4.8753 4.0033 4.8727 6.7663l-0.01681 17.988c-0.0045 4.8315-6.0288 6.743-9.4485 7.921z" fill="#3f424d" stroke-width="1.1477"/><path d="m35.606 37.236c-3.5492 1.2226-17.661 4.2149-27.259 2.1874-2.3181-0.48964-4.0736-2.8468-4.0678-5.216l0.049207-20.123c0.00828-3.4022 4.0211-6.2011 6.4416-6.1956 10.354 0.023666 19.103-0.00591 28.774 1.3285 2.347 0.32383 4.1804 3.4327 4.1782 5.802l-0.01441 15.424c-0.0039 4.1428-5.1694 5.7819-8.1017 6.792z" fill="#fff" stroke-width=".98413"/><path d="m12.858 16.812h6.4681q2.8796 0 4.1644 0.70883 1.2848 0.68668 1.2848 2.3259v2.5252q0 1.2626-0.90819 1.9936-0.88604 0.70883-2.3702 0.90819l4.1644 5.9143h-3.9872l-3.7657-5.6485h-1.5949v5.6485h-3.4556zm6.4238 6.4459q1.2183 0 1.6613-0.31011 0.44302-0.33226 0.44302-1.2626v-1.0189q0-0.79744-0.48732-1.0854-0.46517-0.31011-1.617-0.31011h-2.9682v3.9872zm12.626-3.0568-3.9207 2.1708v-2.7246l4.541-2.8353h2.6138v14.376h-3.234z" fill="#3f424d" stroke-width=".4622" aria-label="R1"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m29.953 46.216 2.1e-5 -5e-6c5.4306-1.4551 8.6833-7.089 7.2282-12.52l-6.6143-24.685c-1.4551-5.4306-7.089-8.6833-12.52-7.2282l-2.1e-5 5.5e-6c-5.4306 1.4551-8.6833 7.089-7.2282 12.52l6.6143 24.685c1.4551 5.4306 7.089 8.6833 12.52 7.2282z" fill="#3f424d" stroke-width="1.2778"/><path d="m28.771 41.807 1.7e-5 -4e-6c4.3529-1.1664 6.9601-5.6821 5.7937-10.035l-5.3016-19.786c-1.1664-4.3529-5.6821-6.9601-10.035-5.7937l-1.7e-5 4.4e-6c-4.3529 1.1664-6.9601 5.6821-5.7937 10.035l5.3016 19.786c1.1664 4.3529 5.6821 6.9601 10.035 5.7937z" fill="#fff" stroke-width="1.0242"/><path d="m24.034 20.416c-0.54232 0-0.98296 0.41005-0.98296 0.91636v5.3348c0 0.50632 0.44064 0.91636 0.98296 0.91636s0.98124-0.41005 0.98124-0.91636v-5.3348c0-0.50632-0.43892-0.91636-0.98124-0.91636zm-5.9615 0.72033c-0.15955 0.0017-0.31975 0.03855-0.46652 0.11513-0.46966 0.24506-0.62269 0.79993-0.34257 1.2384l2.9506 4.6191c0.28012 0.43848 0.88858 0.59512 1.3582 0.35005 0.46966-0.24506 0.62269-0.79837 0.34257-1.2369l-2.9506-4.6192c-0.19258-0.30146-0.5407-0.4705-0.89172-0.46674zm11.856 0c-0.35102-0.0037-0.69914 0.16528-0.89172 0.46674l-2.9506 4.6191c-0.28011 0.43848-0.12709 0.99179 0.34257 1.2369 0.46967 0.24506 1.0781 0.08843 1.3582-0.35005l2.9506-4.6191c0.28011-0.43848 0.12709-0.99335-0.34257-1.2384-0.14677-0.07658-0.30696-0.11342-0.46652-0.11513z" fill="#3f424d" fill-rule="evenodd" stroke-width=".082805"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m33.019 13.476h-18.037c-0.8274 0-1.5059 0.67847-1.5059 1.5059v18.037c0 0.8274 0.67847 1.5059 1.5059 1.5059h18.037c0.8274 0 1.5059-0.67847 1.5059-1.5059v-18.037c0-0.8274-0.66192-1.5059-1.5059-1.5059zm-1.4893 18.037h-15.026v-15.026h15.026z" fill="#3f424d" stroke-width="1.6548"/></svg>

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m13.766 32.511h20.449c0.60033 0 1.1631-0.31892 1.4821-0.84421 0.30016-0.52529 0.30016-1.1819 0-1.7072l-10.224-17.71c-0.60033-1.0506-2.345-1.0506-2.9454 0l-10.224 17.71c-0.30016 0.52529-0.30016 1.1819 0 1.7072s0.86297 0.84421 1.4633 0.84421zm10.224-15.984 7.2602 12.588h-14.539z" fill="#3f424d" stroke-width="1.876"/></svg>

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.016 13.475h6.1623l7.5893 21.049h-5.1244l-1.8811-5.546h-7.6866l-1.8487 5.546h-4.9947zm5.6433 12.13-2.6595-7.9137h-0.12973l-2.6595 7.9137z" fill="#3f424d" stroke-width=".67675" aria-label="A"/></svg>

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m15.973 13.476h8.5299q3.0163 0 4.6379 0.45406 1.6541 0.42163 2.3352 1.3946 0.68109 0.94056 0.68109 2.6595v2.5946q0 0.87569-0.71353 1.6541-0.68109 0.77839-1.6216 1.0703v0.16216q1.2325 0.12973 2.2379 1.0703 1.0379 0.90812 1.0379 2.0433v3.2433q0 2.5622-2.0433 3.6325t-6.3244 1.0703h-8.7569zm8.5299 8.5623q1.2 0 1.7838-0.1946t0.77839-0.61623q0.22703-0.45406 0.22703-1.2973v-1.0379q0-0.74596-0.1946-1.1027-0.1946-0.3892-0.81082-0.55136-0.58379-0.16216-1.8811-0.16216h-3.373v4.9622zm0.12973 8.8866q1.8487 0 2.6271-0.42163t0.77839-1.3622v-1.6865q0-1.1676-0.61623-1.6541-0.58379-0.4865-2.1081-0.4865h-4.2812v5.6109z" fill="#3f424d" stroke-width=".67675" aria-label="B"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6.3645 12.915h35.271c2.9719 0 5.3645 2.3926 5.3645 5.3645v11.442c0 2.9719-2.3926 5.3645-5.3645 5.3645h-35.271c-2.9719 0-5.3645-2.3926-5.3645-5.3645v-11.442c0-2.9719 2.3926-5.3645 5.3645-5.3645z" fill="#3f424d"/><path d="m9.2118 14.704h29.576c2.4921 0 4.4983 2.0062 4.4983 4.4983v9.5944c0 2.4921-2.0062 4.4983-4.4983 4.4983h-29.576c-2.4921 0-4.4983-2.0062-4.4983-4.4983v-9.5944c0-2.4921 2.0062-4.4983 4.4983-4.4983z" fill="#fff"/><path d="m13.757 18h2.8844v9.9476h5.8983v2.0524h-8.7827zm10.724 0h4.8629q1.7196 0 2.6441 0.25886 0.94299 0.24037 1.3313 0.79507 0.38829 0.53621 0.38829 1.5162v1.4792q0 0.49923-0.40678 0.94299-0.38829 0.44376-0.9245 0.61017v0.09245q0.70262 0.07396 1.2758 0.61017 0.59168 0.51772 0.59168 1.1649v1.849q0 1.4607-1.1649 2.0709-1.1649 0.61017-3.6055 0.61017h-4.9923zm4.8629 4.8814q0.68413 0 1.0169-0.11094 0.33282-0.11094 0.44376-0.35131 0.12943-0.25886 0.12943-0.7396v-0.59168q0-0.42527-0.11094-0.62866-0.11094-0.22188-0.46225-0.31433-0.33282-0.09245-1.0724-0.09245h-1.923v2.829zm0.07396 5.0663q1.0539 0 1.4977-0.24037 0.44376-0.24037 0.44376-0.77658v-0.96148q0-0.66564-0.35131-0.94299-0.33282-0.27735-1.2018-0.27735h-2.4407v3.1988z" fill="#3f424d" stroke-width=".38581" aria-label="LB"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m6.3645 12.915h35.271c2.9719 0 5.3645 2.3926 5.3645 5.3645v11.442c0 2.9719-2.3926 5.3645-5.3645 5.3645h-35.271c-2.9719 0-5.3645-2.3926-5.3645-5.3645v-11.442c0-2.9719 2.3926-5.3645 5.3645-5.3645z" fill="#3f424d"/><path d="m9.2118 14.704h29.576c2.4921 0 4.4983 2.0062 4.4983 4.4983v9.5944c0 2.4921-2.0062 4.4983-4.4983 4.4983h-29.576c-2.4921 0-4.4983-2.0062-4.4983-4.4983v-9.5944c0-2.4921 2.0062-4.4983 4.4983-4.4983z" fill="#fff"/><path d="m12.943 18h5.3991q2.4037 0 3.4761 0.59168 1.0724 0.57319 1.0724 1.9414v2.1079q0 1.0539-0.75809 1.6641-0.7396 0.59168-1.9784 0.75809l3.4761 4.9368h-3.3282l-3.1433-4.7149h-1.3313v4.7149h-2.8844zm5.3621 5.3806q1.0169 0 1.3867-0.25886 0.3698-0.27735 0.3698-1.0539v-0.85054q0-0.66564-0.40678-0.90601-0.38829-0.25886-1.3498-0.25886h-2.4777v3.3282zm6.9892-5.3806h4.8629q1.7196 0 2.6441 0.25886 0.94299 0.24037 1.3313 0.79507 0.38829 0.53621 0.38829 1.5162v1.4792q0 0.49923-0.40678 0.94299-0.38829 0.44376-0.9245 0.61017v0.09245q0.70262 0.07396 1.2758 0.61017 0.59168 0.51772 0.59168 1.1649v1.849q0 1.4607-1.1649 2.0709-1.1649 0.61017-3.6055 0.61017h-4.9923zm4.8629 4.8814q0.68413 0 1.017-0.11094 0.33282-0.11094 0.44376-0.35131 0.12943-0.25886 0.12943-0.7396v-0.59168q0-0.42527-0.11094-0.62866-0.11094-0.22188-0.46225-0.31433-0.33282-0.09245-1.0724-0.09245h-1.923v2.829zm0.07396 5.0663q1.0539 0 1.4977-0.24037 0.44376-0.24037 0.44376-0.77658v-0.96148q0-0.66564-0.35131-0.94299-0.33282-0.27735-1.2018-0.27735h-2.4407v3.1988z" fill="#3f424d" stroke-width=".38581" aria-label="RB"/></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m16.169 14.061c-1.4868 0-2.6934 0.8682-2.6934 1.9395 0 1.0713 1.2065 1.9395 2.6934 1.9395h15.664c1.4868 0 2.6914-0.8682 2.6914-1.9395 0-1.0713-1.2046-1.9395-2.6914-1.9395zm0 8c-1.4868 0-2.6934 0.8682-2.6934 1.9395 0 1.0713 1.2065 1.9395 2.6934 1.9395h15.664c1.4868 0 2.6914-0.8682 2.6914-1.9395 0-1.0713-1.2046-1.9395-2.6914-1.9395zm0 8c-1.4868 0-2.6934 0.8682-2.6934 1.9395 0 1.0713 1.2065 1.9395 2.6934 1.9395h15.664c1.4868 0 2.6914-0.8682 2.6914-1.9395 0-1.0713-1.2046-1.9395-2.6914-1.9395z" fill="#3f424d" fill-rule="evenodd" stroke-width=".19943"/></svg>

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m12.75 14.492c-0.62128 0-1.1257 0.38721-1.1257 0.86456v12.1c0 0.47737 0.50442 0.86456 1.1257 0.86456h3.3753v-1.7274h-2.2496v-10.373h13.498v1.7291h2.2496v-2.5937c0-0.47735-0.50268-0.86456-1.1239-0.86456zm6.7489 5.1874c-0.62128 0-1.1239 0.38721-1.1239 0.86456v12.1c0 0.47737 0.50266 0.86456 1.1239 0.86456h15.749c0.62125 0 1.1239-0.3872 1.1239-0.86456v-12.1c0-0.47735-0.50268-0.86456-1.1239-0.86456zm1.1257 1.7291h13.498v10.371h-13.498z" clip-rule="evenodd" fill="#3f424d" fill-rule="evenodd" stroke-width=".98604"/></svg>

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.097 23.789-7.4272-10.314h5.8379l4.5082 7.0055 4.4758-7.0055h5.8379l-7.4272 10.314 7.7839 10.735h-5.8704l-4.8001-7.4596-4.8325 7.4596h-5.8379z" fill="#3f424d" stroke-width=".67675" aria-label="X"/></svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@@ -0,0 +1 @@
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m21.438 26.092-7.6218-12.616h5.7406l4.4433 8.238 4.4109-8.238h5.7731l-7.6866 12.552v8.4974h-5.0595z" fill="#3f424d" stroke-width="1.0811" aria-label="Y"/></svg>

After

Width:  |  Height:  |  Size: 559 B

View File

@@ -217,6 +217,56 @@ CONTEXT_MENU_STYLE = f"""
}} }}
""" """
VIRTUAL_KEYBOARD_STYLE = """
VirtualKeyboard {
background-color: rgba(30, 30, 30, 200);
border-radius: 0px;
border: none;
}
QPushButton {
font-size: 14px;
border: 1px solid #555;
border-top-color: #666;
border-left-color: #666;
border-radius: 3px;
min-width: 30px;
min-height: 30px;
padding: 4px;
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #505050, stop:1 #404040);
color: #e0e0e0;
}
QPushButton:hover {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #606060, stop:1 #505050);
border: 1px solid #666;
border-top-color: #777;
border-left-color: #777;
}
QPushButton:focus {
border: 2px solid #4a90e2;
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a5a5a, stop:1 #454545);
}
QPushButton:pressed {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3a3a3a, stop:1 #303030);
border: 1px solid #444;
border-bottom-color: #555;
border-right-color: #555;
padding-top: 5px;
padding-bottom: 3px;
padding-left: 5px;
padding-right: 3px;
}
QPushButton[checked="true"] {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a90e2, stop:1 #3a7ad2);
color: white;
border: 1px solid #2a6ac2;
border-top-color: #5aa0f2;
border-left-color: #5aa0f2;
}
QPushButton[checked="true"]:focus {
border: 2px solid #6aa3f5;
}
"""
# ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН), ЛЭЙБЛОВ, КНОПОК # ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН), ЛЭЙБЛОВ, КНОПОК
MAIN_WINDOW_STYLE = f""" MAIN_WINDOW_STYLE = f"""
QWidget {{ QWidget {{
@@ -916,6 +966,96 @@ SETTINGS_CHECKBOX_STYLE = f"""
}} }}
""" """
WINETRICKS_TAB_STYLE = f"""
QTabWidget::pane {{
border: 1px solid {color_d};
background: {color_b};
border-radius: {border_radius_a};
}}
QTabBar::tab {{
background: {color_c};
color: {color_f};
padding: 8px 16px;
border-top-left-radius: {border_radius_a};
border-top-right-radius: {border_radius_a};
margin-right: 2px;
}}
QTabBar::tab:selected {{
background: {color_a};
color: {color_f};
}}
QTabBar::tab:hover {{
background: {color_e};
}}
"""
WINETRICKS_TABBLE_STYLE = f"""
QTableWidget {{
background: {color_c};
color: {color_f};
gridline-color: {color_d};
alternate-background-color: {color_d};
border: {border_a};
border-radius: {border_radius_a};
font-family: '{font_family}';
font-size: {font_size_a};
}}
QHeaderView::section {{
background: {color_d};
color: {color_f};
padding: 5px;
border: {border_a};
font-weight: bold;
}}
QTableWidget::item {{
padding: 8px;
border-bottom: 1px solid {color_d};
}}
QTableWidget::item:selected {{
background: {color_a};
color: {color_f};
}}
QTableWidget::item:hover {{
background: {color_e};
}}
QTableWidget::indicator {{
width: 24px;
height: 24px;
border: {border_b} {color_a};
border-radius: {border_radius_a};
background: rgba(255, 255, 255, 0.1);
}}
QTableWidget::indicator:unchecked {{
background: rgba(255, 255, 255, 0.1);
image: none;
}}
QTableWidget::indicator:checked {{
background: {color_a};
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
border: {border_b} {color_f};
}}
QTableWidget::indicator:hover {{
background: rgba(255, 255, 255, 0.2);
border: {border_b} {color_a};
}}
QTableWidget::indicator:focus {{
border: {border_c} {color_a};
}}
{SCROLL_AREA_STYLE}
"""
WINETRICKS_LOG_STYLE = f"""
QTextEdit {{
background: {color_c};
border: {border_a};
border-radius: {border_radius_a};
color: {color_f};
font-family: '{font_family}';
font-size: {font_size_a};
padding: 5px;
}}
"""
FILE_EXPLORER_STYLE = f""" FILE_EXPLORER_STYLE = f"""
QListView {{ QListView {{
font-size: {font_size_a}; font-size: {font_size_a};

Some files were not shown because too many files have changed in this diff Show More