Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e9c75b998f
|
|||
|
bbfbc00c11
|
|||
|
b7804fdd01
|
|||
|
|
043da2cf5d | ||
|
2fa10e7db3
|
|||
|
b1b9706272
|
|||
|
9c11d33c0a
|
|||
|
173e1cb88e
|
|||
|
30606c7ec1
|
|||
|
873e8b050e
|
|||
|
59dad21945
|
|||
|
b0c4e943ae
|
|||
|
19e01bba17
|
|||
|
836e6cdd36
|
|||
|
b2a1046f9d
|
|||
|
80a2c06b5a
|
|||
|
f0a4ace735
|
|||
|
7dfaee6831
|
|||
|
5481cd80d7
|
|||
|
a016cfa810
|
|||
|
8fc097ccaf
|
|||
|
ad3eeb6e06
|
|||
|
92631cd2c6
|
|||
|
4477679f2d
|
|||
|
|
b6644eeee5
|
||
|
|
2e921226c4
|
||
|
|
4fc1ea73d3
|
||
|
|
3c15cbe495
|
||
|
fed6aafed5
|
|||
|
2e8be13437
|
|||
|
ea272c29b6
|
|||
|
17262f6c9f
|
|||
|
e07f3f06bc
|
|||
|
16a3f4e09a
|
|||
|
a448ba29b0
|
|||
|
06e55db54d
|
|||
|
5fce23f261
|
|||
|
|
96ad40d625 | ||
|
|
a30f6f2e74 | ||
|
0231073b19
|
|||
|
dec24429f5
|
|||
|
4a758f3b3c
|
|||
|
0853dd1579
|
|||
|
bbb87c0455
|
|||
|
b32a71a125
|
|||
|
|
bddf9f850a | ||
|
|
a9c3cfa167 | ||
|
7675bc4cdc
|
|||
|
ffa203f019
|
|||
|
3eed25ecee
|
|||
|
3736bb279e
|
|||
|
|
b59ee5ae8e | ||
|
33176590fd
|
|||
|
8046065929
|
|||
|
|
fbad5add6c | ||
|
438e9737ea
|
|||
|
2d39a4c740
|
|||
|
567203b0b0
|
|||
|
502cbc5030
|
|||
|
9b61215152
|
|||
|
10d3fe8ab4
|
|||
|
a568ad9ef8
|
|||
|
f074843fc8
|
|||
|
4ab078b93e
|
|||
|
7df6ad3b80
|
|||
|
464ad0fe9c
|
|||
|
cde92885d4
|
|||
|
120c7b319c
|
|||
|
596aed0077
|
|||
|
6fc6cb1e02
|
|||
|
186e28a19b
|
|||
|
28e4d1e77c
|
|||
|
fff1f888c4
|
|||
|
fdd5a0a3d5
|
|||
|
792e52d981
|
|||
|
84d5e46a74
|
|||
|
4bc764d568
|
|||
|
9a18aa037e
|
|||
|
ed62d2d1c4
|
|||
|
accc9b18b6
|
|||
|
82249d7eab
|
|||
|
476c896940
|
|||
|
b1047ba18e
|
|||
|
987199d8e6
|
|||
|
|
ef1acd4581 |
@@ -12,7 +12,7 @@ jobs:
|
||||
name: Build AppImage
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||
python3-build pyproject-rpm-macros python3-setuptools \
|
||||
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
||||
redhat-rpm-config nodejs npm
|
||||
|
||||
- name: Setup rpmbuild environment
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||
|
||||
- name: Checkout repo
|
||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Copy fedora.spec
|
||||
run: |
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
name: Build Arch Package
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
||||
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Upload Arch package
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
|
||||
env:
|
||||
# Common version, will be used for tagging the release
|
||||
VERSION: 0.1.7
|
||||
VERSION: 0.1.8
|
||||
PKGDEST: "/tmp/portprotonqt"
|
||||
PACKAGE: "portprotonqt"
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||
python3-build pyproject-rpm-macros python3-setuptools \
|
||||
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
||||
redhat-rpm-config nodejs npm
|
||||
|
||||
- name: Setup rpmbuild environment
|
||||
@@ -180,10 +180,12 @@ jobs:
|
||||
|
||||
- name: Release
|
||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||
env:
|
||||
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
|
||||
with:
|
||||
body_path: changelog.txt
|
||||
token: ${{ env.GITEA_TOKEN }}
|
||||
tag_name: v${{ env.VERSION }}
|
||||
prerelease: true
|
||||
files: release/**/*
|
||||
sha256sum: true
|
||||
sha256sum: false
|
||||
|
||||
@@ -16,10 +16,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
uses: https://gitea.com/actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
fedora: ${{ steps.check.outputs.fedora }}
|
||||
arch: ${{ steps.check.outputs.arch }}
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Install required dependencies
|
||||
run: |
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||
|
||||
- name: Checkout repo
|
||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Copy fedora-git.spec
|
||||
run: |
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||
container:
|
||||
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
|
||||
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
|
||||
volumes:
|
||||
- /usr:/usr-host
|
||||
- /opt:/opt-host
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Upload Arch package
|
||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
name: Check code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||
uses: https://gitea.com/actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ repos:
|
||||
- id: check-yaml
|
||||
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
rev: 0.8.22
|
||||
rev: 0.9.5
|
||||
hooks:
|
||||
- id: uv-lock
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.14.0
|
||||
rev: v0.14.3
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
|
||||
|
||||
25
CHANGELOG.md
@@ -3,6 +3,31 @@
|
||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [0.1.8] - 2025-10-18
|
||||
|
||||
### Added
|
||||
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
|
||||
- В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет
|
||||
- К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада
|
||||
- Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы)
|
||||
|
||||
### Changed
|
||||
- При завершении автоустановки приложение больше не перезапускается
|
||||
- Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название
|
||||
- Обновлены и дополнены скриншоты темы
|
||||
|
||||
### Fixed
|
||||
- Исправлено наложение карточек при смене фильтра игр
|
||||
- Исправлена невозможность запуска приложения без подключёного геймпада
|
||||
- Исправлена невозможность установки компонентов Winetricks через геймпад
|
||||
- Ресиверы и виртуальные устройства больше не считаются за геймпад
|
||||
|
||||
|
||||
### Contributors
|
||||
- @Vector_null
|
||||
|
||||
---
|
||||
|
||||
## [0.1.7] - 2025-10-12
|
||||
|
||||
### Added
|
||||
|
||||
15
TODO.md
@@ -1,6 +1,6 @@
|
||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||
- [X] Добавить возможность управления с геймпада
|
||||
- [ ] Добавить возможность управления с тачскрина
|
||||
- [X] Добавить возможность управления с тачскрина (Формально и так есть)
|
||||
- [X] Добавить возможность управления с мыши и клавиатуры
|
||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
||||
@@ -11,18 +11,18 @@
|
||||
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
||||
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||
- [X] Получать описания и названия игр из базы данных Steam
|
||||
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
|
||||
- [X] Получать обложки для игр из CDN Steam
|
||||
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
||||
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
||||
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||
- [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
||||
- [X] Избавиться от вызовов yad
|
||||
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
||||
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
||||
- [X] Добавить экранную клавиатуру в поиск
|
||||
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
||||
- [X] Добавить индикацию запуска приложения
|
||||
- [X] Достигнуть паритета функциональности с Ingame
|
||||
- [ ] Достигнуть паритета функциональности с PortProton
|
||||
- [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов)
|
||||
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
||||
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
||||
- [X] Добавить переводы в переопределения
|
||||
@@ -49,7 +49,7 @@
|
||||
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
||||
- [X] Добавить систему избранного для карточек
|
||||
- [X] Заменить все `print` на `logging`
|
||||
- [ ] Привести все логи к единому языку
|
||||
- [X] Привести все логи к единому языку
|
||||
- [X] Уменьшить количество подстановок в переводах
|
||||
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
||||
- [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] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||
- [ ] Доделать светлую тему
|
||||
- [ ] Добавить подсказки к управлению с геймпада
|
||||
- [X] Добавить подсказки к управлению с геймпада
|
||||
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
||||
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
||||
|
||||
@@ -6,11 +6,12 @@ script:
|
||||
- uv pip install --no-cache-dir ../
|
||||
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
||||
- cp -r share AppDir/usr
|
||||
- cp -r lib AppDir/usr
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetwork*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
|
||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
|
||||
- shopt -s extglob
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Network*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
||||
AppDir:
|
||||
path: ./AppDir
|
||||
after_bundle:
|
||||
@@ -36,7 +37,7 @@ AppDir:
|
||||
id: ru.linux_gaming.PortProtonQt
|
||||
name: PortProtonQt
|
||||
icon: ru.linux_gaming.PortProtonQt
|
||||
version: 0.1.7
|
||||
version: 0.1.8
|
||||
exec: usr/bin/python3
|
||||
exec_args: "-m portprotonqt.app $@"
|
||||
apt:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pkgname=portprotonqt
|
||||
pkgver=0.1.7
|
||||
pkgver=0.1.8
|
||||
pkgrel=1
|
||||
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
||||
arch=('any')
|
||||
@@ -20,4 +20,5 @@ package() {
|
||||
cd "$srcdir/PortProtonQt"
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
cp -r build-aux/share "$pkgdir/usr/"
|
||||
cp -r build-aux/lib "$pkgdir/usr/"
|
||||
}
|
||||
|
||||
@@ -25,4 +25,5 @@ package() {
|
||||
cd "$srcdir/PortProtonQt"
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
cp -r build-aux/share "$pkgdir/usr/"
|
||||
cp -r build-aux/lib "$pkgdir/usr/"
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ BuildRequires: python3-build
|
||||
BuildRequires: pyproject-rpm-macros
|
||||
BuildRequires: python3dist(setuptools)
|
||||
BuildRequires: git
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
%description
|
||||
%{summary}
|
||||
@@ -69,11 +70,13 @@ cd %{oname}
|
||||
%pyproject_install
|
||||
%pyproject_save_files %{pypi_name}
|
||||
cp -r build-aux/share %{buildroot}/usr/
|
||||
cp -r build-aux/lib %{buildroot}/usr/
|
||||
|
||||
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
||||
%{_bindir}/%{pypi_name}
|
||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||
%{_udevrulesdir}/60-portprotonqt.rules
|
||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||
%{bash_completions_dir}/portprotonqt
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
%global pypi_name portprotonqt
|
||||
%global pypi_version 0.1.7
|
||||
%global pypi_version 0.1.8
|
||||
%global oname PortProtonQt
|
||||
%global _python_no_extras_requires 1
|
||||
|
||||
@@ -19,6 +19,7 @@ BuildRequires: python3-build
|
||||
BuildRequires: pyproject-rpm-macros
|
||||
BuildRequires: python3dist(setuptools)
|
||||
BuildRequires: git
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
%description
|
||||
%{summary}
|
||||
@@ -68,11 +69,13 @@ cd %{oname}
|
||||
%pyproject_install
|
||||
%pyproject_save_files %{pypi_name}
|
||||
cp -r build-aux/share %{buildroot}/usr/
|
||||
cp -r build-aux/lib %{buildroot}/usr/
|
||||
|
||||
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
||||
%{_bindir}/%{pypi_name}
|
||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||
%{_udevrulesdir}/60-portprotonqt.rules
|
||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||
%{bash_completions_dir}/portprotonqt
|
||||
|
||||
|
||||
1
build-aux/lib/udev/rules.d/60-portprotonqt.rules
Normal file
@@ -0,0 +1 @@
|
||||
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
|
||||
@@ -1021,7 +1021,7 @@
|
||||
},
|
||||
{
|
||||
"normalized_name": "farlight 84",
|
||||
"status": "Supported"
|
||||
"status": "Denied"
|
||||
},
|
||||
{
|
||||
"normalized_name": "riders republic",
|
||||
@@ -1436,8 +1436,8 @@
|
||||
"status": "Broken"
|
||||
},
|
||||
{
|
||||
"normalized_name": "blue protocol",
|
||||
"status": "Broken"
|
||||
"normalized_name": "blue protocol star resonance",
|
||||
"status": "Running"
|
||||
},
|
||||
{
|
||||
"normalized_name": "dark and darker",
|
||||
|
||||
12972
data/games_appid.json
@@ -1,4 +1,108 @@
|
||||
[
|
||||
{
|
||||
"normalized_title": "split/second",
|
||||
"slug": "split-second"
|
||||
},
|
||||
{
|
||||
"normalized_title": "warzone 2100",
|
||||
"slug": "warzone-2100"
|
||||
},
|
||||
{
|
||||
"normalized_title": "foundation",
|
||||
"slug": "foundation"
|
||||
},
|
||||
{
|
||||
"normalized_title": "земский собор [демо]",
|
||||
"slug": "zemskij-sobor-demo"
|
||||
},
|
||||
{
|
||||
"normalized_title": "crusader kings 3",
|
||||
"slug": "crusader-kings-3"
|
||||
},
|
||||
{
|
||||
"normalized_title": "nadir a grimdark deck builder",
|
||||
"slug": "nadir-a-grimdark-deck-builder"
|
||||
},
|
||||
{
|
||||
"normalized_title": "oriental empires",
|
||||
"slug": "oriental-empires"
|
||||
},
|
||||
{
|
||||
"normalized_title": "vampire the masquerade bloodlines 2",
|
||||
"slug": "vampire-the-masquerade-bloodlines-2"
|
||||
},
|
||||
{
|
||||
"normalized_title": "escape from duckov",
|
||||
"slug": "escape-from-duckov"
|
||||
},
|
||||
{
|
||||
"normalized_title": "xiii",
|
||||
"slug": "xiii"
|
||||
},
|
||||
{
|
||||
"normalized_title": "saints row 2",
|
||||
"slug": "saints-row-2"
|
||||
},
|
||||
{
|
||||
"normalized_title": "frozenheim",
|
||||
"slug": "frozenheim"
|
||||
},
|
||||
{
|
||||
"normalized_title": "saints row (2022)",
|
||||
"slug": "saints-row-2022"
|
||||
},
|
||||
{
|
||||
"normalized_title": "iron harvest",
|
||||
"slug": "iron-harvest"
|
||||
},
|
||||
{
|
||||
"normalized_title": "tom clancy's splinter cell blacklist",
|
||||
"slug": "tom-clancys-splinter-cell-blacklist"
|
||||
},
|
||||
{
|
||||
"normalized_title": "painkiller overdose",
|
||||
"slug": "painkiller-overdose"
|
||||
},
|
||||
{
|
||||
"normalized_title": "ancestors legacy",
|
||||
"slug": "ancestors-legacy"
|
||||
},
|
||||
{
|
||||
"normalized_title": "bye sweet carole",
|
||||
"slug": "bye-sweet-carole"
|
||||
},
|
||||
{
|
||||
"normalized_title": "painkiller black",
|
||||
"slug": "painkiller-black-edition"
|
||||
},
|
||||
{
|
||||
"normalized_title": "hogwarts legacy",
|
||||
"slug": "hogwarts-legacy"
|
||||
},
|
||||
{
|
||||
"normalized_title": "active matter",
|
||||
"slug": "active-matter"
|
||||
},
|
||||
{
|
||||
"normalized_title": "tom clancy's splinter cell",
|
||||
"slug": "tom-clancys-splinter-cell"
|
||||
},
|
||||
{
|
||||
"normalized_title": "sniper ghost warrior",
|
||||
"slug": "sniper-ghost-warrior"
|
||||
},
|
||||
{
|
||||
"normalized_title": "fate undiscovered realms",
|
||||
"slug": "fate-undiscovered-realms"
|
||||
},
|
||||
{
|
||||
"normalized_title": "dying light the beast deluxe",
|
||||
"slug": "dying-light-the-beast-deluxe-edition"
|
||||
},
|
||||
{
|
||||
"normalized_title": "spellforce platinum",
|
||||
"slug": "spellforce-platinum-edition"
|
||||
},
|
||||
{
|
||||
"normalized_title": "dirt rally 2.0 game of the year",
|
||||
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
||||
@@ -271,10 +375,6 @@
|
||||
"normalized_title": "steins;gate the distant valhalla",
|
||||
"slug": "steins-gate-the-distant-valhalla"
|
||||
},
|
||||
{
|
||||
"normalized_title": "hogwarts legacy",
|
||||
"slug": "hogwarts-legacy"
|
||||
},
|
||||
{
|
||||
"normalized_title": "osu!",
|
||||
"slug": "osu"
|
||||
|
||||
@@ -20,3 +20,33 @@ Stop Game
|
||||
Fullscreen
|
||||
Fulscreen
|
||||
\t
|
||||
Горячая
|
||||
vkbasalt
|
||||
dgVoodoo2
|
||||
Zink
|
||||
Vulkan
|
||||
VKD3D
|
||||
DirectX12
|
||||
Prev Dir
|
||||
Forced
|
||||
GOverlay
|
||||
Glide
|
||||
all
|
||||
futex
|
||||
DLSS
|
||||
fullscreen
|
||||
ProtonGE
|
||||
window
|
||||
compositing
|
||||
Zink
|
||||
Use
|
||||
bundled
|
||||
dxvk
|
||||
older games
|
||||
versions
|
||||
DLL Overrides
|
||||
COMP
|
||||
VKD3D
|
||||
Select needed
|
||||
CPUs
|
||||
cores
|
||||
|
||||
@@ -17,17 +17,31 @@ import json
|
||||
|
||||
|
||||
class PySide6DependencyAnalyzer:
|
||||
def __init__(self):
|
||||
def __init__(self, project_root: Path = None):
|
||||
# Системные библиотеки, которые нужно всегда оставлять
|
||||
self.system_libs = {
|
||||
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
|
||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
|
||||
'libQt6Svg'
|
||||
}
|
||||
|
||||
self.critical_modules = {
|
||||
'QtSvg',
|
||||
}
|
||||
|
||||
self.real_dependencies = {}
|
||||
self.used_modules_code = set()
|
||||
self.used_modules_ldd = set()
|
||||
self.all_required_modules = set()
|
||||
# Определяем корень проекта
|
||||
if project_root is None:
|
||||
# Корень проекта - две директории выше от скрипта
|
||||
self.project_root = Path(__file__).parent.parent
|
||||
else:
|
||||
self.project_root = project_root
|
||||
|
||||
self.venv_path = self.project_root / ".venv"
|
||||
self.build_path = self.project_root / "build-aux"
|
||||
|
||||
def find_python_files(self, directory: Path) -> List[Path]:
|
||||
"""Находит все Python файлы в директории"""
|
||||
@@ -44,24 +58,61 @@ class PySide6DependencyAnalyzer:
|
||||
"""Находит все PySide6 библиотеки (.so файлы)"""
|
||||
libs = {}
|
||||
|
||||
# Поиск в единственной локации
|
||||
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
|
||||
print(f"Поиск PySide6 библиотек в: {search_path}")
|
||||
# Ищем venv в корне проекта
|
||||
venv_candidates = [
|
||||
self.venv_path, # .venv
|
||||
self.project_root / "venv",
|
||||
self.project_root / ".virtualenv",
|
||||
]
|
||||
|
||||
if search_path.exists():
|
||||
# Ищем .so файлы модулей
|
||||
for so_file in search_path.glob("Qt*.*.so"):
|
||||
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
||||
if module_name.startswith('Qt'):
|
||||
libs[module_name] = so_file
|
||||
pyside6_path = None
|
||||
|
||||
# Также ищем в подпапках
|
||||
for subdir in search_path.iterdir():
|
||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
||||
for so_file in subdir.glob("*.so*"):
|
||||
if 'Qt' in so_file.name:
|
||||
libs[subdir.name] = so_file
|
||||
break
|
||||
# Пробуем найти PySide6 в venv
|
||||
for venv in venv_candidates:
|
||||
if venv.exists():
|
||||
# Ищем Python версию
|
||||
lib_path = venv / "lib"
|
||||
if lib_path.exists():
|
||||
for python_dir in lib_path.iterdir():
|
||||
if python_dir.name.startswith('python'):
|
||||
candidate = python_dir / "site-packages" / "PySide6"
|
||||
if candidate.exists():
|
||||
pyside6_path = candidate
|
||||
print(f"Найден PySide6 в: {candidate}")
|
||||
break
|
||||
if pyside6_path:
|
||||
break
|
||||
|
||||
if not pyside6_path:
|
||||
print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...")
|
||||
# Если не нашли в venv, пробуем в AppDir
|
||||
if base_path:
|
||||
appdir_candidate = base_path / "AppDir/usr/local/lib"
|
||||
if appdir_candidate.exists():
|
||||
for python_dir in appdir_candidate.iterdir():
|
||||
if python_dir.name.startswith('python'):
|
||||
candidate = python_dir / "dist-packages" / "PySide6"
|
||||
if candidate.exists():
|
||||
pyside6_path = candidate
|
||||
print(f"Найден PySide6 в AppDir: {candidate}")
|
||||
break
|
||||
|
||||
if not pyside6_path:
|
||||
return libs
|
||||
|
||||
# Ищем .so файлы модулей
|
||||
for so_file in pyside6_path.glob("Qt*.*.so"):
|
||||
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
||||
if module_name.startswith('Qt'):
|
||||
libs[module_name] = so_file
|
||||
|
||||
# Также ищем в подпапках
|
||||
for subdir in pyside6_path.iterdir():
|
||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
||||
for so_file in subdir.glob("*.so*"):
|
||||
if 'Qt' in so_file.name:
|
||||
libs[subdir.name] = so_file
|
||||
break
|
||||
|
||||
return libs
|
||||
|
||||
@@ -257,8 +308,10 @@ class PySide6DependencyAnalyzer:
|
||||
|
||||
# Модули для удаления
|
||||
if removable_modules:
|
||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
|
||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
||||
removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
|
||||
if removable_filtered:
|
||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)])
|
||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
||||
|
||||
# Генерируем команду для удаления нативных библиотек с сохранением нужных
|
||||
required_libs = set()
|
||||
@@ -276,39 +329,82 @@ class PySide6DependencyAnalyzer:
|
||||
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
||||
])
|
||||
|
||||
# Заменяем блок очистки в рецепте
|
||||
import re
|
||||
|
||||
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
|
||||
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
|
||||
# Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
|
||||
# Паттерн: после " - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:"
|
||||
pattern = r'( - cp -r lib AppDir/usr\n)((?: - (?:rm|shopt).*\n)*?)(?=AppDir:)'
|
||||
|
||||
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
|
||||
match = re.search(pattern, recipe_content)
|
||||
|
||||
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
|
||||
if not match:
|
||||
print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте")
|
||||
print("Добавляем команды очистки перед блоком AppDir:")
|
||||
|
||||
# Просто вставим команды перед AppDir:
|
||||
appdir_pos = recipe_content.find('AppDir:')
|
||||
if appdir_pos != -1:
|
||||
new_content = (
|
||||
recipe_content[:appdir_pos] +
|
||||
'\n'.join(cleanup_lines) + '\n' +
|
||||
recipe_content[appdir_pos:]
|
||||
)
|
||||
return new_content
|
||||
else:
|
||||
print("ОШИБКА: Не найден блок AppDir: в рецепте")
|
||||
return ""
|
||||
|
||||
# Создаем замену - группа 1 (cp -r lib) + новые команды очистки
|
||||
replacement = r'\1' + '\n'.join(cleanup_lines) + '\n'
|
||||
|
||||
updated_recipe = re.sub(pattern, replacement, recipe_content, count=1)
|
||||
|
||||
return updated_recipe
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
||||
parser.add_argument('project_path', help='Путь к проекту для анализа')
|
||||
parser.add_argument('project_path', nargs='?', default='.',
|
||||
help='Путь к проекту для анализа (по умолчанию: текущая директория)')
|
||||
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
||||
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
||||
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
||||
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
project_path = Path(args.project_path)
|
||||
project_path = Path(args.project_path).resolve()
|
||||
if not project_path.exists():
|
||||
print(f"Ошибка: путь {project_path} не существует")
|
||||
sys.exit(1)
|
||||
|
||||
appdir_path = Path(args.appdir) if args.appdir else None
|
||||
appdir_path = Path(args.appdir).resolve() if args.appdir else None
|
||||
if appdir_path and not appdir_path.exists():
|
||||
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
||||
appdir_path = None
|
||||
|
||||
analyzer = PySide6DependencyAnalyzer()
|
||||
# Определяем корень проекта
|
||||
# Если запущен из подпапки проекта, ищем корень
|
||||
project_root = project_path
|
||||
if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists():
|
||||
project_root = project_path
|
||||
else:
|
||||
# Пытаемся найти корень проекта
|
||||
current = project_path
|
||||
while current != current.parent:
|
||||
if (current / ".git").exists() or (current / "pyproject.toml").exists():
|
||||
project_root = current
|
||||
break
|
||||
current = current.parent
|
||||
|
||||
print(f"Корень проекта: {project_root}")
|
||||
|
||||
analyzer = PySide6DependencyAnalyzer(project_root=project_root)
|
||||
|
||||
# Если указан custom venv путь
|
||||
if args.venv:
|
||||
analyzer.venv_path = Path(args.venv).resolve()
|
||||
print(f"Использую указанный venv: {analyzer.venv_path}")
|
||||
|
||||
results = analyzer.analyze_project(project_path, appdir_path)
|
||||
|
||||
# Сохраняем в анализатор для генерации команд
|
||||
@@ -347,13 +443,13 @@ def main():
|
||||
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
||||
|
||||
if args.verbose and results['real_dependencies']:
|
||||
Devlin(f"\nРеальные зависимости (ldd):")
|
||||
print(f"\nРеальные зависимости (ldd):")
|
||||
for module, deps in results['real_dependencies'].items():
|
||||
if deps:
|
||||
print(f" {module} → {', '.join(deps)}")
|
||||
|
||||
# Обновляем AppImage рецепт
|
||||
recipe_path = Path("../build-aux/AppImageBuilder.yml")
|
||||
recipe_path = analyzer.build_path / "AppImageBuilder.yml"
|
||||
if recipe_path.exists():
|
||||
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
||||
if updated_recipe:
|
||||
|
||||
@@ -21,9 +21,9 @@ Current translation status:
|
||||
|
||||
| Locale | Progress | Translated |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 240 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 240 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 of 240 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 339 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 339 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 339 of 339 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
| Локаль | Прогресс | Переведено |
|
||||
| :----- | -------: | ---------: |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 240 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 240 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 из 240 |
|
||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 339 |
|
||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 339 |
|
||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 339 из 339 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo, QTimer, Qt
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
||||
|
||||
from portprotonqt.main_window import MainWindow
|
||||
from portprotonqt.config_utils import save_fullscreen_config, get_portproton_location
|
||||
from portprotonqt.config_utils import (
|
||||
save_fullscreen_config,
|
||||
read_fullscreen_config,
|
||||
get_portproton_start_command
|
||||
)
|
||||
from portprotonqt.logger import get_logger, setup_logger
|
||||
from portprotonqt.cli import parse_args
|
||||
|
||||
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
||||
__app_name__ = "PortProtonQt"
|
||||
__app_version__ = "0.1.7"
|
||||
__app_version__ = "0.1.8"
|
||||
|
||||
def get_version():
|
||||
try:
|
||||
commit = subprocess.check_output(
|
||||
['git', 'rev-parse', '--short', 'HEAD'],
|
||||
stderr=subprocess.DEVNULL
|
||||
).decode('utf-8').strip()
|
||||
["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():
|
||||
os.environ['PW_CLI'] = '1'
|
||||
os.environ['PROCESS_LOG'] = '1'
|
||||
os.environ['START_FROM_STEAM'] = '1'
|
||||
os.environ["PW_CLI"] = "1"
|
||||
os.environ["PROCESS_LOG"] = "1"
|
||||
os.environ["START_FROM_STEAM"] = "1"
|
||||
|
||||
portproton_path = get_portproton_location()
|
||||
start_sh = get_portproton_start_command()
|
||||
|
||||
if portproton_path is None:
|
||||
if start_sh is None:
|
||||
return
|
||||
|
||||
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
|
||||
subprocess.run([script_path, 'cli', '--initial'])
|
||||
subprocess.run(start_sh + ["cli", "--initial"])
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||
@@ -43,41 +48,116 @@ def main():
|
||||
app.setApplicationVersion(__app_version__)
|
||||
|
||||
args = parse_args()
|
||||
|
||||
# Setup logger with specified debug level
|
||||
setup_logger(args.debug_level)
|
||||
|
||||
# Reinitialize logger after setup to ensure it uses the new configuration
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# --- Single-instance logic ---
|
||||
server_name = __app_id__
|
||||
socket = QLocalSocket()
|
||||
socket.connectToServer(server_name)
|
||||
|
||||
if socket.waitForConnected(200):
|
||||
# Второй экземпляр — передаём команду первому
|
||||
fullscreen = args.fullscreen or read_fullscreen_config()
|
||||
msg = b"show:fullscreen" if fullscreen else b"show"
|
||||
socket.write(msg)
|
||||
socket.flush()
|
||||
socket.waitForBytesWritten(500)
|
||||
socket.disconnectFromServer()
|
||||
logger.info("Restored existing instance from tray")
|
||||
return
|
||||
|
||||
# Если старый сокет остался — удалить
|
||||
QLocalServer.removeServer(server_name)
|
||||
|
||||
local_server = QLocalServer()
|
||||
if not local_server.listen(server_name):
|
||||
logger.warning(f"Failed to start local server: {local_server.errorString()}")
|
||||
return
|
||||
|
||||
# --- Qt translations ---
|
||||
system_locale = QLocale.system()
|
||||
qt_translator = QTranslator()
|
||||
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
||||
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
||||
app.installTranslator(qt_translator)
|
||||
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"
|
||||
)
|
||||
|
||||
# --- Main Window ---
|
||||
version = get_version()
|
||||
window = MainWindow(app_name=__app_name__, version=version)
|
||||
|
||||
if args.fullscreen:
|
||||
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
||||
# --- Handle incoming connections ---
|
||||
def handle_new_connection():
|
||||
conn = local_server.nextPendingConnection()
|
||||
if not conn:
|
||||
return
|
||||
|
||||
if conn.waitForReadyRead(1000):
|
||||
data = conn.readAll().data()
|
||||
msg = bytes(data).decode("utf-8", errors="ignore")
|
||||
logger.info(f"IPC message received: {msg}")
|
||||
|
||||
def restore_window():
|
||||
try:
|
||||
if msg.startswith("show"):
|
||||
if hasattr(window, "restore_from_tray"):
|
||||
window.restore_from_tray() # type: ignore[attr-defined]
|
||||
else:
|
||||
window.showNormal()
|
||||
window.raise_()
|
||||
window.activateWindow()
|
||||
window.setWindowState(
|
||||
window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive
|
||||
)
|
||||
|
||||
if ":fullscreen" in msg:
|
||||
logger.info("Switching to fullscreen via IPC")
|
||||
save_fullscreen_config(True)
|
||||
window.showFullScreen()
|
||||
else:
|
||||
logger.info("Switching to normal window via IPC")
|
||||
save_fullscreen_config(False)
|
||||
window.showNormal()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to restore window: {e}")
|
||||
|
||||
# Выполняем в основном потоке
|
||||
QTimer.singleShot(0, restore_window)
|
||||
|
||||
conn.disconnectFromServer()
|
||||
|
||||
local_server.newConnection.connect(handle_new_connection)
|
||||
|
||||
# --- Initial fullscreen state ---
|
||||
launch_fullscreen = args.fullscreen or read_fullscreen_config()
|
||||
if launch_fullscreen:
|
||||
logger.info(
|
||||
f"Launching in fullscreen mode ({'--fullscreen' if args.fullscreen else 'config'})"
|
||||
)
|
||||
save_fullscreen_config(True)
|
||||
window.showFullScreen()
|
||||
else:
|
||||
logger.info("Launching in normal mode")
|
||||
save_fullscreen_config(False)
|
||||
window.showNormal()
|
||||
|
||||
# --- Cleanup ---
|
||||
def cleanup_on_exit():
|
||||
nonlocal window
|
||||
app.aboutToQuit.disconnect()
|
||||
if window:
|
||||
window.close()
|
||||
app.quit()
|
||||
try:
|
||||
local_server.close()
|
||||
QLocalServer.removeServer(server_name)
|
||||
if window:
|
||||
window.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Cleanup error: {e}")
|
||||
|
||||
app.aboutToQuit.connect(cleanup_on_exit)
|
||||
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import os
|
||||
import configparser
|
||||
import shutil
|
||||
import subprocess
|
||||
from portprotonqt.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
_portproton_location = None
|
||||
_portproton_start_sh = None
|
||||
|
||||
# Configuration cache for performance optimization
|
||||
_config_cache = {}
|
||||
_config_last_modified = {}
|
||||
|
||||
# Paths to configuration files
|
||||
CONFIG_FILE = os.path.join(
|
||||
@@ -26,13 +32,35 @@ THEMES_DIRS = [
|
||||
]
|
||||
|
||||
def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
||||
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails."""
|
||||
cp = configparser.ConfigParser()
|
||||
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails.
|
||||
Uses caching to avoid repeated file reads for better performance.
|
||||
"""
|
||||
# Check if file exists
|
||||
if not os.path.exists(config_file):
|
||||
logger.debug(f"Configuration file {config_file} not found")
|
||||
return None
|
||||
|
||||
# Get file modification time
|
||||
try:
|
||||
current_mtime = os.path.getmtime(config_file)
|
||||
except OSError:
|
||||
logger.warning(f"Failed to get modification time for {config_file}")
|
||||
return None
|
||||
|
||||
# Check if we have a cached version that's still valid
|
||||
if config_file in _config_cache and config_file in _config_last_modified:
|
||||
if _config_last_modified[config_file] == current_mtime:
|
||||
logger.debug(f"Using cached config for {config_file}")
|
||||
return _config_cache[config_file]
|
||||
|
||||
# Read and parse the config file
|
||||
cp = configparser.ConfigParser()
|
||||
try:
|
||||
cp.read(config_file, encoding="utf-8")
|
||||
# Update cache
|
||||
_config_cache[config_file] = cp
|
||||
_config_last_modified[config_file] = current_mtime
|
||||
logger.debug(f"Config file {config_file} loaded and cached")
|
||||
return cp
|
||||
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
|
||||
logger.warning(f"Invalid configuration file format: {e}")
|
||||
@@ -41,6 +69,14 @@ def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
||||
logger.warning(f"Failed to read configuration file: {e}")
|
||||
return None
|
||||
|
||||
def invalidate_config_cache(config_file: str = CONFIG_FILE):
|
||||
"""Invalidates the cached configuration for the specified file."""
|
||||
if config_file in _config_cache:
|
||||
del _config_cache[config_file]
|
||||
if config_file in _config_last_modified:
|
||||
del _config_last_modified[config_file]
|
||||
logger.debug(f"Config cache invalidated for {config_file}")
|
||||
|
||||
def read_config():
|
||||
"""Reads the configuration file and returns a dictionary of parameters.
|
||||
Example line in config (no sections):
|
||||
@@ -75,6 +111,8 @@ def save_theme_to_config(theme_name):
|
||||
cp["Appearance"]["theme"] = theme_name
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_time_config():
|
||||
"""Reads time settings from the [Time] section of the configuration file.
|
||||
@@ -94,6 +132,8 @@ def save_time_config(detail_level):
|
||||
cp["Time"]["detail_level"] = detail_level
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_file_content(file_path):
|
||||
"""Reads the content of a file and returns it as a string."""
|
||||
@@ -101,14 +141,14 @@ def read_file_content(file_path):
|
||||
return f.read().strip()
|
||||
|
||||
def get_portproton_location():
|
||||
"""Returns the path to the PortProton directory.
|
||||
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
|
||||
If the path is invalid, uses the default directory.
|
||||
"""
|
||||
"""Возвращает путь к PortProton каталогу (строку) или None."""
|
||||
global _portproton_location
|
||||
|
||||
if _portproton_location is not None:
|
||||
return _portproton_location
|
||||
|
||||
location = None
|
||||
|
||||
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
||||
try:
|
||||
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
||||
@@ -116,19 +156,46 @@ def get_portproton_location():
|
||||
_portproton_location = location
|
||||
logger.info(f"PortProton path from configuration: {location}")
|
||||
return _portproton_location
|
||||
logger.warning(f"Invalid PortProton path in configuration: {location}, using default path")
|
||||
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
|
||||
except (OSError, PermissionError) as e:
|
||||
logger.warning(f"Failed to read PortProton configuration file: {e}, using default path")
|
||||
logger.warning(f"Failed to read PortProton configuration file: {e}")
|
||||
|
||||
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||
if os.path.isdir(default_dir):
|
||||
_portproton_location = default_dir
|
||||
logger.info(f"Using flatpak PortProton directory: {default_dir}")
|
||||
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||
if os.path.isdir(default_flatpak_dir):
|
||||
_portproton_location = default_flatpak_dir
|
||||
logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
|
||||
return _portproton_location
|
||||
|
||||
logger.warning("PortProton configuration and flatpak directory not found")
|
||||
logger.warning("PortProton configuration and Flatpak directory not found")
|
||||
return None
|
||||
|
||||
def get_portproton_start_command():
|
||||
"""Возвращает список команд для запуска PortProton (start.sh или flatpak run)."""
|
||||
portproton_path = get_portproton_location()
|
||||
if not portproton_path:
|
||||
return None
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["flatpak", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
if "ru.linux_gaming.PortProton" in result.stdout:
|
||||
logger.info("Detected Flatpak installation")
|
||||
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
|
||||
if os.path.exists(start_sh_path):
|
||||
return [start_sh_path]
|
||||
|
||||
logger.warning("Neither flatpak nor start.sh found for PortProton")
|
||||
return None
|
||||
|
||||
|
||||
def parse_desktop_entry(file_path):
|
||||
"""Reads and parses a .desktop file using configparser.
|
||||
Returns None if the [Desktop Entry] section is missing.
|
||||
@@ -176,6 +243,30 @@ def save_card_size(card_width):
|
||||
cp["Cards"]["card_width"] = str(card_width)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_auto_card_size():
|
||||
"""Reads the card size (width) for Auto Install from the [Cards] section.
|
||||
Returns 250 if the parameter is not set.
|
||||
"""
|
||||
cp = read_config_safely(CONFIG_FILE)
|
||||
if cp is None or not cp.has_section("Cards") or not cp.has_option("Cards", "auto_card_width"):
|
||||
save_auto_card_size(250)
|
||||
return 250
|
||||
return cp.getint("Cards", "auto_card_width", fallback=250)
|
||||
|
||||
def save_auto_card_size(card_width):
|
||||
"""Saves the card size (width) for Auto Install to the [Cards] section."""
|
||||
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
||||
if "Cards" not in cp:
|
||||
cp["Cards"] = {}
|
||||
cp["Cards"]["auto_card_width"] = str(card_width)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
|
||||
def read_sort_method():
|
||||
"""Reads the sort method from the [Games] section.
|
||||
@@ -195,6 +286,8 @@ def save_sort_method(sort_method):
|
||||
cp["Games"]["sort_method"] = sort_method
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_display_filter():
|
||||
"""Reads the display_filter parameter from the [Games] section.
|
||||
@@ -214,6 +307,8 @@ def save_display_filter(filter_value):
|
||||
cp["Games"]["display_filter"] = filter_value
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_favorites():
|
||||
"""Reads the list of favorite games from the [Favorites] section.
|
||||
@@ -239,6 +334,8 @@ def save_favorites(favorites):
|
||||
cp["Favorites"]["games"] = f'"{fav_str}"'
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_rumble_config():
|
||||
"""Reads the gamepad rumble setting from the [Gamepad] section.
|
||||
@@ -258,6 +355,29 @@ def save_rumble_config(rumble_enabled):
|
||||
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
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)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def ensure_default_proxy_config():
|
||||
"""Ensures the [Proxy] section exists in the configuration file.
|
||||
@@ -302,6 +422,8 @@ def save_proxy_config(proxy_url="", proxy_user="", proxy_password=""):
|
||||
cp["Proxy"]["proxy_password"] = proxy_password
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_fullscreen_config():
|
||||
"""Reads the fullscreen mode setting from the [Display] section.
|
||||
@@ -321,6 +443,8 @@ def save_fullscreen_config(fullscreen):
|
||||
cp["Display"]["fullscreen"] = str(fullscreen)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_window_geometry() -> tuple[int, int]:
|
||||
"""Reads the window width and height from the [MainWindow] section.
|
||||
@@ -342,6 +466,8 @@ def save_window_geometry(width: int, height: int):
|
||||
cp["MainWindow"]["height"] = str(height)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def reset_config():
|
||||
"""Resets the configuration file by deleting it.
|
||||
@@ -351,6 +477,8 @@ def reset_config():
|
||||
try:
|
||||
os.remove(CONFIG_FILE)
|
||||
logger.info("Configuration file %s deleted", CONFIG_FILE)
|
||||
# Invalidate cache after deletion
|
||||
invalidate_config_cache()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete configuration file: {e}")
|
||||
|
||||
@@ -365,6 +493,9 @@ def clear_cache():
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete cache: {e}")
|
||||
|
||||
# Also clear our internal config cache
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_auto_fullscreen_gamepad():
|
||||
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
|
||||
Returns False if the parameter is missing.
|
||||
@@ -383,6 +514,8 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
|
||||
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
def read_favorite_folders():
|
||||
"""Reads the list of favorite folders from the [FavoritesFolders] section.
|
||||
@@ -408,3 +541,26 @@ def save_favorite_folders(folders):
|
||||
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||
cp.write(configfile)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
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)
|
||||
# Invalidate cache after saving
|
||||
invalidate_config_cache()
|
||||
|
||||
@@ -11,7 +11,7 @@ from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QLineEdit, QApplicati
|
||||
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
||||
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
||||
from portprotonqt.localization import _
|
||||
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders
|
||||
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders, get_portproton_start_command
|
||||
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
||||
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
||||
@@ -406,16 +406,7 @@ class ContextMenuManager:
|
||||
)
|
||||
return
|
||||
# Construct EGS launch command
|
||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
||||
start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if self.portproton_location and ".var" not in self.portproton_location:
|
||||
wrapper = start_sh_path
|
||||
if not os.path.exists(start_sh_path):
|
||||
self.signals.show_warning_dialog.emit(
|
||||
_("Error"),
|
||||
_("start.sh not found at {path}").format(path=start_sh_path)
|
||||
)
|
||||
return
|
||||
wrapper = get_portproton_start_command()
|
||||
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
|
||||
else:
|
||||
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
|
||||
|
||||
@@ -13,7 +13,7 @@ from portprotonqt.localization import get_egs_language, _
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.image_utils import load_pixmap_async
|
||||
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
|
||||
from portprotonqt.config_utils import get_portproton_location
|
||||
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
|
||||
from portprotonqt.steam_api import (
|
||||
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
|
||||
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
|
||||
@@ -254,14 +254,7 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba
|
||||
return
|
||||
|
||||
# Determine wrapper
|
||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
||||
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
|
||||
if portproton_dir is not None and ".var" not in portproton_dir:
|
||||
wrapper = start_sh_path
|
||||
if not os.path.exists(start_sh_path):
|
||||
logger.error(f"start.sh not found at {start_sh_path}")
|
||||
callback((False, f"start.sh not found at {start_sh_path}"))
|
||||
return
|
||||
wrapper = get_portproton_start_command()
|
||||
|
||||
# Create launch script
|
||||
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
||||
from PySide6.QtCore import Signal, Property, Qt, QUrl
|
||||
from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer
|
||||
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
||||
from collections.abc import Callable
|
||||
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
||||
@@ -102,7 +102,7 @@ class GameCard(QFrame):
|
||||
|
||||
self.favoriteLabel = ClickableLabel(self.coverWidget)
|
||||
self.favoriteLabel.clicked.connect(self.toggle_favorite)
|
||||
self.is_favorite = self.name in read_favorites()
|
||||
self.is_favorite = self.name in set(read_favorites())
|
||||
self.update_favorite_icon()
|
||||
self.favoriteLabel.raise_()
|
||||
|
||||
@@ -203,7 +203,7 @@ class GameCard(QFrame):
|
||||
self.update_cover_pixmap()
|
||||
|
||||
def update_cover_pixmap(self):
|
||||
if self.base_pixmap:
|
||||
if self.base_pixmap and not self.base_pixmap.isNull():
|
||||
scaled_width = int(self.base_card_width * self._scale)
|
||||
scaled_pixmap = self.base_pixmap.scaled(scaled_width, int(scaled_width * 1.5), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
|
||||
@@ -404,14 +404,22 @@ class GameCard(QFrame):
|
||||
self.favoriteLabel.setText("☆")
|
||||
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
||||
|
||||
parent = self.parent()
|
||||
while parent:
|
||||
if hasattr(parent, 'game_library_manager'):
|
||||
QTimer.singleShot(0, parent.game_library_manager.update_game_grid) # type: ignore[attr-defined]
|
||||
break
|
||||
parent = parent.parent()
|
||||
|
||||
def toggle_favorite(self):
|
||||
favorites = read_favorites()
|
||||
favorites_set = set(favorites)
|
||||
if self.is_favorite:
|
||||
if self.name in favorites:
|
||||
if self.name in favorites_set:
|
||||
favorites.remove(self.name)
|
||||
self.is_favorite = False
|
||||
else:
|
||||
if self.name not in favorites:
|
||||
if self.name not in favorites_set:
|
||||
favorites.append(self.name)
|
||||
self.is_favorite = True
|
||||
save_favorites(favorites)
|
||||
|
||||
@@ -33,6 +33,7 @@ class MainWindowProtocol(Protocol):
|
||||
# Required attributes
|
||||
searchEdit: CustomLineEdit
|
||||
_last_card_width: int
|
||||
card_width: int
|
||||
current_hovered_card: GameCard | None
|
||||
current_focused_card: GameCard | None
|
||||
gamesListWidget: QWidget | None
|
||||
@@ -128,6 +129,8 @@ class GameLibraryManager:
|
||||
self.card_width = self.sizeSlider.value()
|
||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||
save_card_size(self.card_width)
|
||||
self.main_window.card_width = self.card_width
|
||||
self.main_window._last_card_width = self.card_width
|
||||
for card in self.game_card_cache.values():
|
||||
card.update_card_size(self.card_width)
|
||||
self.update_game_grid()
|
||||
@@ -217,6 +220,16 @@ class GameLibraryManager:
|
||||
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:
|
||||
@@ -254,8 +267,9 @@ class GameLibraryManager:
|
||||
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]
|
||||
favorites_set = set(favorites) # Convert to set for O(1) lookup
|
||||
fav_games = [g for g in games_list if g[0] in favorites_set]
|
||||
non_fav_games = [g for g in games_list if g[0] not in favorites_set]
|
||||
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
|
||||
@@ -346,6 +360,8 @@ class GameLibraryManager:
|
||||
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):
|
||||
@@ -453,11 +469,3 @@ class GameLibraryManager:
|
||||
def filter_games_delayed(self):
|
||||
"""Filters games based on search text and updates the grid."""
|
||||
self.update_game_grid(is_filter=True)
|
||||
|
||||
def calculate_columns(self, card_width: int) -> int:
|
||||
"""Calculate the number of columns based on card width and assumed container width."""
|
||||
# Assuming a typical container width; adjust as needed
|
||||
available_width = 1200 # Example width, can be dynamic if widget access is added
|
||||
spacing = 15 # Assumed spacing between cards
|
||||
columns = max(1, (available_width - spacing) // (card_width + spacing))
|
||||
return min(columns, 8) # Cap at reasonable max
|
||||
|
||||
@@ -36,11 +36,22 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
||||
current_theme_name = read_theme_from_config()
|
||||
|
||||
def finish_with(pixmap: QPixmap):
|
||||
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||
x = (scaled.width() - width) // 2
|
||||
y = (scaled.height() - height) // 2
|
||||
cropped = scaled.copy(x, y, width, height)
|
||||
callback(cropped)
|
||||
# Check if pixmap is valid before attempting to scale it
|
||||
if pixmap.isNull():
|
||||
# Create a default placeholder pixmap instead of trying to scale a null pixmap
|
||||
placeholder_pixmap = QPixmap(width, height)
|
||||
placeholder_pixmap.fill(QColor("#333333"))
|
||||
painter = QPainter(placeholder_pixmap)
|
||||
painter.setPen(QPen(QColor("white")))
|
||||
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
||||
painter.end()
|
||||
callback(placeholder_pixmap)
|
||||
else:
|
||||
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||
x = (scaled.width() - width) // 2
|
||||
y = (scaled.height() - height) // 2
|
||||
cropped = scaled.copy(x, y, width, height)
|
||||
callback(cropped)
|
||||
|
||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
||||
image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
|
||||
@@ -58,6 +69,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
||||
local_path = os.path.join(image_folder, f"{appid}.jpg")
|
||||
if os.path.exists(local_path):
|
||||
pixmap = QPixmap(local_path)
|
||||
# Check if the pixmap loaded successfully
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load image from {local_path}")
|
||||
finish_with(pixmap)
|
||||
return
|
||||
|
||||
@@ -69,6 +83,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||
if placeholder_path and QFile.exists(placeholder_path):
|
||||
pixmap.load(placeholder_path)
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
||||
else:
|
||||
pixmap = QPixmap(width, height)
|
||||
pixmap.fill(QColor("#333333"))
|
||||
@@ -83,11 +99,19 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки URL {cover}: {e}")
|
||||
|
||||
if cover and cover.startswith(("http://", "https://")):
|
||||
# SteamGridDB (SGDB)
|
||||
if cover and cover.startswith("https://cdn2.steamgriddb.com"):
|
||||
try:
|
||||
local_path = os.path.join(image_folder, f"{app_name}.jpg")
|
||||
parts = cover.split("/")
|
||||
filename = parts[-1] if parts else "sgdb_cover.png"
|
||||
# SGDB ссылки содержат уникальный хеш в названии — используем как имя
|
||||
local_path = os.path.join(image_folder, filename)
|
||||
|
||||
if os.path.exists(local_path):
|
||||
pixmap = QPixmap(local_path)
|
||||
# Check if the pixmap loaded successfully
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load image from {local_path}")
|
||||
finish_with(pixmap)
|
||||
return
|
||||
|
||||
@@ -99,6 +123,45 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||
if placeholder_path and QFile.exists(placeholder_path):
|
||||
pixmap.load(placeholder_path)
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
||||
else:
|
||||
pixmap = QPixmap(width, height)
|
||||
pixmap.fill(QColor("#333333"))
|
||||
painter = QPainter(pixmap)
|
||||
painter.setPen(QPen(QColor("white")))
|
||||
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
||||
painter.end()
|
||||
finish_with(pixmap)
|
||||
|
||||
logger.info("Downloading SGDB cover for %s -> %s", app_name or "unknown", filename)
|
||||
downloader.download_async(cover, local_path, timeout=5, callback=on_downloaded)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки SGDB URL {cover}: {e}")
|
||||
|
||||
if cover and cover.startswith(("http://", "https://")):
|
||||
try:
|
||||
local_path = os.path.join(image_folder, f"{app_name}.jpg")
|
||||
if os.path.exists(local_path):
|
||||
pixmap = QPixmap(local_path)
|
||||
# Check if the pixmap loaded successfully
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load image from {local_path}")
|
||||
finish_with(pixmap)
|
||||
return
|
||||
|
||||
def on_downloaded(result: str | None):
|
||||
pixmap = QPixmap()
|
||||
if result and os.path.exists(result):
|
||||
pixmap.load(result)
|
||||
if pixmap.isNull():
|
||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||
if placeholder_path and QFile.exists(placeholder_path):
|
||||
pixmap.load(placeholder_path)
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
||||
else:
|
||||
pixmap = QPixmap(width, height)
|
||||
pixmap.fill(QColor("#333333"))
|
||||
@@ -115,6 +178,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
||||
|
||||
if cover and QFile.exists(cover):
|
||||
pixmap = QPixmap(cover)
|
||||
# Check if the pixmap loaded successfully
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load image from {cover}")
|
||||
finish_with(pixmap)
|
||||
return
|
||||
|
||||
@@ -122,6 +188,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
||||
pixmap = QPixmap()
|
||||
if placeholder_path and QFile.exists(placeholder_path):
|
||||
pixmap.load(placeholder_path)
|
||||
if pixmap.isNull():
|
||||
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
||||
else:
|
||||
pixmap = QPixmap(width, height)
|
||||
pixmap.fill(QColor("#333333"))
|
||||
@@ -141,7 +209,15 @@ def round_corners(pixmap, radius):
|
||||
"""
|
||||
if pixmap.isNull():
|
||||
return pixmap
|
||||
|
||||
# Check if radius is valid to prevent issues
|
||||
if radius <= 0:
|
||||
return pixmap
|
||||
|
||||
size = pixmap.size()
|
||||
if size.width() <= 0 or size.height() <= 0:
|
||||
return pixmap
|
||||
|
||||
rounded = QPixmap(size)
|
||||
rounded.fill(QColor(0, 0, 0, 0))
|
||||
painter = QPainter(rounded)
|
||||
@@ -244,20 +320,31 @@ class FullscreenDialog(QDialog):
|
||||
QApplication.processEvents()
|
||||
|
||||
pixmap, caption = self.images[self.current_index]
|
||||
# Учитываем devicePixelRatio для масштабирования высокого качества
|
||||
device_pixel_ratio = get_device_pixel_ratio()
|
||||
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
|
||||
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
|
||||
# Check if pixmap is valid before attempting to scale it
|
||||
if pixmap.isNull():
|
||||
# Create a default placeholder pixmap instead of trying to scale a null pixmap
|
||||
placeholder_pixmap = QPixmap(self.FIXED_WIDTH - 80, self.FIXED_HEIGHT)
|
||||
placeholder_pixmap.fill(QColor("#333333"))
|
||||
painter = QPainter(placeholder_pixmap)
|
||||
painter.setPen(QPen(QColor("white")))
|
||||
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
||||
painter.end()
|
||||
self.imageLabel.setPixmap(placeholder_pixmap)
|
||||
else:
|
||||
# Учитываем devicePixelRatio для масштабирования высокого качества
|
||||
device_pixel_ratio = get_device_pixel_ratio()
|
||||
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
|
||||
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
|
||||
|
||||
# Масштабируем изображение из оригинального pixmap
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
target_width,
|
||||
target_height,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
||||
self.imageLabel.setPixmap(scaled_pixmap)
|
||||
# Масштабируем изображение из оригинального pixmap
|
||||
scaled_pixmap = pixmap.scaled(
|
||||
target_width,
|
||||
target_height,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation
|
||||
)
|
||||
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
||||
self.imageLabel.setPixmap(scaled_pixmap)
|
||||
self.captionLabel.setText(caption)
|
||||
self.setWindowTitle(caption)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
||||
"POT-Creation-Date: 2025-11-23 15:42+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: de_DE\n"
|
||||
@@ -76,10 +76,6 @@ msgstr ""
|
||||
msgid "Legendary executable not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Success"
|
||||
msgstr ""
|
||||
|
||||
@@ -124,6 +120,10 @@ msgstr ""
|
||||
msgid "Removed '{game_name}' from favorites"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launch game \"{name}\" with PortProton"
|
||||
msgstr ""
|
||||
@@ -252,13 +252,43 @@ msgstr ""
|
||||
msgid "Select All"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Dir"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prev Dir"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prev Tab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Next Tab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid "File Explorer"
|
||||
msgstr ""
|
||||
|
||||
@@ -326,12 +356,6 @@ msgstr ""
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -347,6 +371,45 @@ msgstr ""
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Exe Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search settings..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
msgid "Setting"
|
||||
msgstr ""
|
||||
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "No changes to apply."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to apply changes. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings updated successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -410,9 +473,6 @@ msgstr ""
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr ""
|
||||
|
||||
@@ -432,6 +492,12 @@ msgstr ""
|
||||
msgid "Installation error."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh Grid"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game library refreshed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -444,6 +510,15 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "A refresh is already in progress..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refreshing..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refreshing game library..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
@@ -494,14 +569,21 @@ msgstr ""
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgid "Clearing prefix..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start prefix clear process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgid "Prefix clear failed with exit code {}."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to run clear prefix command: {}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
@@ -579,6 +661,9 @@ msgstr ""
|
||||
msgid "Games Display Filter:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad Type:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Proxy URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -603,6 +688,12 @@ msgstr ""
|
||||
msgid "Application Fullscreen Mode:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minimize to tray on close"
|
||||
msgstr ""
|
||||
|
||||
msgid "Application Close Mode:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auto Fullscreen on Gamepad connected"
|
||||
msgstr ""
|
||||
|
||||
@@ -677,6 +768,10 @@ msgstr ""
|
||||
msgid "Error applying theme '{0}'"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Executable not found: {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid "LAST LAUNCH"
|
||||
msgstr ""
|
||||
|
||||
@@ -735,6 +830,281 @@ msgstr ""
|
||||
msgid "File not found: {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
||||
"combination - right Shift + F12)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enable vkBasalt by default to improve graphics in games running on "
|
||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Super + F : Toggle fullscreen\n"
|
||||
"Super + N : Toggle nearest neighbour filtering\n"
|
||||
"Super + U : Toggle FSR upscaling\n"
|
||||
"Super + Y : Toggle NIS upscaling\n"
|
||||
"Super + I : Increase FSR sharpness by 1\n"
|
||||
"Super + O : Decrease FSR sharpness by 1\n"
|
||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
||||
"Super + G : Toggle keyboard grab\n"
|
||||
"Super + C : Update clipboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable futex-based in-process synchronization primitives."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable vkd3d support - Ray Tracing"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
||||
msgstr ""
|
||||
|
||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disguise all NVIDIA GPU features"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run the application in WINE virtual desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run the application in a terminal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable startup mode and WINE version selector window"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use system GameMode for performance optimization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable forced use of third-party DirectX libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fix pink-tinted video playback in some games"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force US keyboard layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use WINE shader caching"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force use of built-in DXGI library"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable desktop compositing for performance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use container launch mode (recommended default)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force DirectInput protocol instead of XInput"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable experimental native Wayland support"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable HDR settings under native Wayland"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use async dxvk-sarek (experimental)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select the Wine or Proton version to use for this executable."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select the Wine prefix to use."
|
||||
msgstr ""
|
||||
|
||||
msgid "Latest"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stable"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulkan Backend"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Select the rendering backend for translating DirectX → Vulkan/OpenGL:\n"
|
||||
"\n"
|
||||
"• Auto – latest DXVK + VKD3D (recommended)\n"
|
||||
" The newest versions from the developers. Give the best compatibility "
|
||||
"and performance in modern games.\n"
|
||||
" Require up-to-date drivers:\n"
|
||||
" – AMD: Mesa 25.0+ or proprietary AMDVLK 2024.Q4+\n"
|
||||
" – NVIDIA: driver 550.54.14 or newer\n"
|
||||
" – Intel: Mesa 24.2+\n"
|
||||
"\n"
|
||||
"• Stable – proven DXVK + VKD3D\n"
|
||||
" Older but extremely well-tested versions. Work on any drivers that "
|
||||
"support Vulkan 1.3+.\n"
|
||||
" The best choice if you have problems with the newest versions.\n"
|
||||
"\n"
|
||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek\n"
|
||||
" Work even on older drivers and video cards that support at least "
|
||||
"Vulkan 1.1.\n"
|
||||
"\n"
|
||||
"• WINED3D – OpenGL translation (fallback)\n"
|
||||
" No DXVK/VKD3D used. DirectX is translated to OpenGL via built-in "
|
||||
"WineD3D.\n"
|
||||
" Works on absolutely any hardware, but performance is significantly "
|
||||
"lower.\n"
|
||||
" Use only as a last resort when nothing else starts."
|
||||
msgstr ""
|
||||
|
||||
msgid "Windows version"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Changing the WINDOWS emulation version may be required to run older "
|
||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
||||
msgstr ""
|
||||
|
||||
msgid "DLL Overrides"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Forced to use/disable the library only for the given application.\n"
|
||||
"\n"
|
||||
"A brief instruction:\n"
|
||||
"* libraries are written WITHOUT the .dll file extension\n"
|
||||
"* libraries are separated by semicolons - ;\n"
|
||||
"* library=n - use the WINDOWS (third-party) library\n"
|
||||
"* library=b - use WINE (built-in) library\n"
|
||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
||||
"* library= - disable the use of this library\n"
|
||||
"\n"
|
||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
||||
msgstr ""
|
||||
|
||||
msgid "Launch Arguments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Adding an argument after the .exe file, just like you would add an "
|
||||
"argument in a shortcut on a WINDOWS system.\n"
|
||||
"\n"
|
||||
"Example: -dx11 -skipintro 1"
|
||||
msgstr ""
|
||||
|
||||
msgid "CPU Cores Limit"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
||||
"recommended to set the value equal to 8)"
|
||||
msgstr ""
|
||||
|
||||
msgid "OpenGL Version"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"You can select the required OpenGL version, some games require a forced "
|
||||
"Compatibility Profile (COMP)."
|
||||
msgstr ""
|
||||
|
||||
msgid "VKD3D Feature Level"
|
||||
msgstr ""
|
||||
|
||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
||||
msgstr ""
|
||||
|
||||
msgid "Locale"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
||||
msgstr ""
|
||||
|
||||
msgid "Window Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Window mode (for Vulkan and OpenGL):\n"
|
||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
||||
"immediate - Unlimited frame rate + tearing.\n"
|
||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
||||
" rate."
|
||||
msgstr ""
|
||||
|
||||
msgid "AMD Vulkan Driver"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
||||
" vulkan will be used to run the game"
|
||||
msgstr ""
|
||||
|
||||
msgid "NUMA Node"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
||||
" single node reduces memory-access latency and limits costly core-to-core"
|
||||
" switches."
|
||||
msgstr ""
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
||||
"POT-Creation-Date: 2025-11-23 15:42+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: es_ES\n"
|
||||
@@ -76,10 +76,6 @@ msgstr ""
|
||||
msgid "Legendary executable not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Success"
|
||||
msgstr ""
|
||||
|
||||
@@ -124,6 +120,10 @@ msgstr ""
|
||||
msgid "Removed '{game_name}' from favorites"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launch game \"{name}\" with PortProton"
|
||||
msgstr ""
|
||||
@@ -252,13 +252,43 @@ msgstr ""
|
||||
msgid "Select All"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Dir"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prev Dir"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prev Tab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Next Tab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid "File Explorer"
|
||||
msgstr ""
|
||||
|
||||
@@ -326,12 +356,6 @@ msgstr ""
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -347,6 +371,45 @@ msgstr ""
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Exe Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search settings..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
msgid "Setting"
|
||||
msgstr ""
|
||||
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "No changes to apply."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to apply changes. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings updated successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -410,9 +473,6 @@ msgstr ""
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr ""
|
||||
|
||||
@@ -432,6 +492,12 @@ msgstr ""
|
||||
msgid "Installation error."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh Grid"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game library refreshed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -444,6 +510,15 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "A refresh is already in progress..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refreshing..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refreshing game library..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
@@ -494,14 +569,21 @@ msgstr ""
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgid "Clearing prefix..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start prefix clear process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgid "Prefix clear failed with exit code {}."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to run clear prefix command: {}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
@@ -579,6 +661,9 @@ msgstr ""
|
||||
msgid "Games Display Filter:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad Type:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Proxy URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -603,6 +688,12 @@ msgstr ""
|
||||
msgid "Application Fullscreen Mode:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minimize to tray on close"
|
||||
msgstr ""
|
||||
|
||||
msgid "Application Close Mode:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auto Fullscreen on Gamepad connected"
|
||||
msgstr ""
|
||||
|
||||
@@ -677,6 +768,10 @@ msgstr ""
|
||||
msgid "Error applying theme '{0}'"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Executable not found: {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid "LAST LAUNCH"
|
||||
msgstr ""
|
||||
|
||||
@@ -735,6 +830,281 @@ msgstr ""
|
||||
msgid "File not found: {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
||||
"combination - right Shift + F12)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enable vkBasalt by default to improve graphics in games running on "
|
||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Super + F : Toggle fullscreen\n"
|
||||
"Super + N : Toggle nearest neighbour filtering\n"
|
||||
"Super + U : Toggle FSR upscaling\n"
|
||||
"Super + Y : Toggle NIS upscaling\n"
|
||||
"Super + I : Increase FSR sharpness by 1\n"
|
||||
"Super + O : Decrease FSR sharpness by 1\n"
|
||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
||||
"Super + G : Toggle keyboard grab\n"
|
||||
"Super + C : Update clipboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable futex-based in-process synchronization primitives."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable vkd3d support - Ray Tracing"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
||||
msgstr ""
|
||||
|
||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disguise all NVIDIA GPU features"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run the application in WINE virtual desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run the application in a terminal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable startup mode and WINE version selector window"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use system GameMode for performance optimization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable forced use of third-party DirectX libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fix pink-tinted video playback in some games"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force US keyboard layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use WINE shader caching"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force use of built-in DXGI library"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable desktop compositing for performance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use container launch mode (recommended default)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force DirectInput protocol instead of XInput"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable experimental native Wayland support"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable HDR settings under native Wayland"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use async dxvk-sarek (experimental)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select the Wine or Proton version to use for this executable."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select the Wine prefix to use."
|
||||
msgstr ""
|
||||
|
||||
msgid "Latest"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stable"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulkan Backend"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Select the rendering backend for translating DirectX → Vulkan/OpenGL:\n"
|
||||
"\n"
|
||||
"• Auto – latest DXVK + VKD3D (recommended)\n"
|
||||
" The newest versions from the developers. Give the best compatibility "
|
||||
"and performance in modern games.\n"
|
||||
" Require up-to-date drivers:\n"
|
||||
" – AMD: Mesa 25.0+ or proprietary AMDVLK 2024.Q4+\n"
|
||||
" – NVIDIA: driver 550.54.14 or newer\n"
|
||||
" – Intel: Mesa 24.2+\n"
|
||||
"\n"
|
||||
"• Stable – proven DXVK + VKD3D\n"
|
||||
" Older but extremely well-tested versions. Work on any drivers that "
|
||||
"support Vulkan 1.3+.\n"
|
||||
" The best choice if you have problems with the newest versions.\n"
|
||||
"\n"
|
||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek\n"
|
||||
" Work even on older drivers and video cards that support at least "
|
||||
"Vulkan 1.1.\n"
|
||||
"\n"
|
||||
"• WINED3D – OpenGL translation (fallback)\n"
|
||||
" No DXVK/VKD3D used. DirectX is translated to OpenGL via built-in "
|
||||
"WineD3D.\n"
|
||||
" Works on absolutely any hardware, but performance is significantly "
|
||||
"lower.\n"
|
||||
" Use only as a last resort when nothing else starts."
|
||||
msgstr ""
|
||||
|
||||
msgid "Windows version"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Changing the WINDOWS emulation version may be required to run older "
|
||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
||||
msgstr ""
|
||||
|
||||
msgid "DLL Overrides"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Forced to use/disable the library only for the given application.\n"
|
||||
"\n"
|
||||
"A brief instruction:\n"
|
||||
"* libraries are written WITHOUT the .dll file extension\n"
|
||||
"* libraries are separated by semicolons - ;\n"
|
||||
"* library=n - use the WINDOWS (third-party) library\n"
|
||||
"* library=b - use WINE (built-in) library\n"
|
||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
||||
"* library= - disable the use of this library\n"
|
||||
"\n"
|
||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
||||
msgstr ""
|
||||
|
||||
msgid "Launch Arguments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Adding an argument after the .exe file, just like you would add an "
|
||||
"argument in a shortcut on a WINDOWS system.\n"
|
||||
"\n"
|
||||
"Example: -dx11 -skipintro 1"
|
||||
msgstr ""
|
||||
|
||||
msgid "CPU Cores Limit"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
||||
"recommended to set the value equal to 8)"
|
||||
msgstr ""
|
||||
|
||||
msgid "OpenGL Version"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"You can select the required OpenGL version, some games require a forced "
|
||||
"Compatibility Profile (COMP)."
|
||||
msgstr ""
|
||||
|
||||
msgid "VKD3D Feature Level"
|
||||
msgstr ""
|
||||
|
||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
||||
msgstr ""
|
||||
|
||||
msgid "Locale"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
||||
msgstr ""
|
||||
|
||||
msgid "Window Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Window mode (for Vulkan and OpenGL):\n"
|
||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
||||
"immediate - Unlimited frame rate + tearing.\n"
|
||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
||||
" rate."
|
||||
msgstr ""
|
||||
|
||||
msgid "AMD Vulkan Driver"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
||||
" vulkan will be used to run the game"
|
||||
msgstr ""
|
||||
|
||||
msgid "NUMA Node"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
||||
" single node reduces memory-access latency and limits costly core-to-core"
|
||||
" switches."
|
||||
msgstr ""
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
||||
"POT-Creation-Date: 2025-11-23 15:43+0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -74,10 +74,6 @@ msgstr ""
|
||||
msgid "Legendary executable not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Success"
|
||||
msgstr ""
|
||||
|
||||
@@ -122,6 +118,10 @@ msgstr ""
|
||||
msgid "Removed '{game_name}' from favorites"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launch game \"{name}\" with PortProton"
|
||||
msgstr ""
|
||||
@@ -250,13 +250,43 @@ msgstr ""
|
||||
msgid "Select All"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select Dir"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prev Dir"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Toggle"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Prev Tab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Next Tab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid "File Explorer"
|
||||
msgstr ""
|
||||
|
||||
@@ -324,12 +354,6 @@ msgstr ""
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Install"
|
||||
msgstr ""
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr ""
|
||||
|
||||
@@ -345,6 +369,45 @@ msgstr ""
|
||||
msgid "Components installed successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Exe Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search settings..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Main"
|
||||
msgstr ""
|
||||
|
||||
msgid "Advanced"
|
||||
msgstr ""
|
||||
|
||||
msgid "Setting"
|
||||
msgstr ""
|
||||
|
||||
msgid "Value"
|
||||
msgstr ""
|
||||
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "disabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "No changes to apply."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to apply changes. Check logs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Settings updated successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -408,9 +471,6 @@ msgstr ""
|
||||
msgid "Fullscreen"
|
||||
msgstr ""
|
||||
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr ""
|
||||
|
||||
@@ -430,6 +490,12 @@ msgstr ""
|
||||
msgid "Installation error."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refresh Grid"
|
||||
msgstr ""
|
||||
|
||||
msgid "Game library refreshed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr ""
|
||||
|
||||
@@ -442,6 +508,15 @@ msgstr ""
|
||||
msgid "Find Games ..."
|
||||
msgstr ""
|
||||
|
||||
msgid "A refresh is already in progress..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refreshing..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Refreshing game library..."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr ""
|
||||
@@ -492,14 +567,21 @@ msgstr ""
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgid "Clearing prefix..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start prefix clear process."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix cleared successfully."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgid "Prefix clear failed with exit code {}."
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to run clear prefix command: {}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
@@ -577,6 +659,9 @@ msgstr ""
|
||||
msgid "Games Display Filter:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Gamepad Type:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Proxy URL"
|
||||
msgstr ""
|
||||
|
||||
@@ -601,6 +686,12 @@ msgstr ""
|
||||
msgid "Application Fullscreen Mode:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minimize to tray on close"
|
||||
msgstr ""
|
||||
|
||||
msgid "Application Close Mode:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Auto Fullscreen on Gamepad connected"
|
||||
msgstr ""
|
||||
|
||||
@@ -675,6 +766,10 @@ msgstr ""
|
||||
msgid "Error applying theme '{0}'"
|
||||
msgstr ""
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Executable not found: {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid "LAST LAUNCH"
|
||||
msgstr ""
|
||||
|
||||
@@ -733,6 +828,281 @@ msgstr ""
|
||||
msgid "File not found: {0}"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
||||
"combination - right Shift + F12)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enable vkBasalt by default to improve graphics in games running on "
|
||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Super + F : Toggle fullscreen\n"
|
||||
"Super + N : Toggle nearest neighbour filtering\n"
|
||||
"Super + U : Toggle FSR upscaling\n"
|
||||
"Super + Y : Toggle NIS upscaling\n"
|
||||
"Super + I : Increase FSR sharpness by 1\n"
|
||||
"Super + O : Decrease FSR sharpness by 1\n"
|
||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
||||
"Super + G : Toggle keyboard grab\n"
|
||||
"Super + C : Update clipboard"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable futex-based in-process synchronization primitives."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable vkd3d support - Ray Tracing"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
||||
msgstr ""
|
||||
|
||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disguise all NVIDIA GPU features"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run the application in WINE virtual desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "Run the application in a terminal"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable startup mode and WINE version selector window"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use system GameMode for performance optimization"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable forced use of third-party DirectX libraries"
|
||||
msgstr ""
|
||||
|
||||
msgid "Fix pink-tinted video playback in some games"
|
||||
msgstr ""
|
||||
|
||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force US keyboard layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use WINE shader caching"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force use of built-in DXGI library"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
||||
msgstr ""
|
||||
|
||||
msgid "Disable desktop compositing for performance"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use container launch mode (recommended default)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force DirectInput protocol instead of XInput"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable experimental native Wayland support"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable HDR settings under native Wayland"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
||||
msgstr ""
|
||||
|
||||
msgid "Use async dxvk-sarek (experimental)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Wine Version"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select the Wine or Proton version to use for this executable."
|
||||
msgstr ""
|
||||
|
||||
msgid "Prefix Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Select the Wine prefix to use."
|
||||
msgstr ""
|
||||
|
||||
msgid "Latest"
|
||||
msgstr ""
|
||||
|
||||
msgid "Stable"
|
||||
msgstr ""
|
||||
|
||||
msgid "Vulkan Backend"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Select the rendering backend for translating DirectX → Vulkan/OpenGL:\n"
|
||||
"\n"
|
||||
"• Auto – latest DXVK + VKD3D (recommended)\n"
|
||||
" The newest versions from the developers. Give the best compatibility "
|
||||
"and performance in modern games.\n"
|
||||
" Require up-to-date drivers:\n"
|
||||
" – AMD: Mesa 25.0+ or proprietary AMDVLK 2024.Q4+\n"
|
||||
" – NVIDIA: driver 550.54.14 or newer\n"
|
||||
" – Intel: Mesa 24.2+\n"
|
||||
"\n"
|
||||
"• Stable – proven DXVK + VKD3D\n"
|
||||
" Older but extremely well-tested versions. Work on any drivers that "
|
||||
"support Vulkan 1.3+.\n"
|
||||
" The best choice if you have problems with the newest versions.\n"
|
||||
"\n"
|
||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek\n"
|
||||
" Work even on older drivers and video cards that support at least "
|
||||
"Vulkan 1.1.\n"
|
||||
"\n"
|
||||
"• WINED3D – OpenGL translation (fallback)\n"
|
||||
" No DXVK/VKD3D used. DirectX is translated to OpenGL via built-in "
|
||||
"WineD3D.\n"
|
||||
" Works on absolutely any hardware, but performance is significantly "
|
||||
"lower.\n"
|
||||
" Use only as a last resort when nothing else starts."
|
||||
msgstr ""
|
||||
|
||||
msgid "Windows version"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Changing the WINDOWS emulation version may be required to run older "
|
||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
||||
msgstr ""
|
||||
|
||||
msgid "DLL Overrides"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Forced to use/disable the library only for the given application.\n"
|
||||
"\n"
|
||||
"A brief instruction:\n"
|
||||
"* libraries are written WITHOUT the .dll file extension\n"
|
||||
"* libraries are separated by semicolons - ;\n"
|
||||
"* library=n - use the WINDOWS (third-party) library\n"
|
||||
"* library=b - use WINE (built-in) library\n"
|
||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
||||
"* library= - disable the use of this library\n"
|
||||
"\n"
|
||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
||||
msgstr ""
|
||||
|
||||
msgid "Launch Arguments"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Adding an argument after the .exe file, just like you would add an "
|
||||
"argument in a shortcut on a WINDOWS system.\n"
|
||||
"\n"
|
||||
"Example: -dx11 -skipintro 1"
|
||||
msgstr ""
|
||||
|
||||
msgid "CPU Cores Limit"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
||||
"recommended to set the value equal to 8)"
|
||||
msgstr ""
|
||||
|
||||
msgid "OpenGL Version"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"You can select the required OpenGL version, some games require a forced "
|
||||
"Compatibility Profile (COMP)."
|
||||
msgstr ""
|
||||
|
||||
msgid "VKD3D Feature Level"
|
||||
msgstr ""
|
||||
|
||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
||||
msgstr ""
|
||||
|
||||
msgid "Locale"
|
||||
msgstr ""
|
||||
|
||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
||||
msgstr ""
|
||||
|
||||
msgid "Window Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Window mode (for Vulkan and OpenGL):\n"
|
||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
||||
"immediate - Unlimited frame rate + tearing.\n"
|
||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
||||
" rate."
|
||||
msgstr ""
|
||||
|
||||
msgid "AMD Vulkan Driver"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
||||
" vulkan will be used to run the game"
|
||||
msgstr ""
|
||||
|
||||
msgid "NUMA Node"
|
||||
msgstr ""
|
||||
|
||||
msgid ""
|
||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
||||
" single node reduces memory-access latency and limits costly core-to-core"
|
||||
" switches."
|
||||
msgstr ""
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
||||
"PO-Revision-Date: 2025-10-12 17:13+0500\n"
|
||||
"POT-Creation-Date: 2025-11-23 15:42+0500\n"
|
||||
"PO-Revision-Date: 2025-11-23 15:42+0500\n"
|
||||
"Last-Translator: \n"
|
||||
"Language: ru_RU\n"
|
||||
"Language-Team: ru_RU <LL@li.org>\n"
|
||||
@@ -77,10 +77,6 @@ msgstr "Остановлен(а) '{game_name}'"
|
||||
msgid "Legendary executable not found at {path}"
|
||||
msgstr "Legendary не найден по пути {path}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr "start.sh не найден по адресу {path}"
|
||||
|
||||
msgid "Success"
|
||||
msgstr "Успешно"
|
||||
|
||||
@@ -127,6 +123,10 @@ msgstr "'{game_name}' был(а) добавлен(а) в избранное"
|
||||
msgid "Removed '{game_name}' from favorites"
|
||||
msgstr "'{game_name}' был(а) удалён(а) из избранного"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "start.sh not found at {path}"
|
||||
msgstr "start.sh не найден по адресу {path}"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launch game \"{name}\" with PortProton"
|
||||
msgstr "Запустить игру \"{name}\" с помощью PortProton"
|
||||
@@ -259,13 +259,43 @@ msgstr "Удалить"
|
||||
msgid "Select All"
|
||||
msgstr "Выбрать всё"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgstr "Идёт запуск {0}"
|
||||
msgid "Open"
|
||||
msgstr "Открыть"
|
||||
|
||||
msgid "Select Dir"
|
||||
msgstr "Выбрать папку"
|
||||
|
||||
msgid "Prev Dir"
|
||||
msgstr "Предыдущий каталог"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "Отмена"
|
||||
|
||||
msgid "Toggle"
|
||||
msgstr "Переключить"
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr "Принудительно установить"
|
||||
|
||||
msgid "Prev Tab"
|
||||
msgstr "Предыдущая вкладка"
|
||||
|
||||
msgid "Next Tab"
|
||||
msgstr "Следующая вкладка"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Сохранить"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Launching {0}"
|
||||
msgstr "Идёт запуск {0}"
|
||||
|
||||
msgid "File Explorer"
|
||||
msgstr "Проводник"
|
||||
|
||||
@@ -333,12 +363,6 @@ msgstr "Шрифты"
|
||||
msgid "Settings"
|
||||
msgstr "Настройки"
|
||||
|
||||
msgid "Force Install"
|
||||
msgstr "Принудительно установить"
|
||||
|
||||
msgid "Install"
|
||||
msgstr "Установить"
|
||||
|
||||
msgid "Winetricks not found. Please try again."
|
||||
msgstr "Winetricks не найден. Повторите попытку."
|
||||
|
||||
@@ -354,6 +378,45 @@ msgstr "Установка не удалась. Проверьте журнал
|
||||
msgid "Components installed successfully."
|
||||
msgstr "Компоненты успешно установлены."
|
||||
|
||||
msgid "Exe Settings"
|
||||
msgstr "Настройки EXE"
|
||||
|
||||
msgid "Search:"
|
||||
msgstr "Поиск:"
|
||||
|
||||
msgid "Search settings..."
|
||||
msgstr "Поиск настроек..."
|
||||
|
||||
msgid "Main"
|
||||
msgstr "Основные"
|
||||
|
||||
msgid "Advanced"
|
||||
msgstr "Расширенные"
|
||||
|
||||
msgid "Setting"
|
||||
msgstr "Параметр"
|
||||
|
||||
msgid "Value"
|
||||
msgstr "Значение"
|
||||
|
||||
msgid "Description"
|
||||
msgstr "Описание"
|
||||
|
||||
msgid "disabled"
|
||||
msgstr "отключено"
|
||||
|
||||
msgid "Info"
|
||||
msgstr "Информация"
|
||||
|
||||
msgid "No changes to apply."
|
||||
msgstr "Изменений для применения нет."
|
||||
|
||||
msgid "Failed to apply changes. Check logs."
|
||||
msgstr "Не удалось применить изменения. Проверьте логи."
|
||||
|
||||
msgid "Settings updated successfully."
|
||||
msgstr "Настройки успешно обновлены."
|
||||
|
||||
msgid "Loading Epic Games Store games..."
|
||||
msgstr "Загрузка игр из Epic Games Store..."
|
||||
|
||||
@@ -417,9 +480,6 @@ msgstr "Назад"
|
||||
msgid "Fullscreen"
|
||||
msgstr "Полный экран"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
msgid "Installation already in progress."
|
||||
msgstr "Установка уже выполняется."
|
||||
|
||||
@@ -439,6 +499,12 @@ msgstr "Установка не удалась."
|
||||
msgid "Installation error."
|
||||
msgstr "Ошибка установки."
|
||||
|
||||
msgid "Refresh Grid"
|
||||
msgstr "Обновить"
|
||||
|
||||
msgid "Game library refreshed"
|
||||
msgstr "Игровая библиотека обновлена"
|
||||
|
||||
msgid "Loading Steam games..."
|
||||
msgstr "Загрузка игр из Steam..."
|
||||
|
||||
@@ -451,6 +517,15 @@ msgstr "Игровая библиотека"
|
||||
msgid "Find Games ..."
|
||||
msgstr "Найти игры..."
|
||||
|
||||
msgid "A refresh is already in progress..."
|
||||
msgstr "Обновление уже выполняется..."
|
||||
|
||||
msgid "Refreshing..."
|
||||
msgstr "Обновление..."
|
||||
|
||||
msgid "Refreshing game library..."
|
||||
msgstr "Обновление игровой библиотеки..."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Added '{name}'"
|
||||
msgstr "'{name}' добавлен(а)"
|
||||
@@ -501,17 +576,22 @@ msgstr "Подтвердите очистку"
|
||||
msgid "Are you sure you want to clear prefix '{}'?"
|
||||
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Prefix '{}' cleared successfully."
|
||||
msgstr "Префикс '{}' успешно удален."
|
||||
msgid "Clearing prefix..."
|
||||
msgstr "Очистка префикса..."
|
||||
|
||||
msgid "Failed to start prefix clear process."
|
||||
msgstr "Не удалось запустить процесс очистки префикса."
|
||||
|
||||
msgid "Prefix cleared successfully."
|
||||
msgstr "Префикс удален успешно."
|
||||
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Prefix '{}' cleared with errors:\n"
|
||||
"{}"
|
||||
msgstr ""
|
||||
"Префикс '{}' очищен с ошибками:\n"
|
||||
"{}"
|
||||
msgid "Prefix clear failed with exit code {}."
|
||||
msgstr "Очистка префикса завершилась с кодом завершения {}."
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Failed to run clear prefix command: {}"
|
||||
msgstr "Не удалось выполнить команду очистки префикса: {}"
|
||||
|
||||
msgid "Failed to start backup process."
|
||||
msgstr "Не удалось запустить процесс резервного копирования."
|
||||
@@ -588,6 +668,9 @@ msgstr "все"
|
||||
msgid "Games Display Filter:"
|
||||
msgstr "Фильтр игр:"
|
||||
|
||||
msgid "Gamepad Type:"
|
||||
msgstr "Тип геймпада:"
|
||||
|
||||
msgid "Proxy URL"
|
||||
msgstr "Адрес прокси"
|
||||
|
||||
@@ -612,6 +695,12 @@ msgstr "Запуск приложения в полноэкранном режи
|
||||
msgid "Application Fullscreen Mode:"
|
||||
msgstr "Режим полноэкранного отображения приложения:"
|
||||
|
||||
msgid "Minimize to tray on close"
|
||||
msgstr "Сворачивать в трей при закрытии"
|
||||
|
||||
msgid "Application Close Mode:"
|
||||
msgstr "Режим закрытия приложения:"
|
||||
|
||||
msgid "Auto Fullscreen on Gamepad connected"
|
||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
||||
|
||||
@@ -688,6 +777,10 @@ msgstr "Тема '{0}' применена успешно"
|
||||
msgid "Error applying theme '{0}'"
|
||||
msgstr "Ошибка при применение темы '{0}'"
|
||||
|
||||
#, python-brace-format
|
||||
msgid "Executable not found: {0}"
|
||||
msgstr "Исполняемый файл не найден: {0}"
|
||||
|
||||
msgid "LAST LAUNCH"
|
||||
msgstr "Последний запуск"
|
||||
|
||||
@@ -746,6 +839,360 @@ msgstr "Неправильный формат команды (flatpak)"
|
||||
msgid "File not found: {0}"
|
||||
msgstr "Файл не найден: {0}"
|
||||
|
||||
msgid ""
|
||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
||||
"combination - right Shift + F12)"
|
||||
msgstr ""
|
||||
"Использование мониторинга FPS и нагрузки системы (включается и "
|
||||
"выключается комбинацией клавиш - правая Shift + F12)"
|
||||
|
||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
||||
msgstr "Принудительное использование системных настроек MANGOHUD (GOverlay и т.д.)"
|
||||
|
||||
msgid ""
|
||||
"Enable vkBasalt by default to improve graphics in games running on "
|
||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
||||
msgstr ""
|
||||
"Включить vkBasalt по умолчанию для улучшения графики в играх на Vulkan. "
|
||||
"(Горячая клавиша HOME отключает vkbasalt)"
|
||||
|
||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
||||
msgstr "Принудительное использование системных настроек VKBASALT (GOverlay и т.д.)"
|
||||
|
||||
msgid ""
|
||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
||||
msgstr ""
|
||||
"Включить dgVoodoo2. Принудительное использование всех библиотек dgVoodoo2"
|
||||
" (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) на всех 3D API."
|
||||
|
||||
msgid ""
|
||||
"Super + F : Toggle fullscreen\n"
|
||||
"Super + N : Toggle nearest neighbour filtering\n"
|
||||
"Super + U : Toggle FSR upscaling\n"
|
||||
"Super + Y : Toggle NIS upscaling\n"
|
||||
"Super + I : Increase FSR sharpness by 1\n"
|
||||
"Super + O : Decrease FSR sharpness by 1\n"
|
||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
||||
"Super + G : Toggle keyboard grab\n"
|
||||
"Super + C : Update clipboard"
|
||||
msgstr ""
|
||||
"Super + F: Переключить полноэкранный режим\n"
|
||||
"Super + N: Переключить фильтрацию ближайшего соседа\n"
|
||||
"Super + U: Переключить апскейлинг FSR\n"
|
||||
"Super + Y: Переключить апскейлинг NIS\n"
|
||||
"Super + I: Увеличить резкость FSR на 1\n"
|
||||
"Super + O: Уменьшить резкость FSR на 1\n"
|
||||
"Super + S: Сделать скриншот (сейчас сохраняется в "
|
||||
"/tmp/gamescope_DATE.png)\n"
|
||||
"Super + G: Переключить захват клавиатуры\n"
|
||||
"Super + C: Обновить буфер обмена"
|
||||
|
||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
||||
msgstr "Включить примитивы синхронизации в процессе на основе eventfd."
|
||||
|
||||
msgid "Enable futex-based in-process synchronization primitives."
|
||||
msgstr "Включить примитивы синхронизации в процессе на основе futex."
|
||||
|
||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
||||
msgstr "Включить синхронизацию в процессе через драйвер ntsync в Linux."
|
||||
|
||||
msgid "Enable vkd3d support - Ray Tracing"
|
||||
msgstr "Включить поддержку vkd3d — трассировка лучей"
|
||||
|
||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
||||
msgstr "Включить DLSS на поддерживаемых видеокартах NVIDIA"
|
||||
|
||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
||||
msgstr "Включить OptiScaler (замена апскейлера / генератора кадров)"
|
||||
|
||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
||||
msgstr "Включить генерацию кадров Lossless Scaling (экспериментально)"
|
||||
|
||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
||||
msgstr "Апскейлинг FSR в полноэкранном режиме с ProtonGE ниже родного разрешения"
|
||||
|
||||
msgid "Disguise all NVIDIA GPU features"
|
||||
msgstr "Маскировать все функции GPU NVIDIA"
|
||||
|
||||
msgid "Run the application in WINE virtual desktop"
|
||||
msgstr "Запускать приложение в виртуальном рабочем столе WINE"
|
||||
|
||||
msgid "Run the application in a terminal"
|
||||
msgstr "Запускать приложение в терминале"
|
||||
|
||||
msgid "Disable startup mode and WINE version selector window"
|
||||
msgstr "Отключить окно выбора режима запуска и версии WINE"
|
||||
|
||||
msgid "Use system GameMode for performance optimization"
|
||||
msgstr "Использовать системный GameMode для оптимизации производительности"
|
||||
|
||||
msgid "Enable forced use of third-party DirectX libraries"
|
||||
msgstr "Включить принудительное использование сторонних библиотек DirectX"
|
||||
|
||||
msgid "Fix pink-tinted video playback in some games"
|
||||
msgstr "Исправить розовый оттенок видео в некоторых играх"
|
||||
|
||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
||||
msgstr "Уменьшить задержку PulseAudio для исправления прерывистого звука"
|
||||
|
||||
msgid "Force US keyboard layout"
|
||||
msgstr "Принудительно использовать раскладку клавиатуры US"
|
||||
|
||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
||||
msgstr "Использовать GStreamer для внутриигровых клипов (поддержка WMF)"
|
||||
|
||||
msgid "Use WINE shader caching"
|
||||
msgstr "Использовать кэширование шейдеров WINE"
|
||||
|
||||
msgid "Force use of built-in DXGI library"
|
||||
msgstr "Принудительно использовать встроенную библиотеку DXGI"
|
||||
|
||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
||||
msgstr "Включить среды выполнения Easy Anti-Cheat и BattlEye"
|
||||
|
||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
||||
msgstr "Использовать системные слои Vulkan (MangoHud, vkBasalt, OBS и т.д.)"
|
||||
|
||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
||||
msgstr "Включить захват OBS Studio через obs-vkcapture"
|
||||
|
||||
msgid "Disable desktop compositing for performance"
|
||||
msgstr "Отключить композицию рабочего стола для производительности"
|
||||
|
||||
msgid "Use container launch mode (recommended default)"
|
||||
msgstr "Использовать режим запуска в контейнере (рекомендуемый по умолчанию)"
|
||||
|
||||
msgid "Force DirectInput protocol instead of XInput"
|
||||
msgstr "Принудительно использовать протокол DirectInput вместо XInput"
|
||||
|
||||
msgid "Enable experimental native Wayland support"
|
||||
msgstr "Включить экспериментальную нативную поддержку Wayland"
|
||||
|
||||
msgid "Enable HDR settings under native Wayland"
|
||||
msgstr "Включить настройки HDR под нативным Wayland"
|
||||
|
||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
||||
msgstr "Использовать Gallium Zink (OpenGL через Vulkan)"
|
||||
|
||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
||||
msgstr "Использовать Gallium Nine (нативный DirectX 9 для Mesa)"
|
||||
|
||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
||||
msgstr "Использовать бэкенд Vulkan WineD3D (Damavand)"
|
||||
|
||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
||||
msgstr "Использовать встроенные dxvk/vkd3d из Wine/Proton"
|
||||
|
||||
msgid "Use async dxvk-sarek (experimental)"
|
||||
msgstr "Использовать асинхронный dxvk-sarek (экспериментально)"
|
||||
|
||||
msgid "Wine Version"
|
||||
msgstr "Версия Wine"
|
||||
|
||||
msgid "Select the Wine or Proton version to use for this executable."
|
||||
msgstr "Выбор версии Wine или Proton для использования с этим исполняемым файлом."
|
||||
|
||||
msgid "Prefix Name"
|
||||
msgstr "Имя префикса"
|
||||
|
||||
msgid "Select the Wine prefix to use."
|
||||
msgstr "Выбор версии Wine для использования."
|
||||
|
||||
msgid "Latest"
|
||||
msgstr "Последние"
|
||||
|
||||
msgid "Stable"
|
||||
msgstr "Стабильные"
|
||||
|
||||
msgid "Vulkan Backend"
|
||||
msgstr "Vulkan рендеринг"
|
||||
|
||||
msgid ""
|
||||
"Select the rendering backend for translating DirectX → Vulkan/OpenGL:\n"
|
||||
"\n"
|
||||
"• Auto – latest DXVK + VKD3D (recommended)\n"
|
||||
" The newest versions from the developers. Give the best compatibility "
|
||||
"and performance in modern games.\n"
|
||||
" Require up-to-date drivers:\n"
|
||||
" – AMD: Mesa 25.0+ or proprietary AMDVLK 2024.Q4+\n"
|
||||
" – NVIDIA: driver 550.54.14 or newer\n"
|
||||
" – Intel: Mesa 24.2+\n"
|
||||
"\n"
|
||||
"• Stable – proven DXVK + VKD3D\n"
|
||||
" Older but extremely well-tested versions. Work on any drivers that "
|
||||
"support Vulkan 1.3+.\n"
|
||||
" The best choice if you have problems with the newest versions.\n"
|
||||
"\n"
|
||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek\n"
|
||||
" Work even on older drivers and video cards that support at least "
|
||||
"Vulkan 1.1.\n"
|
||||
"\n"
|
||||
"• WINED3D – OpenGL translation (fallback)\n"
|
||||
" No DXVK/VKD3D used. DirectX is translated to OpenGL via built-in "
|
||||
"WineD3D.\n"
|
||||
" Works on absolutely any hardware, but performance is significantly "
|
||||
"lower.\n"
|
||||
" Use only as a last resort when nothing else starts."
|
||||
msgstr ""
|
||||
"Выбор рендеринга для трансляции DirectX → Vulkan/OpenGL:\n"
|
||||
"\n"
|
||||
"• Авто – последние версии DXVK + VKD3D (рекомендуется)\n"
|
||||
" Новейшие версии от разработчиков. Обеспечивают наилучшую совместимость и"
|
||||
" производительность в современных играх.\n"
|
||||
" Требуются актуальные драйверы:\n"
|
||||
" – AMD: Mesa 25.0+ или проприетарный AMDVLK 2024.Q4+\n"
|
||||
" – NVIDIA: 550.54.14 или новее\n"
|
||||
" – Intel: Mesa 24.2+\n"
|
||||
"\n"
|
||||
"• Стабильный – проверенные версии DXVK + VKD3D\n"
|
||||
" Более старые, но тщательно протестированные версии. Работают с любыми "
|
||||
"драйверами, поддерживающие Vulkan 1.3+.\n"
|
||||
" Лучший выбор, если у вас возникли проблемы с последними версиями.\n"
|
||||
"\n"
|
||||
"• Sarek – экспериментальная версия DXVK-Sarek + VKD3D-Sarek\n"
|
||||
"Работает даже на старых драйверах и видеокартах, поддерживающих как "
|
||||
"минимум Vulkan 1.1.\n"
|
||||
"• WINED3D – трансляция OpenGL (для видеокарт без поддержки Vulkan)\n"
|
||||
"DXVK/VKD3D не используется. DirectX транслируется в OpenGL через "
|
||||
"встроенную WineD3D.Работает абсолютно на любом оборудовании, но "
|
||||
"производительность значительно снижается.Используйте только в крайнем "
|
||||
"случае, когда ничего другое не запускается."
|
||||
|
||||
msgid "Windows version"
|
||||
msgstr "Версия Windows"
|
||||
|
||||
msgid ""
|
||||
"Changing the WINDOWS emulation version may be required to run older "
|
||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
||||
msgstr ""
|
||||
"Изменение версии эмуляции WINDOWS может потребоваться для запуска старых "
|
||||
"игр. Версии WINDOWS ниже 10 не поддерживают новые игры с DirectX 12"
|
||||
|
||||
msgid "DLL Overrides"
|
||||
msgstr "Переопределения DLL"
|
||||
|
||||
msgid ""
|
||||
"Forced to use/disable the library only for the given application.\n"
|
||||
"\n"
|
||||
"A brief instruction:\n"
|
||||
"* libraries are written WITHOUT the .dll file extension\n"
|
||||
"* libraries are separated by semicolons - ;\n"
|
||||
"* library=n - use the WINDOWS (third-party) library\n"
|
||||
"* library=b - use WINE (built-in) library\n"
|
||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
||||
"* library= - disable the use of this library\n"
|
||||
"\n"
|
||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
||||
msgstr ""
|
||||
"Принудительное использование/отключение библиотеки только для данного "
|
||||
"приложения.\n"
|
||||
"\n"
|
||||
"Краткая инструкция:\n"
|
||||
"* библиотеки пишутся БЕЗ расширения .dll\n"
|
||||
"* библиотеки разделяются точкой с запятой - ;\n"
|
||||
"* library=n — использовать библиотеку WINDOWS (стороннюю)\n"
|
||||
"* library=b — использовать библиотеку WINE (встроенную)\n"
|
||||
"* library=n,b — использовать библиотеку WINDOWS, затем WINE\n"
|
||||
"* library=b,n — использовать библиотеку WINE, затем WINDOWS\n"
|
||||
"* library= — отключить использование этой библиотеки\n"
|
||||
"\n"
|
||||
"Пример: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
||||
|
||||
msgid "Launch Arguments"
|
||||
msgstr "Аргументы запуска"
|
||||
|
||||
msgid ""
|
||||
"Adding an argument after the .exe file, just like you would add an "
|
||||
"argument in a shortcut on a WINDOWS system.\n"
|
||||
"\n"
|
||||
"Example: -dx11 -skipintro 1"
|
||||
msgstr ""
|
||||
"Добавление аргумента после файла .exe, как вы бы добавили аргумент в "
|
||||
"ярлыке на системе WINDOWS.\n"
|
||||
"\n"
|
||||
"Пример: -dx11 -skipintro 1"
|
||||
|
||||
msgid "CPU Cores Limit"
|
||||
msgstr "Ограничение ядер CPU"
|
||||
|
||||
msgid ""
|
||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
||||
"recommended to set the value equal to 8)"
|
||||
msgstr ""
|
||||
"Ограничение количества ядер CPU полезно для игр на Unity (рекомендуется "
|
||||
"установить значение равным 8)"
|
||||
|
||||
msgid "OpenGL Version"
|
||||
msgstr "Версия OpenGL"
|
||||
|
||||
msgid ""
|
||||
"You can select the required OpenGL version, some games require a forced "
|
||||
"Compatibility Profile (COMP)."
|
||||
msgstr ""
|
||||
"Вы можете выбрать требуемую версию OpenGL, некоторые игры требуют "
|
||||
"принудительного профиля совместимости (COMP)."
|
||||
|
||||
msgid "VKD3D Feature Level"
|
||||
msgstr "Уровень возможностей VKD3D"
|
||||
|
||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
||||
msgstr ""
|
||||
"Вы можете установить принудительный уровень возможностей VKD3D для игр на"
|
||||
" DirectX12"
|
||||
|
||||
msgid "Locale"
|
||||
msgstr "Локаль"
|
||||
|
||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
||||
msgstr ""
|
||||
"Принудительно установить определённую локаль для приложения. Исправляет "
|
||||
"проблемы с кодировкой в устаревшем ПО"
|
||||
|
||||
msgid "Window Mode"
|
||||
msgstr "Режим окна"
|
||||
|
||||
msgid ""
|
||||
"Window mode (for Vulkan and OpenGL):\n"
|
||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
||||
"immediate - Unlimited frame rate + tearing.\n"
|
||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
||||
" rate."
|
||||
msgstr ""
|
||||
"Режим окна (для Vulkan и OpenGL):\n"
|
||||
"fifo — Первый вошёл, первый вышел. Ограничивает частоту кадров + без "
|
||||
"разрывов. (VSync)\n"
|
||||
"immediate — Неограниченная частота кадров + разрывы.\n"
|
||||
"mailbox — Трёхбуферная. Неограниченная частота кадров + без разрывов.\n"
|
||||
"relaxed — То же, что fifo, но позволяет разрывы при частоте ниже частоты "
|
||||
"обновления монитора."
|
||||
|
||||
msgid "AMD Vulkan Driver"
|
||||
msgstr "Драйвер Vulkan AMD"
|
||||
|
||||
msgid ""
|
||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
||||
" vulkan will be used to run the game"
|
||||
msgstr ""
|
||||
"Выберите нужную реализацию Vulkan AMD. Выбор, какая реализация Vulkan "
|
||||
"будет использоваться для запуска игры"
|
||||
|
||||
msgid "NUMA Node"
|
||||
msgstr "Узел NUMA"
|
||||
|
||||
msgid ""
|
||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
||||
" single node reduces memory-access latency and limits costly core-to-core"
|
||||
" switches."
|
||||
msgstr ""
|
||||
"Узел NUMA для аффинности CPU. В многоядерных системах CPU разделены на "
|
||||
"узлы NUMA, каждый со своей локальной памятью и ядрами. Привязка игры к "
|
||||
"одному узлу уменьшает задержку доступа к памяти и ограничивает "
|
||||
"дорогостоящие переключения между ядрами."
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr "Перезагрузить"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import psutil
|
||||
import re
|
||||
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog
|
||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, WinetricksDialog, ExeSettingsDialog
|
||||
from portprotonqt.game_card import GameCard
|
||||
from portprotonqt.animations import DetailPageAnimations
|
||||
from portprotonqt.custom_widgets import ClickableLabel, AutoSizeButton, NavLabel, FlowLayout
|
||||
@@ -29,7 +29,8 @@ from portprotonqt.config_utils import (
|
||||
read_display_filter, read_favorites, save_favorites, save_time_config, save_sort_method,
|
||||
save_display_filter, save_proxy_config, read_proxy_config, read_fullscreen_config,
|
||||
save_fullscreen_config, read_window_geometry, save_window_geometry, reset_config,
|
||||
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config
|
||||
clear_cache, read_auto_fullscreen_gamepad, save_auto_fullscreen_gamepad, read_rumble_config, save_rumble_config, read_gamepad_type, save_gamepad_type, read_minimize_to_tray, save_minimize_to_tray,
|
||||
read_auto_card_size, save_auto_card_size, get_portproton_start_command
|
||||
)
|
||||
from portprotonqt.localization import _, get_egs_language, read_metadata_translations
|
||||
from portprotonqt.howlongtobeat_api import HowLongToBeat
|
||||
@@ -39,7 +40,7 @@ from portprotonqt.game_library_manager import GameLibraryManager
|
||||
from portprotonqt.virtual_keyboard import VirtualKeyboard
|
||||
|
||||
from PySide6.QtWidgets import (QLineEdit, QMainWindow, QStatusBar, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QStackedWidget, QComboBox,
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller)
|
||||
QDialog, QFormLayout, QFrame, QGraphicsDropShadowEffect, QMessageBox, QApplication, QPushButton, QProgressBar, QCheckBox, QSizePolicy, QGridLayout, QScrollArea, QScroller, QSlider)
|
||||
from PySide6.QtCore import Qt, QAbstractAnimation, QUrl, Signal, QTimer, Slot, QProcess
|
||||
from PySide6.QtGui import QIcon, QPixmap, QColor, QDesktopServices
|
||||
from typing import cast
|
||||
@@ -63,6 +64,7 @@ class MainWindow(QMainWindow):
|
||||
self.theme = self.theme_manager.apply_theme(selected_theme)
|
||||
self.tray_manager = TrayManager(self, app_name, self.current_theme_name)
|
||||
self.card_width = read_card_size()
|
||||
self.auto_card_width = read_auto_card_size()
|
||||
self._last_card_width = self.card_width
|
||||
self.setWindowTitle(f"{app_name} {version}")
|
||||
self.setMinimumSize(800, 600)
|
||||
@@ -72,6 +74,7 @@ class MainWindow(QMainWindow):
|
||||
self.target_exe = None
|
||||
self.current_running_button = None
|
||||
self.portproton_location = get_portproton_location()
|
||||
self.start_sh = get_portproton_start_command()
|
||||
|
||||
self.game_library_manager = GameLibraryManager(self, self.theme, None)
|
||||
|
||||
@@ -260,6 +263,10 @@ class MainWindow(QMainWindow):
|
||||
GamepadType.XBOX: "xbox_y",
|
||||
GamepadType.PLAYSTATION: "ps_square",
|
||||
},
|
||||
'prev_dir': {
|
||||
GamepadType.XBOX: "xbox_y",
|
||||
GamepadType.PLAYSTATION: "ps_square",
|
||||
},
|
||||
}
|
||||
return mappings.get(action, {}).get(gtype, "placeholder")
|
||||
|
||||
@@ -452,11 +459,11 @@ class MainWindow(QMainWindow):
|
||||
self.current_install_script = script_name
|
||||
self.seen_progress = False
|
||||
self.current_percent = 0.0
|
||||
start_sh = os.path.join(self.portproton_location or "", "data", "scripts", "start.sh") if self.portproton_location else ""
|
||||
if not os.path.exists(start_sh):
|
||||
start_sh = self.start_sh
|
||||
if not start_sh:
|
||||
self.installing = False
|
||||
return
|
||||
cmd = [start_sh, "cli", "--autoinstall", script_name]
|
||||
cmd = start_sh + ["cli", "--autoinstall", script_name]
|
||||
self.install_process = QProcess(self)
|
||||
self.install_process.finished.connect(self.on_install_finished)
|
||||
self.install_process.errorOccurred.connect(self.on_install_error)
|
||||
@@ -516,12 +523,26 @@ class MainWindow(QMainWindow):
|
||||
self.install_monitor_timer = None
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(100)
|
||||
|
||||
if exit_code == 0:
|
||||
self.update_status_message.emit(_("Installation completed successfully."), 5000)
|
||||
QTimer.singleShot(500, lambda: self.restart_application())
|
||||
|
||||
desktop_dir = self.portproton_location or ""
|
||||
new_desktops = [e.path for e in os.scandir(desktop_dir) if e.name.endswith(".desktop")]
|
||||
if new_desktops:
|
||||
latest = max(new_desktops, key=os.path.getmtime)
|
||||
self._process_desktop_file_async(
|
||||
latest,
|
||||
lambda result: (
|
||||
self.game_library_manager.add_game_incremental(result)
|
||||
if result else None
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
self.update_status_message.emit(_("Installation failed."), 5000)
|
||||
QMessageBox.warning(self, _("Error"), f"Installation failed (code: {exit_code}).")
|
||||
|
||||
self.progress_bar.setVisible(False)
|
||||
self.current_install_script = None
|
||||
if self.install_process:
|
||||
@@ -544,6 +565,16 @@ class MainWindow(QMainWindow):
|
||||
self.game_library_manager.set_games(games)
|
||||
self.progress_bar.setVisible(False)
|
||||
|
||||
# Clear the refresh in progress flag
|
||||
if hasattr(self, '_refresh_in_progress'):
|
||||
self._refresh_in_progress = False
|
||||
|
||||
# Re-enable the refresh button if it exists
|
||||
if hasattr(self, 'refreshButton'):
|
||||
self.refreshButton.setEnabled(True)
|
||||
self.refreshButton.setText(_("Refresh Grid"))
|
||||
self.update_status_message.emit(_("Game library refreshed"), 3000)
|
||||
|
||||
def open_portproton_forum_topic(self, topic_name: str):
|
||||
"""Open the PortProton forum topic or search page for this game."""
|
||||
result = self.portproton_api.get_forum_topic_slug(topic_name)
|
||||
@@ -820,6 +851,25 @@ class MainWindow(QMainWindow):
|
||||
for i, btn in self.tabButtons.items():
|
||||
btn.setChecked(i == index)
|
||||
self.stackedWidget.setCurrentIndex(index)
|
||||
if hasattr(self, "game_library_manager"):
|
||||
mgr = self.game_library_manager
|
||||
if mgr.gamesListWidget and mgr.gamesListLayout:
|
||||
games_layout = mgr.gamesListLayout
|
||||
games_widget = mgr.gamesListWidget
|
||||
QTimer.singleShot(0, lambda: (
|
||||
games_layout.invalidate(),
|
||||
games_widget.adjustSize(),
|
||||
games_widget.updateGeometry()
|
||||
))
|
||||
if hasattr(self, "autoInstallContainer") and hasattr(self, "autoInstallContainerLayout"):
|
||||
auto_layout = self.autoInstallContainerLayout
|
||||
auto_widget = self.autoInstallContainer
|
||||
QTimer.singleShot(0, lambda: (
|
||||
auto_layout.invalidate(),
|
||||
auto_widget.adjustSize(),
|
||||
auto_widget.updateGeometry()
|
||||
))
|
||||
|
||||
|
||||
def openSystemOverlay(self):
|
||||
"""Opens the system overlay dialog."""
|
||||
@@ -841,7 +891,16 @@ class MainWindow(QMainWindow):
|
||||
self.addGameButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
|
||||
self.addGameButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.addGameButton.clicked.connect(self.openAddGameDialog)
|
||||
layout.addWidget(self.addGameButton, alignment=Qt.AlignmentFlag.AlignRight)
|
||||
layout.addWidget(self.addGameButton)
|
||||
|
||||
# Refresh button
|
||||
self.refreshButton = AutoSizeButton(_("Refresh Grid"), icon=self.theme_manager.get_icon("update"))
|
||||
self.refreshButton.setStyleSheet(self.theme.ADDGAME_BACK_BUTTON_STYLE)
|
||||
self.refreshButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
self.refreshButton.clicked.connect(self.refreshGames)
|
||||
layout.addWidget(self.refreshButton)
|
||||
|
||||
layout.addStretch() # Add stretch to push search to the right
|
||||
|
||||
self.searchEdit = CustomLineEdit(self, theme=self.theme)
|
||||
icon: QIcon = cast(QIcon, self.theme_manager.get_icon("search"))
|
||||
@@ -861,6 +920,32 @@ class MainWindow(QMainWindow):
|
||||
layout.addWidget(self.searchEdit)
|
||||
return self.container, self.searchEdit
|
||||
|
||||
def refreshGames(self):
|
||||
"""Refresh the game grid by reloading all games without restarting the application."""
|
||||
# Prevent multiple refreshes at once
|
||||
if hasattr(self, '_refresh_in_progress') and self._refresh_in_progress:
|
||||
# If refresh is already in progress, just update the status
|
||||
self.update_status_message.emit(_("A refresh is already in progress..."), 3000)
|
||||
return
|
||||
|
||||
# Mark that a refresh is in progress
|
||||
self._refresh_in_progress = True
|
||||
|
||||
# Clear the search field to ensure all games are shown after refresh
|
||||
self.searchEdit.clear()
|
||||
|
||||
# Disable the refresh button during refresh to prevent multiple clicks
|
||||
self.refreshButton.setEnabled(False)
|
||||
self.refreshButton.setText(_("Refreshing..."))
|
||||
|
||||
# Show progress bar
|
||||
self.progress_bar.setVisible(True)
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate
|
||||
self.update_status_message.emit(_("Refreshing game library..."), 0)
|
||||
|
||||
# Reload games using the existing loadGames functionality
|
||||
QTimer.singleShot(0, self.loadGames)
|
||||
|
||||
def on_search_text_changed(self, text: str):
|
||||
"""Search text change handler with debounce."""
|
||||
self.searchDebounceTimer.stop()
|
||||
@@ -1063,8 +1148,7 @@ class MainWindow(QMainWindow):
|
||||
autoInstallPage = QWidget()
|
||||
autoInstallPage.setStyleSheet(self.theme.LIBRARY_WIDGET_STYLE)
|
||||
autoInstallLayout = QVBoxLayout(autoInstallPage)
|
||||
autoInstallLayout.setContentsMargins(20, 0, 20, 0)
|
||||
autoInstallLayout.setSpacing(0)
|
||||
autoInstallLayout.setSpacing(15)
|
||||
|
||||
# Верхняя панель с заголовком и поиском
|
||||
headerWidget = QWidget()
|
||||
@@ -1113,6 +1197,25 @@ class MainWindow(QMainWindow):
|
||||
|
||||
autoInstallLayout.addWidget(self.autoInstallScrollArea)
|
||||
|
||||
# Slider for card size
|
||||
sliderLayout = QHBoxLayout()
|
||||
sliderLayout.setSpacing(0)
|
||||
sliderLayout.setContentsMargins(0, 0, 0, 0)
|
||||
sliderLayout.addStretch()
|
||||
|
||||
self.auto_size_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.auto_size_slider.setMinimum(200)
|
||||
self.auto_size_slider.setMaximum(250)
|
||||
self.auto_size_slider.setValue(self.auto_card_width)
|
||||
self.auto_size_slider.setTickInterval(10)
|
||||
self.auto_size_slider.setFixedWidth(150)
|
||||
self.auto_size_slider.setToolTip(f"{self.auto_card_width} px")
|
||||
self.auto_size_slider.setStyleSheet(self.theme.SLIDER_SIZE_STYLE)
|
||||
self.auto_size_slider.sliderReleased.connect(self.on_auto_slider_released)
|
||||
sliderLayout.addWidget(self.auto_size_slider)
|
||||
|
||||
autoInstallLayout.addLayout(sliderLayout)
|
||||
|
||||
# Хранение карточек
|
||||
self.autoInstallGameCards = {}
|
||||
self.allAutoInstallCards = []
|
||||
@@ -1122,7 +1225,7 @@ class MainWindow(QMainWindow):
|
||||
if exe_name in self.autoInstallGameCards and local_path:
|
||||
card = self.autoInstallGameCards[exe_name]
|
||||
card.cover_path = local_path
|
||||
load_pixmap_async(local_path, self.card_width, int(self.card_width * 1.5), card.on_cover_loaded)
|
||||
load_pixmap_async(local_path, self.auto_card_width, int(self.auto_card_width * 1.5), card.on_cover_loaded)
|
||||
|
||||
# Загрузка игр
|
||||
def on_autoinstall_games_loaded(games: list[tuple]):
|
||||
@@ -1158,7 +1261,7 @@ class MainWindow(QMainWindow):
|
||||
None, None, None, game_source,
|
||||
select_callback=select_callback,
|
||||
theme=self.theme,
|
||||
card_width=self.card_width,
|
||||
card_width=self.auto_card_width,
|
||||
parent=self.autoInstallContainer,
|
||||
)
|
||||
|
||||
@@ -1196,10 +1299,30 @@ class MainWindow(QMainWindow):
|
||||
# Показываем прогресс
|
||||
self.autoInstallProgress.setVisible(True)
|
||||
self.autoInstallProgress.setRange(0, 0)
|
||||
self.portproton_api.get_autoinstall_games_async(on_autoinstall_games_loaded)
|
||||
|
||||
# Store the thread to prevent premature destruction
|
||||
self.autoInstallLoadThread = self.portproton_api.start_autoinstall_games_load(on_autoinstall_games_loaded)
|
||||
|
||||
# Optional: Clean up thread when finished (prevents leak)
|
||||
if self.autoInstallLoadThread:
|
||||
def on_thread_finished():
|
||||
self.autoInstallLoadThread = None # Release reference
|
||||
self.autoInstallLoadThread.finished.connect(on_thread_finished)
|
||||
|
||||
self.stackedWidget.addWidget(autoInstallPage)
|
||||
|
||||
def on_auto_slider_released(self):
|
||||
"""Handles auto-install slider release to update card size."""
|
||||
if hasattr(self, 'auto_size_slider') and self.auto_size_slider:
|
||||
self.auto_card_width = self.auto_size_slider.value()
|
||||
self.auto_size_slider.setToolTip(f"{self.auto_card_width} px")
|
||||
save_auto_card_size(self.auto_card_width)
|
||||
for card in self.allAutoInstallCards:
|
||||
card.update_card_size(self.auto_card_width)
|
||||
self.autoInstallContainerLayout.invalidate()
|
||||
self.autoInstallContainer.updateGeometry()
|
||||
self.autoInstallScrollArea.updateGeometry()
|
||||
|
||||
def filterAutoInstallGames(self):
|
||||
"""Filter auto install game cards based on search text."""
|
||||
search_text = self.autoInstallSearchLineEdit.text().lower().strip()
|
||||
@@ -1347,12 +1470,10 @@ class MainWindow(QMainWindow):
|
||||
prefix = self.prefixCombo.currentText()
|
||||
if not wine or not prefix:
|
||||
return
|
||||
if not self.portproton_location:
|
||||
if not self.portproton_location or not self.start_sh:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
return
|
||||
cmd = [start_sh, "cli", cli_arg, wine, prefix]
|
||||
start_sh = self.start_sh
|
||||
cmd = start_sh + ["cli", cli_arg, wine, prefix]
|
||||
|
||||
# Показываем прогресс-бар перед запуском
|
||||
self.wine_progress_bar.setVisible(True)
|
||||
@@ -1431,12 +1552,13 @@ class MainWindow(QMainWindow):
|
||||
QMessageBox.warning(self, _("Error"), f"Failed to launch tool: {error}")
|
||||
|
||||
def clear_prefix(self):
|
||||
"""Очистка префикса (позже удалить)."""
|
||||
"""Очищает префикс"""
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
selected_wine = self.wineCombo.currentText()
|
||||
|
||||
if not selected_prefix or not selected_wine:
|
||||
return
|
||||
if not self.portproton_location:
|
||||
if not self.portproton_location or not self.start_sh:
|
||||
return
|
||||
|
||||
reply = QMessageBox.question(
|
||||
@@ -1449,98 +1571,35 @@ class MainWindow(QMainWindow):
|
||||
if reply != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
prefix_dir = os.path.join(self.portproton_location, "data", "prefixes", selected_prefix)
|
||||
if not os.path.exists(prefix_dir):
|
||||
start_sh = self.start_sh
|
||||
|
||||
self.wine_progress_bar.setVisible(True)
|
||||
self.update_status_message.emit(_("Clearing prefix..."), 0)
|
||||
|
||||
self.clear_process = QProcess(self)
|
||||
self.clear_process.finished.connect(lambda exitCode, exitStatus: self._on_clear_prefix_finished(exitCode))
|
||||
self.clear_process.errorOccurred.connect(lambda error: self._on_clear_prefix_error(error))
|
||||
cmd = start_sh + ["cli", "--clear_pfx", selected_wine, selected_prefix]
|
||||
self.clear_process.start(cmd[0], cmd[1:])
|
||||
|
||||
if not self.clear_process.waitForStarted(5000):
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.update_status_message.emit("", 0)
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start prefix clear process."))
|
||||
return
|
||||
|
||||
success = True
|
||||
errors = []
|
||||
|
||||
# Удаление файлов
|
||||
files_to_remove = [
|
||||
os.path.join(prefix_dir, "*.dot*"),
|
||||
os.path.join(prefix_dir, "*.prog*"),
|
||||
os.path.join(prefix_dir, ".wine_ver"),
|
||||
os.path.join(prefix_dir, "system.reg"),
|
||||
os.path.join(prefix_dir, "user.reg"),
|
||||
os.path.join(prefix_dir, "userdef.reg"),
|
||||
os.path.join(prefix_dir, "winetricks.log"),
|
||||
os.path.join(prefix_dir, ".update-timestamp"),
|
||||
os.path.join(prefix_dir, "drive_c", ".windows-serial"),
|
||||
]
|
||||
|
||||
import glob
|
||||
for pattern in files_to_remove:
|
||||
if "*" in pattern: # Глобальный паттерн
|
||||
matches = glob.glob(pattern)
|
||||
for file_path in matches:
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
else: # Конкретный файл
|
||||
try:
|
||||
if os.path.exists(pattern):
|
||||
os.remove(pattern)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
|
||||
# Удаление директорий
|
||||
dirs_to_remove = [
|
||||
os.path.join(prefix_dir, "drive_c", "windows"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Setup"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Windows"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "WindowsTask"),
|
||||
os.path.join(prefix_dir, "drive_c", "ProgramData", "Package Cache"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Application Data", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Local Settings", "Temporary Internet Files"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Application Data", "wine_gecko"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "Public", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Application Data", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Local Settings", "Temporary Internet Files"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "Microsoft"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Application Data", "wine_gecko"),
|
||||
os.path.join(prefix_dir, "drive_c", "users", "steamuser", "Temp"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Internet Explorer"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows Media Player"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files", "Windows NT"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Internet Explorer"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows Media Player"),
|
||||
os.path.join(prefix_dir, "drive_c", "Program Files (x86)", "Windows NT"),
|
||||
]
|
||||
|
||||
import shutil
|
||||
for dir_path in dirs_to_remove:
|
||||
try:
|
||||
if os.path.exists(dir_path):
|
||||
shutil.rmtree(dir_path)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
|
||||
tmp_path = os.path.join(self.portproton_location, "data", "tmp")
|
||||
if os.path.exists(tmp_path):
|
||||
import glob
|
||||
bin_files = glob.glob(os.path.join(tmp_path, "*.bin"))
|
||||
foz_files = glob.glob(os.path.join(tmp_path, "*.foz"))
|
||||
for file_path in bin_files + foz_files:
|
||||
try:
|
||||
os.remove(file_path)
|
||||
except Exception as e:
|
||||
success = False
|
||||
errors.append(str(e))
|
||||
|
||||
if success:
|
||||
QMessageBox.information(self, _("Success"), _("Prefix '{}' cleared successfully.").format(selected_prefix))
|
||||
def _on_clear_prefix_finished(self, exitCode):
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.update_status_message.emit("", 0)
|
||||
if exitCode == 0:
|
||||
QMessageBox.information(self, _("Success"), _("Prefix cleared successfully."))
|
||||
else:
|
||||
error_msg = _("Prefix '{}' cleared with errors:\n{}").format(selected_prefix, "\n".join(errors[:5]))
|
||||
QMessageBox.warning(self, _("Warning"), error_msg)
|
||||
QMessageBox.warning(self, _("Error"), _("Prefix clear failed with exit code {}.").format(exitCode))
|
||||
|
||||
def _on_clear_prefix_error(self, error):
|
||||
self.wine_progress_bar.setVisible(False)
|
||||
self.update_status_message.emit("", 0)
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to run clear prefix command: {}").format(error))
|
||||
|
||||
def create_prefix_backup(self):
|
||||
selected_prefix = self.prefixCombo.currentText()
|
||||
@@ -1552,14 +1611,12 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _perform_backup(self, backup_dir, prefix_name):
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
if not self.portproton_location:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
if not self.portproton_location or not self.start_sh:
|
||||
return
|
||||
start_sh = self.start_sh
|
||||
self.backup_process = QProcess(self)
|
||||
self.backup_process.finished.connect(lambda exitCode, exitStatus: self._on_backup_finished(exitCode))
|
||||
cmd = [start_sh, "--backup-prefix", prefix_name, backup_dir]
|
||||
cmd = start_sh + ["--backup-prefix", prefix_name, backup_dir]
|
||||
self.backup_process.start(cmd[0], cmd[1:])
|
||||
if not self.backup_process.waitForStarted():
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start backup process."))
|
||||
@@ -1572,14 +1629,12 @@ class MainWindow(QMainWindow):
|
||||
def _perform_restore(self, file_path):
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
return
|
||||
if not self.portproton_location:
|
||||
return
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
if not os.path.exists(start_sh):
|
||||
if not self.portproton_location or not self.start_sh:
|
||||
return
|
||||
start_sh = self.start_sh
|
||||
self.restore_process = QProcess(self)
|
||||
self.restore_process.finished.connect(lambda exitCode, exitStatus: self._on_restore_finished(exitCode))
|
||||
cmd = [start_sh, "--restore-prefix", file_path]
|
||||
cmd = start_sh + ["--restore-prefix", file_path]
|
||||
self.restore_process.start(cmd[0], cmd[1:])
|
||||
if not self.restore_process.waitForStarted():
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to start restore process."))
|
||||
@@ -1765,7 +1820,22 @@ class MainWindow(QMainWindow):
|
||||
self.gamesDisplayCombo.setCurrentIndex(idx)
|
||||
formLayout.addRow(self.gamesDisplayTitle, self.gamesDisplayCombo)
|
||||
|
||||
# 4. Proxy settings
|
||||
# 4 Gamepad Type
|
||||
self.gamepadTypeCombo = QComboBox()
|
||||
self.gamepadTypeCombo.addItems(["Xbox", "PlayStation"])
|
||||
self.gamepadTypeCombo.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.gamepadTypeCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||
self.gamepadTypeTitle = QLabel(_("Gamepad Type:"))
|
||||
self.gamepadTypeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.gamepadTypeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
current_type_str = read_gamepad_type()
|
||||
if current_type_str == "playstation":
|
||||
self.gamepadTypeCombo.setCurrentText("PlayStation")
|
||||
else:
|
||||
self.gamepadTypeCombo.setCurrentText("Xbox")
|
||||
formLayout.addRow(self.gamepadTypeTitle, self.gamepadTypeCombo)
|
||||
|
||||
# 5. Proxy settings
|
||||
self.proxyUrlEdit = CustomLineEdit(self, theme=self.theme)
|
||||
self.proxyUrlEdit.setPlaceholderText(_("Proxy URL"))
|
||||
self.proxyUrlEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
|
||||
@@ -1797,7 +1867,7 @@ class MainWindow(QMainWindow):
|
||||
self.proxyPasswordTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
formLayout.addRow(self.proxyPasswordTitle, self.proxyPasswordEdit)
|
||||
|
||||
# 5. Fullscreen setting for application
|
||||
# 6. Fullscreen setting for application
|
||||
self.fullscreenCheckBox = QCheckBox(_("Launch Application in Fullscreen"))
|
||||
self.fullscreenCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
self.fullscreenCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
@@ -1808,7 +1878,19 @@ class MainWindow(QMainWindow):
|
||||
self.fullscreenCheckBox.setChecked(current_fullscreen)
|
||||
formLayout.addRow(self.fullscreenTitle, self.fullscreenCheckBox)
|
||||
|
||||
# 6. Automatic fullscreen on gamepad connection
|
||||
# 7. Minimize to tray setting
|
||||
self.minimizeToTrayCheckBox = QCheckBox(_("Minimize to tray on close"))
|
||||
self.minimizeToTrayCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
self.minimizeToTrayCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.minimizeToTrayTitle = QLabel(_("Application Close Mode:"))
|
||||
self.minimizeToTrayTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
||||
self.minimizeToTrayTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
current_minimize_to_tray = read_minimize_to_tray()
|
||||
self.minimizeToTrayCheckBox.setChecked(current_minimize_to_tray)
|
||||
self.minimizeToTrayCheckBox.toggled.connect(lambda checked: save_minimize_to_tray(checked))
|
||||
formLayout.addRow(self.minimizeToTrayTitle, self.minimizeToTrayCheckBox)
|
||||
|
||||
# 8. Automatic fullscreen on gamepad connection
|
||||
self.autoFullscreenGamepadCheckBox = QCheckBox(_("Auto Fullscreen on Gamepad connected"))
|
||||
self.autoFullscreenGamepadCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
self.autoFullscreenGamepadCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
@@ -1820,7 +1902,7 @@ class MainWindow(QMainWindow):
|
||||
self.autoFullscreenGamepadCheckBox.setChecked(current_auto_fullscreen)
|
||||
formLayout.addRow(self.autoFullscreenGamepadTitle, self.autoFullscreenGamepadCheckBox)
|
||||
|
||||
# 7. Gamepad haptic feedback config
|
||||
# 9. Gamepad haptic feedback config
|
||||
self.gamepadRumbleCheckBox = QCheckBox(_("Gamepad haptic feedback"))
|
||||
self.gamepadRumbleCheckBox.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
self.gamepadRumbleCheckBox.setStyleSheet(self.theme.SETTINGS_CHECKBOX_STYLE)
|
||||
@@ -1831,7 +1913,7 @@ class MainWindow(QMainWindow):
|
||||
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
||||
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
||||
|
||||
# # 8. Legendary Authentication
|
||||
# # 9. Legendary Authentication
|
||||
# self.legendaryAuthButton = AutoSizeButton(
|
||||
# _("Open Legendary Login"),
|
||||
# icon=self.theme_manager.get_icon("login")self.theme_manager.get_icon("login")
|
||||
@@ -2011,6 +2093,19 @@ class MainWindow(QMainWindow):
|
||||
rumble_enabled = self.gamepadRumbleCheckBox.isChecked()
|
||||
save_rumble_config(rumble_enabled)
|
||||
|
||||
gamepad_type_text = self.gamepadTypeCombo.currentText()
|
||||
gpad_type = "playstation" if gamepad_type_text == "PlayStation" else "xbox"
|
||||
save_gamepad_type(gpad_type)
|
||||
|
||||
if hasattr(self, 'input_manager'):
|
||||
if gpad_type == "playstation":
|
||||
self.input_manager.gamepad_type = GamepadType.PLAYSTATION
|
||||
elif gpad_type == "xbox":
|
||||
self.input_manager.gamepad_type = GamepadType.XBOX
|
||||
else:
|
||||
self.input_manager.gamepad_type = GamepadType.UNKNOWN
|
||||
self.updateControlHints()
|
||||
|
||||
for card in self.game_library_manager.game_card_cache.values():
|
||||
card.update_badge_visibility(filter_key)
|
||||
|
||||
@@ -2209,6 +2304,14 @@ class MainWindow(QMainWindow):
|
||||
def darkenColor(self, color, factor=200):
|
||||
return color.darker(factor)
|
||||
|
||||
def open_exe_settings(self, exe_path):
|
||||
"""Open the ExeSettingsDialog for the given executable."""
|
||||
if not os.path.exists(exe_path):
|
||||
QMessageBox.warning(self, _("Error"), _("Executable not found: {0}").format(exe_path))
|
||||
return
|
||||
dialog = ExeSettingsDialog(self, self.theme, exe_path)
|
||||
dialog.exec()
|
||||
|
||||
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
|
||||
detailPage = QWidget()
|
||||
self._animations = {}
|
||||
@@ -2511,8 +2614,6 @@ class MainWindow(QMainWindow):
|
||||
|
||||
clear_layout(hltbLayout)
|
||||
|
||||
|
||||
|
||||
has_data = False
|
||||
|
||||
if main_story_time is not None:
|
||||
@@ -2586,6 +2687,7 @@ class MainWindow(QMainWindow):
|
||||
file_to_check = entry_exec_split[0]
|
||||
current_exe = os.path.basename(file_to_check) if file_to_check else None
|
||||
|
||||
buttons_layout = QHBoxLayout()
|
||||
if self.target_exe is not None and current_exe == self.target_exe:
|
||||
playButton = AutoSizeButton(_("Stop"), icon=self.theme_manager.get_icon("stop"))
|
||||
else:
|
||||
@@ -2594,7 +2696,17 @@ class MainWindow(QMainWindow):
|
||||
playButton.setFixedSize(120, 40)
|
||||
playButton.setStyleSheet(self.theme.PLAY_BUTTON_STYLE)
|
||||
playButton.clicked.connect(lambda: self.toggleGame(exec_line, playButton))
|
||||
detailsLayout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
buttons_layout.addWidget(playButton, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
# Settings button
|
||||
settings_icon = self.theme_manager.get_icon("settings")
|
||||
settings_button = AutoSizeButton(_("Settings"), icon=settings_icon)
|
||||
settings_button.setFixedSize(120, 40)
|
||||
settings_button.setStyleSheet(self.theme.PLAY_BUTTON_STYLE)
|
||||
settings_button.clicked.connect(lambda: self.open_exe_settings(file_to_check))
|
||||
buttons_layout.addWidget(settings_button, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
buttons_layout.addStretch()
|
||||
detailsLayout.addLayout(buttons_layout)
|
||||
|
||||
contentFrameLayout.addWidget(detailsWidget)
|
||||
mainLayout.addStretch()
|
||||
@@ -2818,10 +2930,7 @@ class MainWindow(QMainWindow):
|
||||
env_vars = os.environ.copy()
|
||||
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
|
||||
|
||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
||||
if self.portproton_location is not None and ".var" not in self.portproton_location:
|
||||
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
|
||||
wrapper = start_sh
|
||||
wrapper = self.start_sh or ""
|
||||
|
||||
cmd = [wrapper, game_exe]
|
||||
|
||||
@@ -2915,13 +3024,6 @@ class MainWindow(QMainWindow):
|
||||
exe_name = os.path.splitext(current_exe)[0]
|
||||
env_vars = os.environ.copy()
|
||||
|
||||
if entry_exec_split[0] == "env" and len(entry_exec_split) > 1 and 'data/scripts/start.sh' in entry_exec_split[1]:
|
||||
env_vars['START_FROM_STEAM'] = '1'
|
||||
env_vars['PROCESS_LOG'] = '1'
|
||||
elif entry_exec_split[0] == "flatpak":
|
||||
env_vars['START_FROM_STEAM'] = '1'
|
||||
env_vars['PROCESS_LOG'] = '1'
|
||||
|
||||
# Запускаем игру
|
||||
try:
|
||||
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
||||
@@ -2943,57 +3045,77 @@ class MainWindow(QMainWindow):
|
||||
logger.error(f"Failed to launch game {exe_name}: {e}")
|
||||
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
|
||||
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Обработчик закрытия окна: сворачивает приложение в трей, если не требуется принудительный выход."""
|
||||
if hasattr(self, 'is_exiting') and self.is_exiting:
|
||||
# Принудительное закрытие: завершаем процессы и приложение
|
||||
for proc in self.game_processes:
|
||||
try:
|
||||
parent = psutil.Process(proc.pid)
|
||||
children = parent.children(recursive=True)
|
||||
for child in children:
|
||||
try:
|
||||
logger.debug(f"Terminating child process {child.pid}")
|
||||
child.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
logger.debug(f"Child process {child.pid} already terminated")
|
||||
psutil.wait_procs(children, timeout=5)
|
||||
for child in children:
|
||||
if child.is_running():
|
||||
logger.debug(f"Killing child process {child.pid}")
|
||||
child.kill()
|
||||
logger.debug(f"Terminating process group {proc.pid}")
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||
except (psutil.NoSuchProcess, ProcessLookupError) as e:
|
||||
logger.debug(f"Process {proc.pid} already terminated: {e}")
|
||||
"""Обработчик закрытия окна: проверяет настройку minimize_to_tray.
|
||||
Если True — сворачиваем в трей (по умолчанию). Иначе — полностью закрываем.
|
||||
"""
|
||||
minimize_to_tray = read_minimize_to_tray()
|
||||
|
||||
self.game_processes = [] # Очищаем список процессов
|
||||
|
||||
# Очищаем таймеры
|
||||
if hasattr(self, 'games_load_timer') and self.games_load_timer is not None and self.games_load_timer.isActive():
|
||||
self.games_load_timer.stop()
|
||||
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer is not None and self.settingsDebounceTimer.isActive():
|
||||
self.settingsDebounceTimer.stop()
|
||||
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer is not None and self.searchDebounceTimer.isActive():
|
||||
self.searchDebounceTimer.stop()
|
||||
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None and self.checkProcessTimer.isActive():
|
||||
self.checkProcessTimer.stop()
|
||||
self.checkProcessTimer.deleteLater()
|
||||
self.checkProcessTimer = None
|
||||
if hasattr(self, 'wine_monitor_timer') and self.wine_monitor_timer is not None:
|
||||
self.wine_monitor_timer.stop()
|
||||
self.wine_monitor_timer.deleteLater()
|
||||
self.wine_monitor_timer = None
|
||||
|
||||
# Сохраняем настройки окна
|
||||
if not read_fullscreen_config():
|
||||
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
|
||||
save_window_geometry(self.width(), self.height())
|
||||
save_card_size(self.card_width)
|
||||
|
||||
event.accept()
|
||||
else:
|
||||
# Сворачиваем в трей вместо закрытия
|
||||
self.hide()
|
||||
if minimize_to_tray:
|
||||
# Просто сворачиваем в трей
|
||||
event.ignore()
|
||||
self.hide()
|
||||
return
|
||||
|
||||
# Полное закрытие приложения
|
||||
self.is_exiting = True
|
||||
event.accept()
|
||||
|
||||
# Скрываем и удаляем иконку трея
|
||||
if hasattr(self, "tray_manager") and self.tray_manager.tray_icon:
|
||||
self.tray_manager.tray_icon.hide()
|
||||
self.tray_manager.tray_icon.deleteLater()
|
||||
|
||||
# Сохраняем размеры карточек
|
||||
save_card_size(self.card_width)
|
||||
save_auto_card_size(self.auto_card_width)
|
||||
|
||||
# Сохраняем размеры окна (если не в полноэкранном режиме)
|
||||
if not read_fullscreen_config():
|
||||
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
|
||||
save_window_geometry(self.width(), self.height())
|
||||
|
||||
# Завершаем все игровые процессы
|
||||
for proc in getattr(self, "game_processes", []):
|
||||
try:
|
||||
parent = psutil.Process(proc.pid)
|
||||
children = parent.children(recursive=True)
|
||||
for child in children:
|
||||
try:
|
||||
logger.debug(f"Terminating child process {child.pid}")
|
||||
child.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
logger.debug(f"Child process {child.pid} already terminated")
|
||||
|
||||
psutil.wait_procs(children, timeout=5)
|
||||
for child in children:
|
||||
if child.is_running():
|
||||
logger.debug(f"Killing child process {child.pid}")
|
||||
child.kill()
|
||||
|
||||
logger.debug(f"Terminating process group {proc.pid}")
|
||||
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
||||
|
||||
except (psutil.NoSuchProcess, ProcessLookupError) as e:
|
||||
logger.debug(f"Process {getattr(proc, 'pid', '?')} already terminated: {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to terminate process {getattr(proc, 'pid', '?')}: {e}")
|
||||
|
||||
self.game_processes = []
|
||||
|
||||
# Универсальная остановка и удаление таймеров
|
||||
timers = [
|
||||
"games_load_timer",
|
||||
"settingsDebounceTimer",
|
||||
"searchDebounceTimer",
|
||||
"checkProcessTimer",
|
||||
"wine_monitor_timer",
|
||||
]
|
||||
|
||||
for tname in timers:
|
||||
timer = getattr(self, tname, None)
|
||||
if timer and timer.isActive():
|
||||
timer.stop()
|
||||
if timer:
|
||||
timer.deleteLater()
|
||||
setattr(self, tname, None)
|
||||
|
||||
@@ -6,13 +6,16 @@ import urllib.parse
|
||||
import time
|
||||
import glob
|
||||
import re
|
||||
import hashlib
|
||||
from collections.abc import Callable
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.logger import get_logger
|
||||
from portprotonqt.config_utils import get_portproton_location
|
||||
|
||||
logger = get_logger(__name__)
|
||||
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||
AUTOINSTALL_CACHE_DURATION = 3600 # 1 hour for autoinstall cache
|
||||
|
||||
def normalize_name(s):
|
||||
"""
|
||||
@@ -59,6 +62,7 @@ class PortProtonAPI:
|
||||
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._autoinstall_cache = None # New: In-memory cache
|
||||
|
||||
def _get_game_dir(self, exe_name: str) -> str:
|
||||
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
||||
@@ -231,67 +235,139 @@ class PortProtonAPI:
|
||||
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 ""
|
||||
def _compute_scripts_signature(self, auto_dir: str) -> str:
|
||||
"""Compute a hash-based signature of the autoinstall scripts to detect changes."""
|
||||
if not os.path.exists(auto_dir):
|
||||
callback(games)
|
||||
return
|
||||
|
||||
return ""
|
||||
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
|
||||
if not scripts:
|
||||
callback(games)
|
||||
return
|
||||
# Simple hash: concatenate sorted filenames and hash
|
||||
filenames_str = "".join(sorted([os.path.basename(s) for s in scripts]))
|
||||
return hashlib.md5(filenames_str.encode()).hexdigest()
|
||||
|
||||
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)
|
||||
def _load_autoinstall_cache(self):
|
||||
"""Load cached autoinstall games if fresh and scripts unchanged."""
|
||||
if self._autoinstall_cache is not None:
|
||||
return self._autoinstall_cache
|
||||
cache_dir = get_cache_dir()
|
||||
cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json")
|
||||
if os.path.exists(cache_file):
|
||||
try:
|
||||
mod_time = os.path.getmtime(cache_file)
|
||||
if time.time() - mod_time < AUTOINSTALL_CACHE_DURATION:
|
||||
with open(cache_file, "rb") as f:
|
||||
data = orjson.loads(f.read())
|
||||
# Check signature
|
||||
cached_signature = data.get("scripts_signature", "")
|
||||
current_signature = self._compute_scripts_signature(
|
||||
os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
|
||||
)
|
||||
if cached_signature != current_signature:
|
||||
logger.info("Scripts signature mismatch; invalidating cache")
|
||||
return None
|
||||
self._autoinstall_cache = data["games"]
|
||||
logger.info(f"Loaded {len(self._autoinstall_cache)} cached autoinstall games")
|
||||
return self._autoinstall_cache
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load autoinstall cache: {e}")
|
||||
return None
|
||||
|
||||
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]
|
||||
def _save_autoinstall_cache(self, games):
|
||||
"""Save parsed autoinstall games to cache with scripts signature."""
|
||||
try:
|
||||
cache_dir = get_cache_dir()
|
||||
cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json")
|
||||
auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
|
||||
scripts_signature = self._compute_scripts_signature(auto_dir)
|
||||
data = {"games": games, "scripts_signature": scripts_signature, "timestamp": time.time()}
|
||||
with open(cache_file, "wb") as f:
|
||||
f.write(orjson.dumps(data))
|
||||
logger.debug(f"Saved {len(games)} autoinstall games to cache with signature {scripts_signature}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save autoinstall cache: {e}")
|
||||
|
||||
if not (display_name and exe_name):
|
||||
continue
|
||||
def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None:
|
||||
"""Start loading auto-install games in a background thread. Returns the thread for management."""
|
||||
# Check cache first (sync, fast)
|
||||
cached_games = self._load_autoinstall_cache()
|
||||
if cached_games is not None:
|
||||
# Emit via callback immediately if cached
|
||||
QThread.msleep(0) # Yield to Qt event loop
|
||||
callback(cached_games)
|
||||
return None # No thread needed
|
||||
|
||||
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)
|
||||
# No cache: Start background thread
|
||||
class AutoinstallWorker(QThread):
|
||||
finished = Signal(list)
|
||||
api: "PortProtonAPI"
|
||||
portproton_location: str | None
|
||||
|
||||
# Поиск обложки
|
||||
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
|
||||
def run(self):
|
||||
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):
|
||||
self.finished.emit(games)
|
||||
return
|
||||
|
||||
if not cover_path:
|
||||
logger.debug(f"No local cover found for autoinstall {exe_name}")
|
||||
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
|
||||
if not scripts:
|
||||
self.finished.emit(games)
|
||||
return
|
||||
|
||||
# Формируем кортеж игры (добавлен 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)
|
||||
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)
|
||||
|
||||
callback(games)
|
||||
for script_path in scripts:
|
||||
display_name, exe_name = self.api.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]
|
||||
user_game_folder = os.path.join(base_autoinstall_dir, exe_name)
|
||||
os.makedirs(user_game_folder, exist_ok=True)
|
||||
|
||||
# Find cover
|
||||
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}")
|
||||
|
||||
game_tuple = (
|
||||
display_name, "", cover_path, "", f"autoinstall:{script_name}",
|
||||
"", "Never", "0h 0m", "", "", 0, 0, "autoinstall", exe_name
|
||||
)
|
||||
games.append(game_tuple)
|
||||
|
||||
self.api._save_autoinstall_cache(games)
|
||||
self.api._autoinstall_cache = games
|
||||
self.finished.emit(games)
|
||||
|
||||
worker = AutoinstallWorker()
|
||||
worker.api = self
|
||||
worker.portproton_location = self.portproton_location
|
||||
worker.finished.connect(lambda games: callback(games))
|
||||
worker.start()
|
||||
logger.info("Started background load of autoinstall games")
|
||||
return worker
|
||||
|
||||
def _load_topics_data(self):
|
||||
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
||||
|
||||
241
portprotonqt/settings_manager.py
Normal file
@@ -0,0 +1,241 @@
|
||||
def get_toggle_settings():
|
||||
"""Get predefined toggle settings with descriptions."""
|
||||
from portprotonqt.localization import _
|
||||
|
||||
return {
|
||||
'PW_MANGOHUD': _("Using FPS and system load monitoring (Turns on and off by the key combination - right Shift + F12)"),
|
||||
'PW_MANGOHUD_USER_CONF': _("Forced use of MANGOHUD system settings (GOverlay, etc.)"),
|
||||
'PW_VKBASALT': _("Enable vkBasalt by default to improve graphics in games running on Vulkan. (The HOME hotkey disables vkbasalt)"),
|
||||
'PW_VKBASALT_USER_CONF': _("Forced use of VKBASALT system settings (GOverlay, etc.)"),
|
||||
'PW_DGVOODOO2': _("Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) on all 3D API."),
|
||||
'PW_GAMESCOPE': _("Super + F : Toggle fullscreen\nSuper + N : Toggle nearest neighbour filtering\nSuper + U : Toggle FSR upscaling\nSuper + Y : Toggle NIS upscaling\nSuper + I : Increase FSR sharpness by 1\nSuper + O : Decrease FSR sharpness by 1\nSuper + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\nSuper + G : Toggle keyboard grab\nSuper + C : Update clipboard"),
|
||||
'PW_USE_ESYNC': _("Enable in-process synchronization primitives based on eventfd."),
|
||||
'PW_USE_FSYNC': _("Enable futex-based in-process synchronization primitives."),
|
||||
'PW_USE_NTSYNC': _("Enable in-process synchronization via the Linux ntsync driver."),
|
||||
'PW_USE_RAY_TRACING': _("Enable vkd3d support - Ray Tracing"),
|
||||
'PW_USE_NVAPI_AND_DLSS': _("Enable DLSS on supported NVIDIA graphics cards"),
|
||||
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
|
||||
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
|
||||
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
|
||||
'PW_HIDE_NVIDIA_GPU': _("Disguise all NVIDIA GPU features"),
|
||||
'PW_VIRTUAL_DESKTOP': _("Run the application in WINE virtual desktop"),
|
||||
'PW_USE_TERMINAL': _("Run the application in a terminal"),
|
||||
'PW_GUI_DISABLED_CS': _("Disable startup mode and WINE version selector window"),
|
||||
'PW_USE_GAMEMODE': _("Use system GameMode for performance optimization"),
|
||||
'PW_USE_D3D_EXTRAS': _("Enable forced use of third-party DirectX libraries"),
|
||||
'PW_FIX_VIDEO_IN_GAME': _("Fix pink-tinted video playback in some games"),
|
||||
'PW_REDUCE_PULSE_LATENCY': _("Reduce PulseAudio latency to fix intermittent sound"),
|
||||
'PW_USE_US_LAYOUT': _("Force US keyboard layout"),
|
||||
'PW_USE_GSTREAMER': _("Use GStreamer for in-game clips (WMF support)"),
|
||||
'PW_USE_SHADER_CACHE': _("Use WINE shader caching"),
|
||||
'PW_USE_WINE_DXGI': _("Force use of built-in DXGI library"),
|
||||
'PW_USE_EAC_AND_BE': _("Enable Easy Anti-Cheat and BattlEye runtimes"),
|
||||
'PW_USE_SYSTEM_VK_LAYERS': _("Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"),
|
||||
'PW_USE_OBS_VKCAPTURE': _("Enable OBS Studio capture via obs-vkcapture"),
|
||||
'PW_DISABLE_COMPOSITING': _("Disable desktop compositing for performance"),
|
||||
'PW_USE_RUNTIME': _("Use container launch mode (recommended default)"),
|
||||
'PW_DINPUT_PROTOCOL': _("Force DirectInput protocol instead of XInput"),
|
||||
'PW_USE_NATIVE_WAYLAND': _("Enable experimental native Wayland support"),
|
||||
'PW_USE_DXVK_HDR': _("Enable HDR settings under native Wayland"),
|
||||
'PW_USE_GALLIUM_ZINK': _("Use Gallium Zink (OpenGL via Vulkan)"),
|
||||
'PW_USE_GALLIUM_NINE': _("Use Gallium Nine (native DirectX 9 for Mesa)"),
|
||||
'PW_USE_WINED3D_VULKAN': _("Use WineD3D Vulkan backend (Damavand)"),
|
||||
'PW_USE_SUPPLIED_DXVK_VKD3D': _("Use bundled dxvk/vkd3d from Wine/Proton"),
|
||||
'PW_USE_SAREK_ASYNC': _("Use async dxvk-sarek (experimental)")
|
||||
}
|
||||
|
||||
|
||||
def get_advanced_settings(disabled_text, logical_core_options, locale_options,
|
||||
amd_vulkan_drivers, is_amd, numa_nodes, dist_options=None, prefix_options=None):
|
||||
"""Get advanced settings configuration."""
|
||||
from portprotonqt.localization import _
|
||||
|
||||
advanced_settings = []
|
||||
if dist_options is None:
|
||||
dist_options = []
|
||||
if prefix_options is None:
|
||||
prefix_options = []
|
||||
|
||||
# 1. Wine Version
|
||||
advanced_settings.append({
|
||||
'key': 'PW_WINE_USE',
|
||||
'name': _("Wine Version"),
|
||||
'description': _("Select the Wine or Proton version to use for this executable."),
|
||||
'type': 'combo',
|
||||
'options': dist_options,
|
||||
'default': ''
|
||||
})
|
||||
|
||||
# 2. Prefix Name
|
||||
advanced_settings.append({
|
||||
'key': 'PW_PREFIX_NAME',
|
||||
'name': _("Prefix Name"),
|
||||
'description': _("Select the Wine prefix to use."),
|
||||
'type': 'combo',
|
||||
'options': prefix_options,
|
||||
'default': 'DEFAULT'
|
||||
})
|
||||
|
||||
# 3. Vulkan Backend
|
||||
vulkan_options = [
|
||||
_("Latest"), # → 6
|
||||
_("Stable"), # → 2
|
||||
("Sarek"), # → 1
|
||||
("WINED3D – OpenGL") # → 0
|
||||
]
|
||||
|
||||
# Маппинг: отображаемый текст → реальное значение в ppdb
|
||||
vulkan_value_map = {
|
||||
vulkan_options[0]: "6",
|
||||
vulkan_options[1]: "2",
|
||||
vulkan_options[2]: "1",
|
||||
vulkan_options[3]: "0",
|
||||
}
|
||||
|
||||
advanced_settings.append({
|
||||
'key': 'PW_VULKAN_USE',
|
||||
'name': _("Vulkan Backend"),
|
||||
'description': _(
|
||||
"Select the rendering backend for translating DirectX → Vulkan/OpenGL:\n\n"
|
||||
"• Auto – latest DXVK + VKD3D (recommended)\n"
|
||||
" The newest versions from the developers. Give the best compatibility and performance in modern games.\n"
|
||||
" Require up-to-date drivers:\n"
|
||||
" – AMD: Mesa 25.0+ or proprietary AMDVLK 2024.Q4+\n"
|
||||
" – NVIDIA: driver 550.54.14 or newer\n"
|
||||
" – Intel: Mesa 24.2+\n\n"
|
||||
"• Stable – proven DXVK + VKD3D\n"
|
||||
" Older but extremely well-tested versions. Work on any drivers that support Vulkan 1.3+.\n"
|
||||
" The best choice if you have problems with the newest versions.\n\n"
|
||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek\n"
|
||||
" Work even on older drivers and video cards that support at least Vulkan 1.1.\n\n"
|
||||
"• WINED3D – OpenGL translation (fallback)\n"
|
||||
" No DXVK/VKD3D used. DirectX is translated to OpenGL via built-in WineD3D.\n"
|
||||
" Works on absolutely any hardware, but performance is significantly lower.\n"
|
||||
" Use only as a last resort when nothing else starts."
|
||||
),
|
||||
'type': 'combo',
|
||||
'options': vulkan_options,
|
||||
'default': '6',
|
||||
'_value_map': vulkan_value_map
|
||||
})
|
||||
|
||||
# 4. Windows version
|
||||
advanced_settings.append({
|
||||
'key': 'PW_WINDOWS_VER',
|
||||
'name': _("Windows version"),
|
||||
'description': _("Changing the WINDOWS emulation version may be required to run older games. WINDOWS versions below 10 do not support new games with DirectX 12"),
|
||||
'type': 'combo',
|
||||
'options': ['11', '10', '7', 'XP'],
|
||||
'default': '10'
|
||||
})
|
||||
|
||||
# 5. DLL Overrides
|
||||
advanced_settings.append({
|
||||
'key': 'WINEDLLOVERRIDES',
|
||||
'name': _("DLL Overrides"),
|
||||
'description': _("Forced to use/disable the library only for the given application.\n\nA brief instruction:\n* libraries are written WITHOUT the .dll file extension\n* libraries are separated by semicolons - ;\n* library=n - use the WINDOWS (third-party) library\n* library=b - use WINE (built-in) library\n* library=n,b - use WINDOWS library and then WINE\n* library=b,n - use WINE library and then WINDOWS\n* library= - disable the use of this library\n\nExample: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"),
|
||||
'type': 'text',
|
||||
'default': ''
|
||||
})
|
||||
|
||||
# 6. Launch arguments
|
||||
advanced_settings.append({
|
||||
'key': 'LAUNCH_PARAMETERS',
|
||||
'name': _("Launch Arguments"),
|
||||
'description': _("Adding an argument after the .exe file, just like you would add an argument in a shortcut on a WINDOWS system.\n\nExample: -dx11 -skipintro 1"),
|
||||
'type': 'text',
|
||||
'default': ''
|
||||
})
|
||||
|
||||
# 7. CPU cores limit
|
||||
advanced_settings.append({
|
||||
'key': 'PW_WINE_CPU_TOPOLOGY',
|
||||
'name': _("CPU Cores Limit"),
|
||||
'description': _("Limiting the number of CPU cores is useful for Unity games (It is recommended to set the value equal to 8)"),
|
||||
'type': 'combo',
|
||||
'options': [disabled_text] + logical_core_options,
|
||||
'default': disabled_text
|
||||
})
|
||||
|
||||
# 8. OpenGL version
|
||||
advanced_settings.append({
|
||||
'key': 'PW_MESA_GL_VERSION_OVERRIDE',
|
||||
'name': _("OpenGL Version"),
|
||||
'description': _("You can select the required OpenGL version, some games require a forced Compatibility Profile (COMP)."),
|
||||
'type': 'combo',
|
||||
'options': [disabled_text, '4.6COMPAT', '4.5COMPAT', '4.3COMPAT', '4.1COMPAT', '3.3COMPAT', '3.2COMPAT'],
|
||||
'default': disabled_text
|
||||
})
|
||||
|
||||
# 9. VKD3D feature level
|
||||
advanced_settings.append({
|
||||
'key': 'PW_VKD3D_FEATURE_LEVEL',
|
||||
'name': _("VKD3D Feature Level"),
|
||||
'description': _("You can set a forced feature level VKD3D for games on DirectX12"),
|
||||
'type': 'combo',
|
||||
'options': [disabled_text, '12_2', '12_1', '12_0', '11_1', '11_0'],
|
||||
'default': disabled_text
|
||||
})
|
||||
|
||||
# 10. Locale
|
||||
advanced_settings.append({
|
||||
'key': 'PW_LOCALE_SELECT',
|
||||
'name': _("Locale"),
|
||||
'description': _("Force certain locale for an app. Fixes encoding issues in legacy software"),
|
||||
'type': 'combo',
|
||||
'options': [disabled_text] + locale_options,
|
||||
'default': disabled_text
|
||||
})
|
||||
|
||||
# 11. Present mode
|
||||
advanced_settings.append({
|
||||
'key': 'PW_MESA_VK_WSI_PRESENT_MODE',
|
||||
'name': _("Window Mode"),
|
||||
'description': _("Window mode (for Vulkan and OpenGL):\nfifo - First in, first out. Limits the frame rate + no tearing. (VSync)\nimmediate - Unlimited frame rate + tearing.\nmailbox - Triple buffering. Unlimited frame rate + no tearing.\nrelaxed - Same as fifo but allows tearing when below the monitors refresh rate."),
|
||||
'type': 'combo',
|
||||
'options': [disabled_text, 'fifo', 'immediate', 'mailbox', 'relaxed'],
|
||||
'default': disabled_text
|
||||
})
|
||||
|
||||
# 12. AMD Vulkan driver
|
||||
amd_options = [disabled_text] + amd_vulkan_drivers if is_amd and amd_vulkan_drivers else [disabled_text]
|
||||
advanced_settings.append({
|
||||
'key': 'PW_AMD_VULKAN_USE',
|
||||
'name': _("AMD Vulkan Driver"),
|
||||
'description': _("Select needed AMD vulkan implementation. Choosing which implementation of vulkan will be used to run the game"),
|
||||
'type': 'combo',
|
||||
'options': amd_options,
|
||||
'default': disabled_text
|
||||
})
|
||||
|
||||
# 13. NUMA node
|
||||
numa_ids = sorted(numa_nodes.keys())
|
||||
numa_options = [disabled_text] + numa_ids if len(numa_ids) > 1 else [disabled_text]
|
||||
advanced_settings.append({
|
||||
'key': 'PW_CPU_NUMA_NODE_INDEX',
|
||||
'name': _("NUMA Node"),
|
||||
'description': _("NUMA node for CPU affinity. In multi-core systems, CPUs are split into NUMA nodes, each with its own local memory and cores. Binding a game to a single node reduces memory-access latency and limits costly core-to-core switches."),
|
||||
'type': 'combo',
|
||||
'options': numa_options,
|
||||
'default': disabled_text
|
||||
})
|
||||
|
||||
return advanced_settings
|
||||
|
||||
|
||||
# Keys that should be recognized as advanced settings
|
||||
ADVANCED_SETTING_KEYS = [
|
||||
'PW_WINE_USE',
|
||||
'PW_PREFIX_NAME',
|
||||
'PW_VULKAN_USE',
|
||||
'PW_WINDOWS_VER',
|
||||
'WINEDLLOVERRIDES',
|
||||
'LAUNCH_PARAMETERS',
|
||||
'PW_WINE_CPU_TOPOLOGY',
|
||||
'PW_MESA_GL_VERSION_OVERRIDE',
|
||||
'PW_VKD3D_FEATURE_LEVEL',
|
||||
'PW_LOCALE_SELECT',
|
||||
'PW_MESA_VK_WSI_PRESENT_MODE',
|
||||
'PW_AMD_VULKAN_USE',
|
||||
'PW_CPU_NUMA_NODE_INDEX',
|
||||
]
|
||||
@@ -13,7 +13,7 @@ from portprotonqt.logger import get_logger
|
||||
from portprotonqt.localization import get_steam_language
|
||||
from portprotonqt.downloader import Downloader
|
||||
from portprotonqt.dialogs import generate_thumbnail
|
||||
from portprotonqt.config_utils import get_portproton_location
|
||||
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
import shutil
|
||||
@@ -23,6 +23,7 @@ import requests
|
||||
import random
|
||||
import base64
|
||||
import glob
|
||||
import urllib.parse
|
||||
|
||||
downloader = Downloader()
|
||||
logger = get_logger(__name__)
|
||||
@@ -411,6 +412,39 @@ def save_app_details(app_id, data):
|
||||
with open(cache_file, "wb") as f:
|
||||
f.write(orjson.dumps(data))
|
||||
|
||||
def fetch_sgdb_cover(game_name: str) -> str:
|
||||
"""
|
||||
Fetch a cover image URL from steamgrid.usebottles.com for the given game.
|
||||
The API returns a single string (quoted URL).
|
||||
"""
|
||||
try:
|
||||
encoded = urllib.parse.quote(game_name)
|
||||
url = f"https://steamgrid.usebottles.com/api/search/{encoded}"
|
||||
resp = requests.get(url, timeout=5)
|
||||
if resp.status_code != 200:
|
||||
logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code)
|
||||
return ""
|
||||
text = resp.text.strip()
|
||||
# Убираем возможные кавычки вокруг строки
|
||||
if text.startswith('"') and text.endswith('"'):
|
||||
text = text[1:-1]
|
||||
if text:
|
||||
logger.info("Fetched SGDB cover for %s: %s", game_name, text)
|
||||
return text
|
||||
except Exception as e:
|
||||
logger.warning("Failed to fetch SGDB cover for %s: %s", game_name, e)
|
||||
return ""
|
||||
|
||||
|
||||
def check_url_exists(url: str) -> bool:
|
||||
"""Check whether a URL returns HTTP 200."""
|
||||
try:
|
||||
r = requests.head(url, timeout=3)
|
||||
return r.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
|
||||
"""
|
||||
Asynchronously fetches detailed app info from Steam API.
|
||||
@@ -629,6 +663,11 @@ def get_full_steam_game_info_async(appid: int, callback: Callable[[dict], None])
|
||||
title = decode_text(app_info.get("name", ""))
|
||||
description = decode_text(app_info.get("short_description", ""))
|
||||
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
||||
if not check_url_exists(cover):
|
||||
logger.info("Steam cover not found for %s, trying SGDB", title)
|
||||
alt_cover = fetch_sgdb_cover(title)
|
||||
if alt_cover:
|
||||
cover = alt_cover
|
||||
|
||||
def on_protondb_tier(tier: str):
|
||||
def on_anticheat_status(anticheat_status: str):
|
||||
@@ -722,12 +761,15 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
|
||||
game_name = desktop_name or exe_name.capitalize()
|
||||
|
||||
if not matching_app:
|
||||
cover = fetch_sgdb_cover(game_name) or ""
|
||||
logger.info("Using SGDB cover for non-Steam game '%s': %s", game_name, cover)
|
||||
|
||||
def on_anticheat_status(anticheat_status: str):
|
||||
callback({
|
||||
"appid": "",
|
||||
"name": decode_text(game_name),
|
||||
"description": "",
|
||||
"cover": "",
|
||||
"cover": cover,
|
||||
"controller_support": "",
|
||||
"protondb_tier": "",
|
||||
"steam_game": "false",
|
||||
@@ -758,6 +800,11 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
|
||||
title = decode_text(app_info.get("name", game_name))
|
||||
description = decode_text(app_info.get("short_description", ""))
|
||||
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
||||
if not check_url_exists(cover):
|
||||
logger.info("Steam cover not found for %s, trying SGDB", title)
|
||||
alt_cover = fetch_sgdb_cover(title)
|
||||
if alt_cover:
|
||||
cover = alt_cover
|
||||
controller_support = app_info.get("controller_support", "")
|
||||
|
||||
def on_protondb_tier(tier: str):
|
||||
@@ -957,7 +1004,8 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
|
||||
return (False, f"Executable file not found: {exe_path}")
|
||||
|
||||
portproton_dir = get_portproton_location()
|
||||
if not portproton_dir:
|
||||
start_sh = get_portproton_start_command()
|
||||
if not portproton_dir or not start_sh:
|
||||
logger.error("PortProton directory not found")
|
||||
return (False, "PortProton directory not found")
|
||||
|
||||
@@ -966,17 +1014,12 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
|
||||
|
||||
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
|
||||
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
|
||||
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
|
||||
|
||||
if not os.path.exists(start_sh_path):
|
||||
logger.error(f"start.sh not found at {start_sh_path}")
|
||||
return (False, f"start.sh not found at {start_sh_path}")
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
script_content = f"""#!/usr/bin/env bash
|
||||
export LD_PRELOAD=
|
||||
export START_FROM_STEAM=1
|
||||
"{start_sh_path}" "{exe_path}" "$@"
|
||||
"{start_sh}" "{exe_path}" "$@"
|
||||
"""
|
||||
try:
|
||||
with open(script_path, "w", encoding="utf-8") as f:
|
||||
|
||||
@@ -5,6 +5,9 @@ from portprotonqt.logger import get_logger
|
||||
from PySide6.QtGui import QIcon, QFontDatabase, QPixmap
|
||||
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
||||
|
||||
# Icon caching for performance optimization
|
||||
_icon_cache = {}
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Папка, где располагаются все дополнительные темы
|
||||
@@ -232,6 +235,14 @@ class ThemeManager:
|
||||
а если файл не найден, то из стандартной темы.
|
||||
Если as_path=True, возвращает путь к иконке вместо QIcon.
|
||||
"""
|
||||
# Create cache key
|
||||
cache_key = f"{icon_name}_{theme_name or self.current_theme_name}_{as_path}"
|
||||
|
||||
# Check if we already have this icon cached
|
||||
if cache_key in _icon_cache:
|
||||
logger.debug(f"Using cached icon for {icon_name}")
|
||||
return _icon_cache[cache_key]
|
||||
|
||||
icon_path = None
|
||||
theme_name = theme_name or self.current_theme_name
|
||||
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
|
||||
@@ -279,12 +290,20 @@ class ThemeManager:
|
||||
# Если иконка всё равно не найдена
|
||||
if not icon_path or not os.path.exists(icon_path):
|
||||
logger.error(f"Warning: icon '{icon_name}' not found")
|
||||
return QIcon() if not as_path else None
|
||||
result = QIcon() if not as_path else None
|
||||
# Cache the result even if it's None
|
||||
_icon_cache[cache_key] = result
|
||||
return result
|
||||
|
||||
if as_path:
|
||||
# Cache the path
|
||||
_icon_cache[cache_key] = icon_path
|
||||
return icon_path
|
||||
|
||||
return QIcon(icon_path)
|
||||
# Create QIcon and cache it
|
||||
icon = QIcon(icon_path)
|
||||
_icon_cache[cache_key] = icon
|
||||
return icon
|
||||
|
||||
def get_theme_image(self, image_name, theme_name=None):
|
||||
"""
|
||||
|
||||
1
portprotonqt/themes/standart/images/icons/settings.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8.0005 1c-0.38761 0-0.77522 0.0327-1.1588 0.0979-0.16351 0.0281-0.30273 0.13627-0.37209 0.28935l-0.39088 0.86264c-0.49378 0.16682-0.96454 0.39759-1.4007 0.68616 2.5e-4 0-0.90672-0.2272-0.90672-0.2272-0.161-0.0403-0.33098 3e-3 -0.45442 0.11569-0.57867 0.5285-1.0672 1.1514-1.4451 1.8432-0.0804 0.14721-0.0841 0.32549-0.01 0.47628l0.41938 0.84865c-0.17954 0.49666-0.29567 1.0147-0.346 1.5417l-0.73995 0.57946c-0.13121 0.10289-0.20407 0.26514-0.19431 0.4335 0.0453 0.78981 0.21961 1.5666 0.51558 2.2983 0.0631 0.15587 0.1978 0.27003 0.36005 0.30467l0.91397 0.19559c0.26993 0.45234 0.59572 0.86802 0.96931 1.2363l-0.0161 0.94973c-3e-3 0.16861 0.0766 0.32755 0.21183 0.42484 0.63551 0.45642 1.3414 0.80207 2.0884 1.0229 0.15926 0.0471 0.33077 0.0109 0.45872-0.0963l0.72016-0.60485c0.51582 0.0674 1.0384 0.0674 1.5544 0l0.72016 0.60485c0.12796 0.10722 0.29946 0.14343 0.45872 0.0963 0.74693-0.22083 1.4528-0.56648 2.0883-1.0229 0.13521-0.0973 0.21465-0.25623 0.21189-0.42484l-0.0161-0.94973c0.37359-0.36829 0.69939-0.78372 0.96932-1.2363l0.91396-0.19559c0.16226-0.0347 0.29695-0.1488 0.36005-0.30467 0.29597-0.73174 0.47026-1.5085 0.51558-2.2983 0.01-0.16836-0.0631-0.33061-0.1943-0.4335l-0.73996-0.57946c-0.0501-0.52671-0.16652-1.045-0.34606-1.5417l0.41944-0.84865c0.0746-0.15079 0.0709-0.32907-0.01-0.47628-0.37785-0.69176-0.86638-1.3147-1.445-1.8432-0.12345-0.11258-0.29343-0.15594-0.45443-0.11569l-0.90697 0.2272c-0.43594-0.28857-0.9067-0.51908-1.4005-0.68616l-0.39088-0.86264c-0.0694-0.15308-0.20858-0.26132-0.37209-0.28935-0.38361-0.0653-0.77121-0.0979-1.1588-0.0979zm0 4.1365a2.8152 2.8635 0 0 1 2.8152 2.8636 2.8152 2.8635 0 0 1-2.8152 2.8635 2.8152 2.8635 0 0 1-2.8152-2.8635 2.8152 2.8635 0 0 1 2.8152-2.8636z" fill="#fff" stroke-width=".25254"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 232 KiB |
BIN
portprotonqt/themes/standart/images/screenshots/Библиотека.jpg
Normal file
|
After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
BIN
portprotonqt/themes/standart/images/screenshots/Карточка.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
BIN
portprotonqt/themes/standart/images/screenshots/Темы.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
@@ -968,9 +968,8 @@ SETTINGS_CHECKBOX_STYLE = f"""
|
||||
|
||||
WINETRICKS_TAB_STYLE = f"""
|
||||
QTabWidget::pane {{
|
||||
border: 1px solid {color_d};
|
||||
background: {color_b};
|
||||
border-radius: {border_radius_a};
|
||||
border-top: 1px solid {color_c};
|
||||
background: {color_h};
|
||||
}}
|
||||
QTabBar::tab {{
|
||||
background: {color_c};
|
||||
@@ -985,15 +984,118 @@ QTabBar::tab:selected {{
|
||||
color: {color_f};
|
||||
}}
|
||||
QTabBar::tab:hover {{
|
||||
background: {color_e};
|
||||
background: {color_a};
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_TABBLE_STYLE = f"""
|
||||
QTableWidget {{
|
||||
QComboBox {{
|
||||
background: {color_c};
|
||||
border: {border_c} {color_g};
|
||||
border-radius: {border_radius_a};
|
||||
padding-left: 12px;
|
||||
color: {color_f};
|
||||
gridline-color: {color_d};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
min-width: 120px;
|
||||
combobox-popup: 0;
|
||||
}}
|
||||
QComboBox:on {{
|
||||
background: {color_b};
|
||||
border: {border_c} {color_a};
|
||||
border-bottom-style: none;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}}
|
||||
QComboBox:hover {{
|
||||
border: {border_c} {color_a};
|
||||
background: {color_a};
|
||||
}}
|
||||
/* Состояние фокуса */
|
||||
QComboBox:focus {{
|
||||
border: {border_c} {color_a};
|
||||
background-color: {color_a};
|
||||
}}
|
||||
QComboBox:disabled {{
|
||||
background: #2a2c35;
|
||||
border: {border_c} #2a2c35;
|
||||
color: #777a84;
|
||||
}}
|
||||
QComboBox::drop-down {{
|
||||
subcontrol-origin: padding;
|
||||
subcontrol-position: center right;
|
||||
border-left: {border_b} rgba(255, 255, 255, 0.05);
|
||||
padding: 12px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}}
|
||||
QComboBox::down-arrow {{
|
||||
image: url({theme_manager.get_icon("down", current_theme_name, as_path=True)});
|
||||
padding: 12px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}}
|
||||
QComboBox::down-arrow:on {{
|
||||
image: url({theme_manager.get_icon("up", current_theme_name, as_path=True)});
|
||||
padding: 12px;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}}
|
||||
/* Список при открытом комбобоксе */
|
||||
QComboBox QAbstractItemView {{
|
||||
outline: none;
|
||||
background: {color_c};
|
||||
border: {border_c} {color_a};
|
||||
border-top-style: none;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}}
|
||||
QListView {{
|
||||
background: {color_c};
|
||||
}}
|
||||
QListView::item {{
|
||||
padding: 7px 7px 7px 12px;
|
||||
margin: 3px;
|
||||
border-radius: {border_radius_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QListView::item:hover {{
|
||||
background: {color_b};
|
||||
}}
|
||||
QListView::item:selected {{
|
||||
background: {color_b};
|
||||
}}
|
||||
/* Выделение в списке при фокусе на элементе */
|
||||
QListView::item:focus {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
}}
|
||||
QLineEdit {{
|
||||
background: {color_c};
|
||||
border: {border_c} rgba(255, 255, 255, 0.01);
|
||||
border-radius: {border_radius_a};
|
||||
height: 34px;
|
||||
padding-left: 12px;
|
||||
color: {color_f};
|
||||
font-family: '{font_family}';
|
||||
font-size: {font_size_a};
|
||||
}}
|
||||
QLineEdit:hover {{
|
||||
background: {color_c};
|
||||
border: {border_c} {color_a};
|
||||
}}
|
||||
QLineEdit:focus {{
|
||||
border: {border_c} {color_a};
|
||||
background-color: {color_e};
|
||||
}}
|
||||
QTableWidget {{
|
||||
background: {color_h};
|
||||
color: {color_f};
|
||||
gridline-color: {color_h};
|
||||
alternate-background-color: {color_d};
|
||||
border: {border_a};
|
||||
border-radius: {border_radius_a};
|
||||
@@ -1009,39 +1111,68 @@ QHeaderView::section {{
|
||||
}}
|
||||
QTableWidget::item {{
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid {color_d};
|
||||
border-bottom: {border_a } {color_c};
|
||||
height: 36px;
|
||||
}}
|
||||
QTableWidget::item:selected {{
|
||||
QTableWidget::item:selected,
|
||||
QTableWidget::item:focus,
|
||||
QTableWidget::item:selected:focus {{
|
||||
background: {color_a};
|
||||
color: {color_f};
|
||||
selection-background-color: {color_a};
|
||||
}}
|
||||
QTableWidget::item:hover {{
|
||||
background: {color_e};
|
||||
background: {color_h};
|
||||
}}
|
||||
QTableWidget::indicator {{
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: {border_b} {color_a};
|
||||
border: {border_c} {color_h};
|
||||
border-radius: {border_radius_a};
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: {color_b};
|
||||
}}
|
||||
QTableWidget::indicator:unchecked {{
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
image: none;
|
||||
}}
|
||||
QTableWidget::indicator:checked {{
|
||||
background: {color_a};
|
||||
background: {color_b};
|
||||
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
|
||||
border: {border_b} {color_f};
|
||||
border: {border_c} {color_a};
|
||||
}}
|
||||
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}
|
||||
QTableWidget::indicator:focus {{
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: {border_c} {color_a};
|
||||
}}
|
||||
QScrollBar:vertical {{
|
||||
width: 10px;
|
||||
border: {border_a};
|
||||
border-radius: 5px;
|
||||
background: rgba(20, 20, 20, 0.30);
|
||||
}}
|
||||
QScrollBar::handle:vertical {{
|
||||
background: #bebebe;
|
||||
border: {border_a};
|
||||
border-radius: 5px;
|
||||
}}
|
||||
QScrollBar::add-line:vertical {{
|
||||
border: {border_a};
|
||||
background: none;
|
||||
}}
|
||||
QScrollBar::sub-line:vertical {{
|
||||
border: {border_a};
|
||||
background: none;
|
||||
}}
|
||||
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {{
|
||||
border: {border_a};
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: none;
|
||||
}}
|
||||
"""
|
||||
|
||||
WINETRICKS_LOG_STYLE = f"""
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from typing import cast
|
||||
from typing import cast, Any
|
||||
from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout,
|
||||
QSizePolicy, QWidget, QLineEdit)
|
||||
from PySide6.QtCore import Qt, Signal, QProcess
|
||||
from PySide6.QtCore import Qt, Signal, QProcess, QSize
|
||||
from PySide6.QtGui import QPixmap, QIcon
|
||||
from portprotonqt.keyboard_layouts import keyboard_layouts
|
||||
from portprotonqt.theme_manager import ThemeManager
|
||||
from portprotonqt.config_utils import read_theme_from_config
|
||||
@@ -43,6 +44,18 @@ class VirtualKeyboard(QFrame):
|
||||
self.margins = 10
|
||||
self.num_cols = 14
|
||||
|
||||
# Find input_manager and main_window
|
||||
self.input_manager: Any = None
|
||||
self.main_window: Any = None
|
||||
parent_widget: QWidget | None = self._parent
|
||||
while parent_widget:
|
||||
if hasattr(parent_widget, 'input_manager'):
|
||||
self.input_manager = cast(Any, parent_widget).input_manager
|
||||
self.main_window = cast(Any, parent_widget)
|
||||
parent_widget = cast(QWidget | None, parent_widget.parent())
|
||||
|
||||
|
||||
self.current_theme_name = read_theme_from_config()
|
||||
self.initUI()
|
||||
self.hide()
|
||||
|
||||
@@ -119,6 +132,34 @@ class VirtualKeyboard(QFrame):
|
||||
self.buttons: dict[str, QPushButton] = {}
|
||||
self.update_keyboard()
|
||||
|
||||
def set_gamepad_icon(self, button, icon_type, gtype=''):
|
||||
"""Set gamepad icon on button based on type"""
|
||||
if icon_type in ['back', 'add_game']:
|
||||
icon_name = self.main_window.get_button_icon(icon_type, gtype)
|
||||
else: # nav left/right
|
||||
if icon_type in ['left', 'right']:
|
||||
direction = icon_type
|
||||
icon_name = self.main_window.get_nav_icon(direction, gtype)
|
||||
else:
|
||||
direction = 'left' if icon_type == 'left' else 'right'
|
||||
icon_name = self.main_window.get_nav_icon(direction, gtype)
|
||||
|
||||
icon_path = theme_manager.get_theme_image(icon_name, self.current_theme_name)
|
||||
pixmap = QPixmap()
|
||||
if icon_path:
|
||||
pixmap.load(str(icon_path))
|
||||
if not pixmap.isNull():
|
||||
button.setIcon(QIcon(pixmap))
|
||||
button.setIconSize(QSize(20, 20))
|
||||
return
|
||||
else:
|
||||
# Fallback to placeholder
|
||||
placeholder = theme_manager.get_theme_image("placeholder", self.current_theme_name)
|
||||
if placeholder:
|
||||
button.setIcon(QIcon(placeholder))
|
||||
button.setIconSize(QSize(20, 20))
|
||||
return
|
||||
|
||||
def update_keyboard(self):
|
||||
coords = self._save_focused_coords()
|
||||
|
||||
@@ -151,6 +192,9 @@ class VirtualKeyboard(QFrame):
|
||||
button.setCheckable(True)
|
||||
button.setChecked(self.shift_pressed)
|
||||
button.clicked.connect(lambda checked: self.on_shift_click(checked))
|
||||
# Add gamepad icon for Shift (RB/R)
|
||||
gtype = self.input_manager.gamepad_type
|
||||
self.set_gamepad_icon(button, 'right', gtype)
|
||||
else:
|
||||
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
|
||||
|
||||
@@ -163,6 +207,9 @@ class VirtualKeyboard(QFrame):
|
||||
shift.setCheckable(True)
|
||||
shift.setChecked(self.shift_pressed)
|
||||
shift.clicked.connect(lambda checked: self.on_shift_click(checked))
|
||||
# Add gamepad icon for Shift (RB/R)
|
||||
gtype = self.input_manager.gamepad_type
|
||||
self.set_gamepad_icon(shift, 'right', gtype)
|
||||
self.keyboard_layout.addWidget(shift, 3, 11, 1, 3)
|
||||
|
||||
button = QPushButton('CAPS')
|
||||
@@ -179,6 +226,9 @@ class VirtualKeyboard(QFrame):
|
||||
backspace.setFixedSize(fixed_w, fixed_h)
|
||||
backspace.pressed.connect(self.on_backspace_pressed)
|
||||
backspace.released.connect(self.stop_backspace_repeat)
|
||||
# Add gamepad icon for Backspace (X/Triangle)
|
||||
gtype = self.input_manager.gamepad_type
|
||||
self.set_gamepad_icon(backspace, 'add_game', gtype)
|
||||
self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1)
|
||||
|
||||
enter = QPushButton('Enter')
|
||||
@@ -189,6 +239,9 @@ class VirtualKeyboard(QFrame):
|
||||
lang = QPushButton('🌐')
|
||||
lang.setFixedSize(fixed_w, fixed_h)
|
||||
lang.clicked.connect(self.on_lang_click)
|
||||
# Add gamepad icon for Lang (LB/L)
|
||||
gtype = self.input_manager.gamepad_type
|
||||
self.set_gamepad_icon(lang, 'left', gtype)
|
||||
self.keyboard_layout.addWidget(lang, 4, 0, 1, 1)
|
||||
|
||||
clear = QPushButton('Clear')
|
||||
@@ -219,6 +272,9 @@ class VirtualKeyboard(QFrame):
|
||||
hide_button = QPushButton('Hide')
|
||||
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
||||
hide_button.clicked.connect(self.hide)
|
||||
# Add gamepad icon for Hide (B/Circle)
|
||||
gtype = self.input_manager.gamepad_type
|
||||
self.set_gamepad_icon(hide_button, 'back', gtype)
|
||||
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
|
||||
|
||||
if coords:
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "portprotonqt"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
||||
readme = "README.md"
|
||||
license = { text = "GPL-3.0" }
|
||||
@@ -32,7 +32,7 @@ dependencies = [
|
||||
"icoextract>=0.2.0",
|
||||
"numpy>=2.2.4",
|
||||
"orjson>=3.11.3",
|
||||
"pillow>=11.3.0",
|
||||
"pillow>=12.0.0",
|
||||
"psutil>=7.1.0",
|
||||
"pyside6==6.9.1",
|
||||
"pyudev>=0.24.3",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"pre-commit": {
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
|
||||