Compare commits
105 Commits
b1047ba18e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d469f0a12 | |||
|
665a4df322
|
|||
|
3abaccb1e0
|
|||
|
77b025f580
|
|||
|
|
42e2025e54 | ||
|
|
8f84bbce31 | ||
|
3026e7da4e
|
|||
|
3522764c3e
|
|||
|
fd456e5330
|
|||
|
99a963d60c
|
|||
|
0b36e73bce
|
|||
|
4baa2e8684
|
|||
|
4344bbca70
|
|||
|
0a8a290d2d
|
|||
|
92652e8faa
|
|||
|
4f2afaed24
|
|||
|
1751e01e47
|
|||
|
0f74a47aed
|
|||
|
666ec654a0
|
|||
|
0c25cc9fd2
|
|||
|
5de83dbf49
|
|||
| 1821faadf6 | |||
|
17f0a6b0ea
|
|||
|
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
|
@@ -12,7 +12,7 @@ jobs:
|
|||||||
name: Build AppImage
|
name: Build AppImage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Install required dependencies
|
- name: Install required dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
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
|
redhat-rpm-config nodejs npm
|
||||||
|
|
||||||
- name: Setup rpmbuild environment
|
- name: Setup rpmbuild environment
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Copy fedora.spec
|
- name: Copy fedora.spec
|
||||||
run: |
|
run: |
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
name: Build Arch Package
|
name: Build Arch Package
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
|
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Prepare container
|
- name: Prepare container
|
||||||
run: |
|
run: |
|
||||||
pacman -Sy --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
pacman -Syuu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
||||||
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
||||||
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
||||||
yes | pacman -Scc
|
yes | pacman -Scc
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Upload Arch package
|
- name: Upload Arch package
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
# Common version, will be used for tagging the release
|
# Common version, will be used for tagging the release
|
||||||
VERSION: 0.1.7
|
VERSION: 0.1.8
|
||||||
PKGDEST: "/tmp/portprotonqt"
|
PKGDEST: "/tmp/portprotonqt"
|
||||||
PACKAGE: "portprotonqt"
|
PACKAGE: "portprotonqt"
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Prepare container
|
- name: Prepare container
|
||||||
run: |
|
run: |
|
||||||
pacman -Sy --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
pacman -Syuu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
||||||
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
||||||
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
||||||
yes | pacman -Scc
|
yes | pacman -Scc
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
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
|
redhat-rpm-config nodejs npm
|
||||||
|
|
||||||
- name: Setup rpmbuild environment
|
- name: Setup rpmbuild environment
|
||||||
@@ -188,4 +188,4 @@ jobs:
|
|||||||
tag_name: v${{ env.VERSION }}
|
tag_name: v${{ env.VERSION }}
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: release/**/*
|
files: release/**/*
|
||||||
sha256sum: true
|
sha256sum: false
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
uses: https://gitea.com/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
fedora: ${{ steps.check.outputs.fedora }}
|
fedora: ${{ steps.check.outputs.fedora }}
|
||||||
arch: ${{ steps.check.outputs.arch }}
|
arch: ${{ steps.check.outputs.arch }}
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Install required dependencies
|
- name: Install required dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Copy fedora-git.spec
|
- name: Copy fedora-git.spec
|
||||||
run: |
|
run: |
|
||||||
@@ -138,7 +138,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
|
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Upload Arch package
|
- name: Upload Arch package
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ jobs:
|
|||||||
name: Check code
|
name: Check code
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
uses: https://gitea.com/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
rev: 0.8.22
|
rev: 0.9.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: uv-lock
|
- id: uv-lock
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.0
|
rev: v0.14.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
|
|
||||||
|
|||||||
52
CHANGELOG.md
@@ -3,6 +3,58 @@
|
|||||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Добавлены основные и расширенные настройки для `.exe`-файлов
|
||||||
|
- Добавлена кнопка обновления сетки без необходимости перезапуска PortProtonQt (F5 на клавиатуре, GUIDE + Select на геймпаде)
|
||||||
|
- Добавлена эмуляция мыши по GUIDE (Xbox или PS) + Start для установки приложений или взаимодействия с инструментами Wine не адаптированные под геймпад (работает только если PortProtonQt вне фокуса)
|
||||||
|
- При сворачивании приложения в трей оно теперь корректно восстанавливается, вместо запуска нового экземпляра
|
||||||
|
- Добавлена поддержка SteamGridDB в качестве дополнительного источника обложек
|
||||||
|
- При добавлении карточки в избранное она автоматически становится первой без необходимости перезапуска
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Изменено оформление виртуальной клавиатуры для лучшего соответствия общей теме
|
||||||
|
- Ускорено чтение конфигов за счёт уменьшения количества обращений к файловой системе.
|
||||||
|
- Из стандартной темы удалены неиспользуемые шрифты
|
||||||
|
- Улучшена совместимость с Qt 6.10
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Добавлено больше проверок на None для избежания вылетов
|
||||||
|
- Улучшена работа с потоками для избежания вылетов
|
||||||
|
- Исправлен запуск PortProton из Flatpak: теперь используется `flatpak run`, а не `start.sh`
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
- @Vector_null
|
||||||
|
- @Dervart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.1.8] - 2025-10-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
|
||||||
|
- В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет
|
||||||
|
- К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада
|
||||||
|
- Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- При завершении автоустановки приложение больше не перезапускается
|
||||||
|
- Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название
|
||||||
|
- Обновлены и дополнены скриншоты темы
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Исправлено наложение карточек при смене фильтра игр
|
||||||
|
- Исправлена невозможность запуска приложения без подключёного геймпада
|
||||||
|
- Исправлена невозможность установки компонентов Winetricks через геймпад
|
||||||
|
- Ресиверы и виртуальные устройства больше не считаются за геймпад
|
||||||
|
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
- @Vector_null
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.1.7] - 2025-10-12
|
## [0.1.7] - 2025-10-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
15
TODO.md
@@ -1,6 +1,6 @@
|
|||||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||||
- [X] Добавить возможность управления с геймпада
|
- [X] Добавить возможность управления с геймпада
|
||||||
- [ ] Добавить возможность управления с тачскрина
|
- [X] Добавить возможность управления с тачскрина (Формально и так есть)
|
||||||
- [X] Добавить возможность управления с мыши и клавиатуры
|
- [X] Добавить возможность управления с мыши и клавиатуры
|
||||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||||
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
||||||
@@ -11,18 +11,18 @@
|
|||||||
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
||||||
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||||
- [X] Получать описания и названия игр из базы данных Steam
|
- [X] Получать описания и названия игр из базы данных Steam
|
||||||
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
|
- [X] Получать обложки для игр из CDN Steam
|
||||||
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
||||||
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
||||||
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
- [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||||
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
||||||
- [X] Избавиться от вызовов yad
|
- [X] Избавиться от вызовов yad
|
||||||
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
||||||
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
- [X] Добавить экранную клавиатуру в поиск
|
||||||
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
||||||
- [X] Добавить индикацию запуска приложения
|
- [X] Добавить индикацию запуска приложения
|
||||||
- [X] Достигнуть паритета функциональности с Ingame
|
- [X] Достигнуть паритета функциональности с Ingame
|
||||||
- [ ] Достигнуть паритета функциональности с PortProton
|
- [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов)
|
||||||
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
||||||
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
||||||
- [X] Добавить переводы в переопределения
|
- [X] Добавить переводы в переопределения
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
||||||
- [X] Добавить систему избранного для карточек
|
- [X] Добавить систему избранного для карточек
|
||||||
- [X] Заменить все `print` на `logging`
|
- [X] Заменить все `print` на `logging`
|
||||||
- [ ] Привести все логи к единому языку
|
- [X] Привести все логи к единому языку
|
||||||
- [X] Уменьшить количество подстановок в переводах
|
- [X] Уменьшить количество подстановок в переводах
|
||||||
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
||||||
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
||||||
@@ -62,7 +62,6 @@
|
|||||||
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
||||||
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
||||||
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||||
- [ ] Доделать светлую тему
|
- [X] Добавить подсказки к управлению с геймпада
|
||||||
- [ ] Добавить подсказки к управлению с геймпада
|
|
||||||
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
||||||
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ script:
|
|||||||
- uv pip install --no-cache-dir ../
|
- uv pip install --no-cache-dir ../
|
||||||
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
||||||
- cp -r share AppDir/usr
|
- cp -r share AppDir/usr
|
||||||
|
- cp -r lib AppDir/usr
|
||||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
- 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/{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
|
- 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:
|
AppDir:
|
||||||
path: ./AppDir
|
path: ./AppDir
|
||||||
after_bundle:
|
after_bundle:
|
||||||
@@ -36,7 +37,7 @@ AppDir:
|
|||||||
id: ru.linux_gaming.PortProtonQt
|
id: ru.linux_gaming.PortProtonQt
|
||||||
name: PortProtonQt
|
name: PortProtonQt
|
||||||
icon: ru.linux_gaming.PortProtonQt
|
icon: ru.linux_gaming.PortProtonQt
|
||||||
version: 0.1.7
|
version: 0.1.8
|
||||||
exec: usr/bin/python3
|
exec: usr/bin/python3
|
||||||
exec_args: "-m portprotonqt.app $@"
|
exec_args: "-m portprotonqt.app $@"
|
||||||
apt:
|
apt:
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
pkgname=portprotonqt
|
pkgname=portprotonqt
|
||||||
pkgver=0.1.7
|
pkgver=0.1.8
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'python-rapidfuzz' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
||||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
@@ -20,4 +20,5 @@ package() {
|
|||||||
cd "$srcdir/PortProtonQt"
|
cd "$srcdir/PortProtonQt"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
cp -r build-aux/share "$pkgdir/usr/"
|
cp -r build-aux/share "$pkgdir/usr/"
|
||||||
|
cp -r build-aux/lib "$pkgdir/usr/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ arch=('any')
|
|||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'python-rapidfuzz' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
||||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
@@ -25,4 +25,5 @@ package() {
|
|||||||
cd "$srcdir/PortProtonQt"
|
cd "$srcdir/PortProtonQt"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
cp -r build-aux/share "$pkgdir/usr/"
|
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: pyproject-rpm-macros
|
||||||
BuildRequires: python3dist(setuptools)
|
BuildRequires: python3dist(setuptools)
|
||||||
BuildRequires: git
|
BuildRequires: git
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
%description
|
%description
|
||||||
%{summary}
|
%{summary}
|
||||||
@@ -43,9 +44,10 @@ Requires: python3-tqdm
|
|||||||
Requires: python3-vdf
|
Requires: python3-vdf
|
||||||
Requires: python3-pefile
|
Requires: python3-pefile
|
||||||
Requires: python3-pillow
|
Requires: python3-pillow
|
||||||
|
Requires: python3-beautifulsoup4
|
||||||
|
Requires: python3-rapidfuzz
|
||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: python3-beautifulsoup4
|
|
||||||
Requires: cabextract
|
Requires: cabextract
|
||||||
Requires: gzip
|
Requires: gzip
|
||||||
Requires: unzip
|
Requires: unzip
|
||||||
@@ -69,11 +71,13 @@ cd %{oname}
|
|||||||
%pyproject_install
|
%pyproject_install
|
||||||
%pyproject_save_files %{pypi_name}
|
%pyproject_save_files %{pypi_name}
|
||||||
cp -r build-aux/share %{buildroot}/usr/
|
cp -r build-aux/share %{buildroot}/usr/
|
||||||
|
cp -r build-aux/lib %{buildroot}/usr/
|
||||||
|
|
||||||
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
|
%{_udevrulesdir}/60-portprotonqt.rules
|
||||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
%{bash_completions_dir}/portprotonqt
|
%{bash_completions_dir}/portprotonqt
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
%global pypi_name portprotonqt
|
%global pypi_name portprotonqt
|
||||||
%global pypi_version 0.1.7
|
%global pypi_version 0.1.8
|
||||||
%global oname PortProtonQt
|
%global oname PortProtonQt
|
||||||
%global _python_no_extras_requires 1
|
%global _python_no_extras_requires 1
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ BuildRequires: python3-build
|
|||||||
BuildRequires: pyproject-rpm-macros
|
BuildRequires: pyproject-rpm-macros
|
||||||
BuildRequires: python3dist(setuptools)
|
BuildRequires: python3dist(setuptools)
|
||||||
BuildRequires: git
|
BuildRequires: git
|
||||||
|
BuildRequires: systemd-rpm-macros
|
||||||
|
|
||||||
%description
|
%description
|
||||||
%{summary}
|
%{summary}
|
||||||
@@ -40,9 +41,10 @@ Requires: python3-tqdm
|
|||||||
Requires: python3-vdf
|
Requires: python3-vdf
|
||||||
Requires: python3-pefile
|
Requires: python3-pefile
|
||||||
Requires: python3-pillow
|
Requires: python3-pillow
|
||||||
|
Requires: python3-beautifulsoup4
|
||||||
|
Requires: python3-rapidfuzz
|
||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: python3-beautifulsoup4
|
|
||||||
Requires: cabextract
|
Requires: cabextract
|
||||||
Requires: gzip
|
Requires: gzip
|
||||||
Requires: unzip
|
Requires: unzip
|
||||||
@@ -68,11 +70,13 @@ cd %{oname}
|
|||||||
%pyproject_install
|
%pyproject_install
|
||||||
%pyproject_save_files %{pypi_name}
|
%pyproject_save_files %{pypi_name}
|
||||||
cp -r build-aux/share %{buildroot}/usr/
|
cp -r build-aux/share %{buildroot}/usr/
|
||||||
|
cp -r build-aux/lib %{buildroot}/usr/
|
||||||
|
|
||||||
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
|
%{_udevrulesdir}/60-portprotonqt.rules
|
||||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
%{bash_completions_dir}/portprotonqt
|
%{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",
|
"normalized_name": "farlight 84",
|
||||||
"status": "Supported"
|
"status": "Denied"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "riders republic",
|
"normalized_name": "riders republic",
|
||||||
@@ -1436,8 +1436,8 @@
|
|||||||
"status": "Broken"
|
"status": "Broken"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "blue protocol",
|
"normalized_name": "blue protocol star resonance",
|
||||||
"status": "Broken"
|
"status": "Running"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "dark and darker",
|
"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",
|
"normalized_title": "dirt rally 2.0 game of the year",
|
||||||
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
||||||
@@ -271,10 +375,6 @@
|
|||||||
"normalized_title": "steins;gate the distant valhalla",
|
"normalized_title": "steins;gate the distant valhalla",
|
||||||
"slug": "steins-gate-the-distant-valhalla"
|
"slug": "steins-gate-the-distant-valhalla"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"normalized_title": "hogwarts legacy",
|
|
||||||
"slug": "hogwarts-legacy"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"normalized_title": "osu!",
|
"normalized_title": "osu!",
|
||||||
"slug": "osu"
|
"slug": "osu"
|
||||||
|
|||||||
@@ -20,3 +20,33 @@ Stop Game
|
|||||||
Fullscreen
|
Fullscreen
|
||||||
Fulscreen
|
Fulscreen
|
||||||
\t
|
\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:
|
class PySide6DependencyAnalyzer:
|
||||||
def __init__(self):
|
def __init__(self, project_root: Path = None):
|
||||||
# Системные библиотеки, которые нужно всегда оставлять
|
# Системные библиотеки, которые нужно всегда оставлять
|
||||||
self.system_libs = {
|
self.system_libs = {
|
||||||
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
||||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
|
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
|
||||||
|
'libQt6Svg'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.critical_modules = {
|
||||||
|
'QtSvg',
|
||||||
}
|
}
|
||||||
|
|
||||||
self.real_dependencies = {}
|
self.real_dependencies = {}
|
||||||
self.used_modules_code = set()
|
self.used_modules_code = set()
|
||||||
self.used_modules_ldd = set()
|
self.used_modules_ldd = set()
|
||||||
self.all_required_modules = 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]:
|
def find_python_files(self, directory: Path) -> List[Path]:
|
||||||
"""Находит все Python файлы в директории"""
|
"""Находит все Python файлы в директории"""
|
||||||
@@ -44,24 +58,61 @@ class PySide6DependencyAnalyzer:
|
|||||||
"""Находит все PySide6 библиотеки (.so файлы)"""
|
"""Находит все PySide6 библиотеки (.so файлы)"""
|
||||||
libs = {}
|
libs = {}
|
||||||
|
|
||||||
# Поиск в единственной локации
|
# Ищем venv в корне проекта
|
||||||
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
|
venv_candidates = [
|
||||||
print(f"Поиск PySide6 библиотек в: {search_path}")
|
self.venv_path, # .venv
|
||||||
|
self.project_root / "venv",
|
||||||
|
self.project_root / ".virtualenv",
|
||||||
|
]
|
||||||
|
|
||||||
if search_path.exists():
|
pyside6_path = None
|
||||||
# Ищем .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 в venv
|
||||||
for subdir in search_path.iterdir():
|
for venv in venv_candidates:
|
||||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
if venv.exists():
|
||||||
for so_file in subdir.glob("*.so*"):
|
# Ищем Python версию
|
||||||
if 'Qt' in so_file.name:
|
lib_path = venv / "lib"
|
||||||
libs[subdir.name] = so_file
|
if lib_path.exists():
|
||||||
break
|
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
|
return libs
|
||||||
|
|
||||||
@@ -257,8 +308,10 @@ class PySide6DependencyAnalyzer:
|
|||||||
|
|
||||||
# Модули для удаления
|
# Модули для удаления
|
||||||
if removable_modules:
|
if removable_modules:
|
||||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
|
removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
|
||||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
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()
|
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})"
|
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
||||||
])
|
])
|
||||||
|
|
||||||
# Заменяем блок очистки в рецепте
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
|
# Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
|
||||||
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
|
# Паттерн: после " - 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
|
return updated_recipe
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
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('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
||||||
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
||||||
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
||||||
|
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
project_path = Path(args.project_path)
|
project_path = Path(args.project_path).resolve()
|
||||||
if not project_path.exists():
|
if not project_path.exists():
|
||||||
print(f"Ошибка: путь {project_path} не существует")
|
print(f"Ошибка: путь {project_path} не существует")
|
||||||
sys.exit(1)
|
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():
|
if appdir_path and not appdir_path.exists():
|
||||||
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
||||||
appdir_path = None
|
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)
|
results = analyzer.analyze_project(project_path, appdir_path)
|
||||||
|
|
||||||
# Сохраняем в анализатор для генерации команд
|
# Сохраняем в анализатор для генерации команд
|
||||||
@@ -347,13 +443,13 @@ def main():
|
|||||||
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
||||||
|
|
||||||
if args.verbose and results['real_dependencies']:
|
if args.verbose and results['real_dependencies']:
|
||||||
Devlin(f"\nРеальные зависимости (ldd):")
|
print(f"\nРеальные зависимости (ldd):")
|
||||||
for module, deps in results['real_dependencies'].items():
|
for module, deps in results['real_dependencies'].items():
|
||||||
if deps:
|
if deps:
|
||||||
print(f" {module} → {', '.join(deps)}")
|
print(f" {module} → {', '.join(deps)}")
|
||||||
|
|
||||||
# Обновляем AppImage рецепт
|
# Обновляем AppImage рецепт
|
||||||
recipe_path = Path("../build-aux/AppImageBuilder.yml")
|
recipe_path = analyzer.build_path / "AppImageBuilder.yml"
|
||||||
if recipe_path.exists():
|
if recipe_path.exists():
|
||||||
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
||||||
if updated_recipe:
|
if updated_recipe:
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ Current translation status:
|
|||||||
|
|
||||||
| Locale | Progress | Translated |
|
| Locale | Progress | Translated |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 240 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 338 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 240 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 338 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 of 240 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 338 of 338 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
| Локаль | Прогресс | Переведено |
|
| Локаль | Прогресс | Переведено |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 240 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 338 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 240 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 338 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 240 из 240 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 338 из 338 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import Any, cast
|
||||||
from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstractAnimation, QParallelAnimationGroup, QRect, Qt, QPoint
|
from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstractAnimation, QParallelAnimationGroup, QRect, Qt, QPoint
|
||||||
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
|
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
|
||||||
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
||||||
@@ -236,14 +237,31 @@ class DetailPageAnimations:
|
|||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
self.theme_manager = ThemeManager()
|
self.theme_manager = ThemeManager()
|
||||||
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
|
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
|
||||||
self.animations = main_window._animations if hasattr(main_window, '_animations') else {}
|
# Ensure the main window has an animations dict
|
||||||
|
if not hasattr(main_window, '_animations'):
|
||||||
|
main_window._animations = {}
|
||||||
|
self.animations = main_window._animations
|
||||||
|
|
||||||
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):
|
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):
|
||||||
"""Animate the detail page based on theme settings."""
|
"""Animate the detail page based on theme settings."""
|
||||||
|
# Check if the detail page is still valid before proceeding
|
||||||
|
if not detail_page or detail_page.isHidden() or detail_page.parent() is None:
|
||||||
|
logger.warning("Detail page is not valid, skipping enter animation")
|
||||||
|
load_image_and_restore_effect()
|
||||||
|
cleanup_animation()
|
||||||
|
return
|
||||||
|
|
||||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350)
|
||||||
|
|
||||||
if animation_type == "fade":
|
if animation_type == "fade":
|
||||||
|
# Check again if page is still valid before starting animation
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during fade setup, skipping animation")
|
||||||
|
load_image_and_restore_effect()
|
||||||
|
cleanup_animation()
|
||||||
|
return
|
||||||
|
|
||||||
original_effect = detail_page.graphicsEffect()
|
original_effect = detail_page.graphicsEffect()
|
||||||
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=True)
|
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=True)
|
||||||
opacity_effect.setOpacity(0.0)
|
opacity_effect.setOpacity(0.0)
|
||||||
@@ -252,17 +270,36 @@ class DetailPageAnimations:
|
|||||||
animation.setDuration(duration)
|
animation.setDuration(duration)
|
||||||
animation.setStartValue(0.0)
|
animation.setStartValue(0.0)
|
||||||
animation.setEndValue(0.999)
|
animation.setEndValue(0.999)
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
||||||
self.animations[detail_page] = animation
|
|
||||||
def restore_effect():
|
def restore_effect():
|
||||||
try:
|
try:
|
||||||
detail_page.setGraphicsEffect(original_effect) # type: ignore
|
# Check if page is still valid before restoring effect
|
||||||
|
if detail_page and not detail_page.isHidden():
|
||||||
|
detail_page.setGraphicsEffect(cast(Any, original_effect))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
logger.warning("Original effect already deleted")
|
logger.warning("Original effect already deleted")
|
||||||
animation.finished.connect(restore_effect)
|
|
||||||
animation.finished.connect(load_image_and_restore_effect)
|
# Only start animation if page is still valid
|
||||||
animation.finished.connect(opacity_effect.deleteLater)
|
if detail_page and not detail_page.isHidden():
|
||||||
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
self.animations[detail_page] = animation
|
||||||
|
animation.finished.connect(restore_effect)
|
||||||
|
animation.finished.connect(load_image_and_restore_effect)
|
||||||
|
animation.finished.connect(opacity_effect.deleteLater)
|
||||||
|
else:
|
||||||
|
logger.warning("Detail page invalid when starting fade, cleaning up")
|
||||||
|
restore_effect()
|
||||||
|
load_image_and_restore_effect()
|
||||||
|
opacity_effect.deleteLater()
|
||||||
|
cleanup_animation()
|
||||||
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
||||||
|
# Check again if page is still valid before starting animation
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during slide setup, skipping animation")
|
||||||
|
load_image_and_restore_effect()
|
||||||
|
cleanup_animation()
|
||||||
|
return
|
||||||
|
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
||||||
start_pos = {
|
start_pos = {
|
||||||
@@ -277,11 +314,25 @@ class DetailPageAnimations:
|
|||||||
animation.setStartValue(start_pos)
|
animation.setStartValue(start_pos)
|
||||||
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
|
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
|
||||||
animation.setEasingCurve(easing_curve)
|
animation.setEasingCurve(easing_curve)
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
||||||
self.animations[detail_page] = animation
|
# Only start animation if page is still valid
|
||||||
animation.finished.connect(cleanup_animation)
|
if detail_page and not detail_page.isHidden():
|
||||||
animation.finished.connect(load_image_and_restore_effect)
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
self.animations[detail_page] = animation
|
||||||
|
animation.finished.connect(cleanup_animation)
|
||||||
|
animation.finished.connect(load_image_and_restore_effect)
|
||||||
|
else:
|
||||||
|
logger.warning("Detail page invalid when starting slide, cleaning up")
|
||||||
|
load_image_and_restore_effect()
|
||||||
|
cleanup_animation()
|
||||||
elif animation_type == "bounce":
|
elif animation_type == "bounce":
|
||||||
|
# Check again if page is still valid before starting animation
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during bounce setup, skipping animation")
|
||||||
|
load_image_and_restore_effect()
|
||||||
|
cleanup_animation()
|
||||||
|
return
|
||||||
|
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration", 400)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration", 400)
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
||||||
detail_page.setWindowOpacity(0.0)
|
detail_page.setWindowOpacity(0.0)
|
||||||
@@ -300,14 +351,27 @@ class DetailPageAnimations:
|
|||||||
group_anim = QParallelAnimationGroup()
|
group_anim = QParallelAnimationGroup()
|
||||||
group_anim.addAnimation(opacity_anim)
|
group_anim.addAnimation(opacity_anim)
|
||||||
group_anim.addAnimation(geometry_anim)
|
group_anim.addAnimation(geometry_anim)
|
||||||
group_anim.finished.connect(load_image_and_restore_effect)
|
|
||||||
group_anim.finished.connect(cleanup_animation)
|
# Only start animation if page is still valid
|
||||||
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
if detail_page and not detail_page.isHidden():
|
||||||
self.animations[detail_page] = group_anim
|
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
self.animations[detail_page] = group_anim
|
||||||
|
group_anim.finished.connect(load_image_and_restore_effect)
|
||||||
|
group_anim.finished.connect(cleanup_animation)
|
||||||
|
else:
|
||||||
|
logger.warning("Detail page invalid when starting bounce, cleaning up")
|
||||||
|
load_image_and_restore_effect()
|
||||||
|
cleanup_animation()
|
||||||
|
|
||||||
def animate_detail_page_exit(self, detail_page: QWidget, cleanup_callback: Callable):
|
def animate_detail_page_exit(self, detail_page: QWidget, cleanup_callback: Callable):
|
||||||
"""Animate the detail page exit based on theme settings."""
|
"""Animate the detail page exit based on theme settings."""
|
||||||
try:
|
try:
|
||||||
|
# Check if the detail page is still valid before proceeding
|
||||||
|
if not detail_page or detail_page.isHidden() or detail_page.parent() is None:
|
||||||
|
logger.warning("Detail page is not valid, skipping exit animation")
|
||||||
|
cleanup_callback()
|
||||||
|
return
|
||||||
|
|
||||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
||||||
|
|
||||||
# Safely stop and remove any existing animation
|
# Safely stop and remove any existing animation
|
||||||
@@ -326,6 +390,13 @@ class DetailPageAnimations:
|
|||||||
# Define animation based on type
|
# Define animation based on type
|
||||||
if animation_type == "fade":
|
if animation_type == "fade":
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration_exit", 350)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration_exit", 350)
|
||||||
|
|
||||||
|
# Check if page is still valid before accessing properties
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during fade exit setup, skipping animation")
|
||||||
|
cleanup_callback()
|
||||||
|
return
|
||||||
|
|
||||||
original_effect = detail_page.graphicsEffect()
|
original_effect = detail_page.graphicsEffect()
|
||||||
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=False)
|
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=False)
|
||||||
opacity_effect.setOpacity(0.999)
|
opacity_effect.setOpacity(0.999)
|
||||||
@@ -334,18 +405,36 @@ class DetailPageAnimations:
|
|||||||
animation.setDuration(duration)
|
animation.setDuration(duration)
|
||||||
animation.setStartValue(0.999)
|
animation.setStartValue(0.999)
|
||||||
animation.setEndValue(0.0)
|
animation.setEndValue(0.0)
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
||||||
self.animations[detail_page] = animation
|
|
||||||
def restore_and_cleanup():
|
def restore_and_cleanup():
|
||||||
try:
|
try:
|
||||||
detail_page.setGraphicsEffect(original_effect) # type: ignore
|
# Check if page is still valid before restoring effect
|
||||||
|
if detail_page and not detail_page.isHidden():
|
||||||
|
detail_page.setGraphicsEffect(cast(Any, original_effect))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
logger.debug("Original effect already deleted")
|
logger.debug("Original effect already deleted")
|
||||||
cleanup_callback()
|
cleanup_callback()
|
||||||
animation.finished.connect(restore_and_cleanup)
|
|
||||||
animation.finished.connect(opacity_effect.deleteLater)
|
# Check if animation is still valid before starting
|
||||||
|
if animation and not detail_page.isHidden():
|
||||||
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
self.animations[detail_page] = animation
|
||||||
|
animation.finished.connect(restore_and_cleanup)
|
||||||
|
animation.finished.connect(opacity_effect.deleteLater)
|
||||||
|
else:
|
||||||
|
logger.warning("Animation or detail page invalid when starting fade exit, cleaning up")
|
||||||
|
restore_and_cleanup()
|
||||||
|
opacity_effect.deleteLater()
|
||||||
|
|
||||||
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500)
|
||||||
|
|
||||||
|
# Check if page is still valid before accessing properties
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during slide exit setup, skipping animation")
|
||||||
|
cleanup_callback()
|
||||||
|
return
|
||||||
|
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
||||||
end_pos = {
|
end_pos = {
|
||||||
"slide_left": QPoint(-self.main_window.width(), 0),
|
"slide_left": QPoint(-self.main_window.width(), 0),
|
||||||
@@ -353,16 +442,37 @@ class DetailPageAnimations:
|
|||||||
"slide_up": QPoint(0, self.main_window.height()),
|
"slide_up": QPoint(0, self.main_window.height()),
|
||||||
"slide_down": QPoint(0, -self.main_window.height())
|
"slide_down": QPoint(0, -self.main_window.height())
|
||||||
}[animation_type]
|
}[animation_type]
|
||||||
|
|
||||||
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
||||||
animation.setDuration(duration)
|
animation.setDuration(duration)
|
||||||
animation.setStartValue(detail_page.pos())
|
animation.setStartValue(detail_page.pos())
|
||||||
animation.setEndValue(end_pos)
|
animation.setEndValue(end_pos)
|
||||||
animation.setEasingCurve(easing_curve)
|
animation.setEasingCurve(easing_curve)
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
||||||
self.animations[detail_page] = animation
|
def slide_cleanup():
|
||||||
animation.finished.connect(cleanup_callback)
|
# Check if page is still valid before cleanup
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.debug("Detail page already cleaned up")
|
||||||
|
cleanup_callback()
|
||||||
|
|
||||||
|
# Check if animation is still valid before starting
|
||||||
|
if animation and not detail_page.isHidden():
|
||||||
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
self.animations[detail_page] = animation
|
||||||
|
animation.finished.connect(slide_cleanup)
|
||||||
|
else:
|
||||||
|
logger.warning("Animation or detail page invalid when starting slide exit, cleaning up")
|
||||||
|
slide_cleanup()
|
||||||
|
|
||||||
elif animation_type == "bounce":
|
elif animation_type == "bounce":
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration_exit", 400)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration_exit", 400)
|
||||||
|
|
||||||
|
# Check if page is still valid before accessing properties
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during bounce exit setup, skipping animation")
|
||||||
|
cleanup_callback()
|
||||||
|
return
|
||||||
|
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
||||||
opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity"))
|
opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity"))
|
||||||
opacity_anim.setDuration(duration)
|
opacity_anim.setDuration(duration)
|
||||||
@@ -375,13 +485,38 @@ class DetailPageAnimations:
|
|||||||
geometry_anim.setStartValue(detail_page.geometry())
|
geometry_anim.setStartValue(detail_page.geometry())
|
||||||
geometry_anim.setEndValue(final_rect)
|
geometry_anim.setEndValue(final_rect)
|
||||||
geometry_anim.setEasingCurve(easing_curve)
|
geometry_anim.setEasingCurve(easing_curve)
|
||||||
|
|
||||||
|
# Check if animations are still valid before creating group
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during bounce exit setup, cleaning up")
|
||||||
|
cleanup_callback()
|
||||||
|
return
|
||||||
|
|
||||||
group_anim = QParallelAnimationGroup()
|
group_anim = QParallelAnimationGroup()
|
||||||
group_anim.addAnimation(opacity_anim)
|
group_anim.addAnimation(opacity_anim)
|
||||||
group_anim.addAnimation(geometry_anim)
|
group_anim.addAnimation(geometry_anim)
|
||||||
group_anim.finished.connect(cleanup_callback)
|
|
||||||
|
# Check if group animation is still valid before connecting
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.warning("Detail page became invalid during group animation setup, cleaning up")
|
||||||
|
cleanup_callback()
|
||||||
|
return
|
||||||
|
|
||||||
|
def bounce_cleanup():
|
||||||
|
# Check if page is still valid before cleanup
|
||||||
|
if not detail_page or detail_page.isHidden():
|
||||||
|
logger.debug("Detail page already cleaned up")
|
||||||
|
cleanup_callback()
|
||||||
|
|
||||||
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
self.animations[detail_page] = group_anim
|
self.animations[detail_page] = group_anim
|
||||||
|
group_anim.finished.connect(bounce_cleanup)
|
||||||
|
except RuntimeError:
|
||||||
|
# Widget was already deleted, which is expected after deleteLater()
|
||||||
|
logger.debug("Detail page already deleted during animation setup")
|
||||||
|
cleanup_callback()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
|
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
|
||||||
self.animations.pop(detail_page, None)
|
if detail_page in self.animations:
|
||||||
|
self.animations.pop(detail_page, None)
|
||||||
cleanup_callback()
|
cleanup_callback()
|
||||||
|
|||||||
@@ -1,41 +1,45 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import subprocess
|
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.QtWidgets import QApplication
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
|
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
||||||
|
|
||||||
from portprotonqt.main_window import MainWindow
|
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.logger import get_logger, setup_logger
|
||||||
from portprotonqt.cli import parse_args
|
from portprotonqt.cli import parse_args
|
||||||
|
|
||||||
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
||||||
__app_name__ = "PortProtonQt"
|
__app_name__ = "PortProtonQt"
|
||||||
__app_version__ = "0.1.7"
|
__app_version__ = "0.1.8"
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
try:
|
try:
|
||||||
commit = subprocess.check_output(
|
commit = subprocess.check_output(
|
||||||
['git', 'rev-parse', '--short', 'HEAD'],
|
["git", "rev-parse", "--short", "HEAD"],
|
||||||
stderr=subprocess.DEVNULL
|
stderr=subprocess.DEVNULL,
|
||||||
).decode('utf-8').strip()
|
).decode("utf-8").strip()
|
||||||
return f"{__app_version__} ({commit})"
|
return f"{__app_version__} ({commit})"
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
||||||
return __app_version__
|
return __app_version__
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ['PW_CLI'] = '1'
|
os.environ["PW_CLI"] = "1"
|
||||||
os.environ['PROCESS_LOG'] = '1'
|
os.environ["PROCESS_LOG"] = "1"
|
||||||
os.environ['START_FROM_STEAM'] = '1'
|
os.environ["START_FROM_STEAM"] = "1"
|
||||||
|
|
||||||
portproton_path = get_portproton_location()
|
# Get the PortProton start command
|
||||||
|
start_sh = get_portproton_start_command()
|
||||||
|
|
||||||
if portproton_path is None:
|
if start_sh is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
script_path = os.path.join(portproton_path, 'data', 'scripts', 'start.sh')
|
|
||||||
subprocess.run([script_path, 'cli', '--initial'])
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||||
app.setDesktopFileName(__app_id__)
|
app.setDesktopFileName(__app_id__)
|
||||||
@@ -43,41 +47,131 @@ def main():
|
|||||||
app.setApplicationVersion(__app_version__)
|
app.setApplicationVersion(__app_version__)
|
||||||
|
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
# Setup logger with specified debug level
|
|
||||||
setup_logger(args.debug_level)
|
setup_logger(args.debug_level)
|
||||||
|
|
||||||
# Reinitialize logger after setup to ensure it uses the new configuration
|
|
||||||
logger = get_logger(__name__)
|
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()
|
system_locale = QLocale.system()
|
||||||
qt_translator = QTranslator()
|
qt_translator = QTranslator()
|
||||||
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
||||||
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
||||||
app.installTranslator(qt_translator)
|
app.installTranslator(qt_translator)
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language")
|
logger.warning(
|
||||||
|
f"Qt translations for {system_locale.name()} not found in {translations_path}, using English"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Main Window ---
|
||||||
version = get_version()
|
version = get_version()
|
||||||
window = MainWindow(app_name=__app_name__, version=version)
|
window = MainWindow(app_name=__app_name__, version=version)
|
||||||
|
|
||||||
if args.fullscreen:
|
# --- Handle incoming connections ---
|
||||||
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
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"):
|
||||||
|
# Ensure the window is visible and not minimized
|
||||||
|
window.setWindowState(window.windowState() & ~Qt.WindowState.WindowMinimized)
|
||||||
|
window.show()
|
||||||
|
window.raise_()
|
||||||
|
window.activateWindow()
|
||||||
|
|
||||||
|
# Ensure window is in active state for systems with strict focus policies
|
||||||
|
window.setWindowState(window.windowState() | 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)
|
save_fullscreen_config(True)
|
||||||
window.showFullScreen()
|
window.showFullScreen()
|
||||||
|
else:
|
||||||
|
logger.info("Launching in normal mode")
|
||||||
|
save_fullscreen_config(False)
|
||||||
|
window.showNormal()
|
||||||
|
|
||||||
|
# Execute the initial PortProton command after the UI is set up
|
||||||
|
def run_initial_command():
|
||||||
|
nonlocal start_sh
|
||||||
|
if start_sh:
|
||||||
|
try:
|
||||||
|
subprocess.run(start_sh + ["cli", "--initial"], timeout=10)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("Initial PortProton command timed out")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error running initial PortProton command: {e}")
|
||||||
|
else:
|
||||||
|
logger.warning("PortProton start command not available, skipping initial command")
|
||||||
|
|
||||||
|
# Run the initial command after the UI is displayed
|
||||||
|
QTimer.singleShot(100, run_initial_command)
|
||||||
|
|
||||||
|
# --- Cleanup ---
|
||||||
def cleanup_on_exit():
|
def cleanup_on_exit():
|
||||||
nonlocal window
|
try:
|
||||||
app.aboutToQuit.disconnect()
|
local_server.close()
|
||||||
if window:
|
QLocalServer.removeServer(server_name)
|
||||||
window.close()
|
if window:
|
||||||
app.quit()
|
window.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Cleanup error: {e}")
|
||||||
|
|
||||||
app.aboutToQuit.connect(cleanup_on_exit)
|
app.aboutToQuit.connect(cleanup_on_exit)
|
||||||
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
_portproton_location = None
|
_portproton_location = None
|
||||||
|
_portproton_start_sh = None
|
||||||
|
|
||||||
|
# Configuration cache for performance optimization
|
||||||
|
_config_cache = {}
|
||||||
|
_config_last_modified = {}
|
||||||
|
|
||||||
# Paths to configuration files
|
# Paths to configuration files
|
||||||
CONFIG_FILE = os.path.join(
|
CONFIG_FILE = os.path.join(
|
||||||
@@ -26,13 +32,35 @@ THEMES_DIRS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
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."""
|
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails.
|
||||||
cp = configparser.ConfigParser()
|
Uses caching to avoid repeated file reads for better performance.
|
||||||
|
"""
|
||||||
|
# Check if file exists
|
||||||
if not os.path.exists(config_file):
|
if not os.path.exists(config_file):
|
||||||
logger.debug(f"Configuration file {config_file} not found")
|
logger.debug(f"Configuration file {config_file} not found")
|
||||||
return None
|
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:
|
try:
|
||||||
cp.read(config_file, encoding="utf-8")
|
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
|
return cp
|
||||||
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
|
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
|
||||||
logger.warning(f"Invalid configuration file format: {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}")
|
logger.warning(f"Failed to read configuration file: {e}")
|
||||||
return None
|
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():
|
def read_config():
|
||||||
"""Reads the configuration file and returns a dictionary of parameters.
|
"""Reads the configuration file and returns a dictionary of parameters.
|
||||||
Example line in config (no sections):
|
Example line in config (no sections):
|
||||||
@@ -75,6 +111,8 @@ def save_theme_to_config(theme_name):
|
|||||||
cp["Appearance"]["theme"] = theme_name
|
cp["Appearance"]["theme"] = theme_name
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_time_config():
|
def read_time_config():
|
||||||
"""Reads time settings from the [Time] section of the configuration file.
|
"""Reads time settings from the [Time] section of the configuration file.
|
||||||
@@ -94,21 +132,29 @@ def save_time_config(detail_level):
|
|||||||
cp["Time"]["detail_level"] = detail_level
|
cp["Time"]["detail_level"] = detail_level
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_file_content(file_path):
|
def read_file_content(file_path):
|
||||||
"""Reads the content of a file and returns it as a string."""
|
"""Reads the content of a file and returns it as a string."""
|
||||||
with open(file_path, encoding="utf-8") as f:
|
try:
|
||||||
return f.read().strip()
|
# Add timeout protection for file operations using a simple approach
|
||||||
|
with open(file_path, encoding="utf-8") as f:
|
||||||
|
content = f.read().strip()
|
||||||
|
return content
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error reading file {file_path}: {e}")
|
||||||
|
raise # Re-raise the exception to be handled by the caller
|
||||||
|
|
||||||
def get_portproton_location():
|
def get_portproton_location():
|
||||||
"""Returns the path to the PortProton directory.
|
"""Возвращает путь к PortProton каталогу (строку) или None."""
|
||||||
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
|
|
||||||
If the path is invalid, uses the default directory.
|
|
||||||
"""
|
|
||||||
global _portproton_location
|
global _portproton_location
|
||||||
|
|
||||||
if _portproton_location is not None:
|
if _portproton_location is not None:
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
|
|
||||||
|
location = None
|
||||||
|
|
||||||
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
||||||
try:
|
try:
|
||||||
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
||||||
@@ -116,19 +162,53 @@ def get_portproton_location():
|
|||||||
_portproton_location = location
|
_portproton_location = location
|
||||||
logger.info(f"PortProton path from configuration: {location}")
|
logger.info(f"PortProton path from configuration: {location}")
|
||||||
return _portproton_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:
|
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}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Unexpected error reading PortProton configuration file: {e}")
|
||||||
|
|
||||||
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||||
if os.path.isdir(default_dir):
|
if os.path.isdir(default_flatpak_dir):
|
||||||
_portproton_location = default_dir
|
_portproton_location = default_flatpak_dir
|
||||||
logger.info(f"Using flatpak PortProton directory: {default_dir}")
|
logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
|
|
||||||
logger.warning("PortProton configuration and flatpak directory not found")
|
logger.warning("PortProton configuration and Flatpak directory not found")
|
||||||
return None
|
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,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
if "ru.linux_gaming.PortProton" in result.stdout:
|
||||||
|
logger.info("Detected Flatpak installation")
|
||||||
|
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning("Flatpak list command timed out")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error checking flatpak list: {e}")
|
||||||
|
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):
|
def parse_desktop_entry(file_path):
|
||||||
"""Reads and parses a .desktop file using configparser.
|
"""Reads and parses a .desktop file using configparser.
|
||||||
Returns None if the [Desktop Entry] section is missing.
|
Returns None if the [Desktop Entry] section is missing.
|
||||||
@@ -176,6 +256,30 @@ def save_card_size(card_width):
|
|||||||
cp["Cards"]["card_width"] = str(card_width)
|
cp["Cards"]["card_width"] = str(card_width)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# 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():
|
def read_sort_method():
|
||||||
"""Reads the sort method from the [Games] section.
|
"""Reads the sort method from the [Games] section.
|
||||||
@@ -195,6 +299,8 @@ def save_sort_method(sort_method):
|
|||||||
cp["Games"]["sort_method"] = sort_method
|
cp["Games"]["sort_method"] = sort_method
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_display_filter():
|
def read_display_filter():
|
||||||
"""Reads the display_filter parameter from the [Games] section.
|
"""Reads the display_filter parameter from the [Games] section.
|
||||||
@@ -214,6 +320,8 @@ def save_display_filter(filter_value):
|
|||||||
cp["Games"]["display_filter"] = filter_value
|
cp["Games"]["display_filter"] = filter_value
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_favorites():
|
def read_favorites():
|
||||||
"""Reads the list of favorite games from the [Favorites] section.
|
"""Reads the list of favorite games from the [Favorites] section.
|
||||||
@@ -239,6 +347,8 @@ def save_favorites(favorites):
|
|||||||
cp["Favorites"]["games"] = f'"{fav_str}"'
|
cp["Favorites"]["games"] = f'"{fav_str}"'
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_rumble_config():
|
def read_rumble_config():
|
||||||
"""Reads the gamepad rumble setting from the [Gamepad] section.
|
"""Reads the gamepad rumble setting from the [Gamepad] section.
|
||||||
@@ -258,6 +368,29 @@ def save_rumble_config(rumble_enabled):
|
|||||||
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
|
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# 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():
|
def ensure_default_proxy_config():
|
||||||
"""Ensures the [Proxy] section exists in the configuration file.
|
"""Ensures the [Proxy] section exists in the configuration file.
|
||||||
@@ -302,6 +435,8 @@ def save_proxy_config(proxy_url="", proxy_user="", proxy_password=""):
|
|||||||
cp["Proxy"]["proxy_password"] = proxy_password
|
cp["Proxy"]["proxy_password"] = proxy_password
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_fullscreen_config():
|
def read_fullscreen_config():
|
||||||
"""Reads the fullscreen mode setting from the [Display] section.
|
"""Reads the fullscreen mode setting from the [Display] section.
|
||||||
@@ -321,6 +456,8 @@ def save_fullscreen_config(fullscreen):
|
|||||||
cp["Display"]["fullscreen"] = str(fullscreen)
|
cp["Display"]["fullscreen"] = str(fullscreen)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_window_geometry() -> tuple[int, int]:
|
def read_window_geometry() -> tuple[int, int]:
|
||||||
"""Reads the window width and height from the [MainWindow] section.
|
"""Reads the window width and height from the [MainWindow] section.
|
||||||
@@ -342,6 +479,8 @@ def save_window_geometry(width: int, height: int):
|
|||||||
cp["MainWindow"]["height"] = str(height)
|
cp["MainWindow"]["height"] = str(height)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def reset_config():
|
def reset_config():
|
||||||
"""Resets the configuration file by deleting it.
|
"""Resets the configuration file by deleting it.
|
||||||
@@ -351,6 +490,8 @@ def reset_config():
|
|||||||
try:
|
try:
|
||||||
os.remove(CONFIG_FILE)
|
os.remove(CONFIG_FILE)
|
||||||
logger.info("Configuration file %s deleted", CONFIG_FILE)
|
logger.info("Configuration file %s deleted", CONFIG_FILE)
|
||||||
|
# Invalidate cache after deletion
|
||||||
|
invalidate_config_cache()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete configuration file: {e}")
|
logger.warning(f"Failed to delete configuration file: {e}")
|
||||||
|
|
||||||
@@ -365,6 +506,9 @@ def clear_cache():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete cache: {e}")
|
logger.warning(f"Failed to delete cache: {e}")
|
||||||
|
|
||||||
|
# Also clear our internal config cache
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_auto_fullscreen_gamepad():
|
def read_auto_fullscreen_gamepad():
|
||||||
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
|
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
|
||||||
Returns False if the parameter is missing.
|
Returns False if the parameter is missing.
|
||||||
@@ -383,6 +527,8 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
|
|||||||
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# Invalidate cache after saving
|
||||||
|
invalidate_config_cache()
|
||||||
|
|
||||||
def read_favorite_folders():
|
def read_favorite_folders():
|
||||||
"""Reads the list of favorite folders from the [FavoritesFolders] section.
|
"""Reads the list of favorite folders from the [FavoritesFolders] section.
|
||||||
@@ -408,3 +554,26 @@ def save_favorite_folders(folders):
|
|||||||
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
|
# 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.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
||||||
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
||||||
from portprotonqt.localization import _
|
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.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.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
|
||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
||||||
@@ -406,16 +406,7 @@ class ContextMenuManager:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
# Construct EGS launch command
|
# Construct EGS launch command
|
||||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
wrapper = get_portproton_start_command()
|
||||||
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
|
|
||||||
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
|
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
|
||||||
else:
|
else:
|
||||||
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
|
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.logger import get_logger
|
||||||
from portprotonqt.image_utils import load_pixmap_async
|
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.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 (
|
from portprotonqt.steam_api import (
|
||||||
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
|
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
|
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
|
return
|
||||||
|
|
||||||
# Determine wrapper
|
# Determine wrapper
|
||||||
wrapper = "flatpak run ru.linux_gaming.PortProton"
|
wrapper = get_portproton_start_command()
|
||||||
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
|
|
||||||
|
|
||||||
# Create launch script
|
# Create launch script
|
||||||
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
|
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
|
||||||
@@ -584,7 +577,7 @@ def get_egs_game_description_async(
|
|||||||
"https://launcher.store.epicgames.com/graphql",
|
"https://launcher.store.epicgames.com/graphql",
|
||||||
json=search_query,
|
json=search_query,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=5
|
timeout=10
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
@@ -604,7 +597,7 @@ def get_egs_game_description_async(
|
|||||||
def fetch_legacy_description(url: str) -> str:
|
def fetch_legacy_description(url: str) -> str:
|
||||||
"""Fetches description from the legacy API, handling DNS failures."""
|
"""Fetches description from the legacy API, handling DNS failures."""
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=headers, timeout=5)
|
response = requests.get(url, headers=headers, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
@@ -626,6 +619,9 @@ def get_egs_game_description_async(
|
|||||||
except requests.exceptions.ConnectionError as e:
|
except requests.exceptions.ConnectionError as e:
|
||||||
logger.error("DNS resolution failed for legacy API %s: %s", url, str(e))
|
logger.error("DNS resolution failed for legacy API %s: %s", url, str(e))
|
||||||
return ""
|
return ""
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning("Request timeout for legacy API %s", url)
|
||||||
|
return ""
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.warning("Failed to fetch legacy API for %s: %s", app_name, str(e))
|
logger.warning("Failed to fetch legacy API for %s: %s", app_name, str(e))
|
||||||
return ""
|
return ""
|
||||||
@@ -677,7 +673,7 @@ def get_egs_game_description_async(
|
|||||||
url = "https://graphql.epicgames.com/graphql"
|
url = "https://graphql.epicgames.com/graphql"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=search_query, headers=headers, timeout=5)
|
response = requests.post(url, json=search_query, headers=headers, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
if namespace:
|
if namespace:
|
||||||
@@ -696,6 +692,9 @@ def get_egs_game_description_async(
|
|||||||
for substring in ["bundle", "pack", "edition", "dlc", "upgrade", "chapter", "набор", "пак", "дополнение"])):
|
for substring in ["bundle", "pack", "edition", "dlc", "upgrade", "chapter", "набор", "пак", "дополнение"])):
|
||||||
return element.get("description", ""), element.get("productSlug", "")
|
return element.get("description", ""), element.get("productSlug", "")
|
||||||
return "", ""
|
return "", ""
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning("GraphQL request timeout for %s with locale %s", app_name, locale)
|
||||||
|
return "", ""
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.warning("Failed to fetch GraphQL data for %s with locale %s: %s", app_name, locale, str(e))
|
logger.warning("Failed to fetch GraphQL data for %s with locale %s: %s", app_name, locale, str(e))
|
||||||
return "", ""
|
return "", ""
|
||||||
@@ -724,6 +723,10 @@ def get_egs_game_description_async(
|
|||||||
logger.debug("Fetched description from legacy API for %s: %s", app_name, (description[:100] + "...") if len(description) > 100 else description)
|
logger.debug("Fetched description from legacy API for %s: %s", app_name, (description[:100] + "...") if len(description) > 100 else description)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
logger.error("Skipping legacy API due to DNS resolution failure for %s", app_name)
|
logger.error("Skipping legacy API due to DNS resolution failure for %s", app_name)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning("Legacy API request timed out for %s", app_name)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Unexpected error fetching legacy API for %s: %s", app_name, str(e))
|
||||||
|
|
||||||
# Step 3: If still no description and no namespace, try GraphQL with title
|
# Step 3: If still no description and no namespace, try GraphQL with title
|
||||||
if not description and not namespace:
|
if not description and not namespace:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
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 PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
||||||
from collections.abc import Callable
|
|
||||||
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter, read_theme_from_config
|
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter, read_theme_from_config
|
||||||
@@ -10,8 +9,6 @@ from portprotonqt.custom_widgets import ClickableLabel
|
|||||||
from portprotonqt.portproton_api import PortProtonAPI
|
from portprotonqt.portproton_api import PortProtonAPI
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.animations import GameCardAnimations
|
from portprotonqt.animations import GameCardAnimations
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
|
|
||||||
class GameCard(QFrame):
|
class GameCard(QFrame):
|
||||||
borderWidthChanged = Signal()
|
borderWidthChanged = Signal()
|
||||||
@@ -102,7 +99,7 @@ class GameCard(QFrame):
|
|||||||
|
|
||||||
self.favoriteLabel = ClickableLabel(self.coverWidget)
|
self.favoriteLabel = ClickableLabel(self.coverWidget)
|
||||||
self.favoriteLabel.clicked.connect(self.toggle_favorite)
|
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.update_favorite_icon()
|
||||||
self.favoriteLabel.raise_()
|
self.favoriteLabel.raise_()
|
||||||
|
|
||||||
@@ -203,7 +200,7 @@ class GameCard(QFrame):
|
|||||||
self.update_cover_pixmap()
|
self.update_cover_pixmap()
|
||||||
|
|
||||||
def update_cover_pixmap(self):
|
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_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)
|
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))
|
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
|
||||||
@@ -404,14 +401,25 @@ class GameCard(QFrame):
|
|||||||
self.favoriteLabel.setText("☆")
|
self.favoriteLabel.setText("☆")
|
||||||
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
||||||
|
|
||||||
|
parent = self.parent()
|
||||||
|
while parent:
|
||||||
|
if hasattr(parent, 'game_library_manager'):
|
||||||
|
# Access using getattr with default to avoid Ruff B009 warning
|
||||||
|
manager = getattr(parent, 'game_library_manager', None)
|
||||||
|
if manager is not None:
|
||||||
|
QTimer.singleShot(0, manager.update_game_grid)
|
||||||
|
break
|
||||||
|
parent = parent.parent()
|
||||||
|
|
||||||
def toggle_favorite(self):
|
def toggle_favorite(self):
|
||||||
favorites = read_favorites()
|
favorites = read_favorites()
|
||||||
|
favorites_set = set(favorites)
|
||||||
if self.is_favorite:
|
if self.is_favorite:
|
||||||
if self.name in favorites:
|
if self.name in favorites_set:
|
||||||
favorites.remove(self.name)
|
favorites.remove(self.name)
|
||||||
self.is_favorite = False
|
self.is_favorite = False
|
||||||
else:
|
else:
|
||||||
if self.name not in favorites:
|
if self.name not in favorites_set:
|
||||||
favorites.append(self.name)
|
favorites.append(self.name)
|
||||||
self.is_favorite = True
|
self.is_favorite = True
|
||||||
save_favorites(favorites)
|
save_favorites(favorites)
|
||||||
@@ -444,9 +452,9 @@ class GameCard(QFrame):
|
|||||||
self.update_scale()
|
self.update_scale()
|
||||||
self.scaleChanged.emit()
|
self.scaleChanged.emit()
|
||||||
|
|
||||||
borderWidth = Property(int, getBorderWidth, setBorderWidth, None, "", notify=cast(Callable[[], None], borderWidthChanged))
|
borderWidth = Property(int, getBorderWidth, setBorderWidth, None, "", notify=borderWidthChanged)
|
||||||
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
|
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=gradientAngleChanged)
|
||||||
scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged))
|
scale = Property(float, getScale, setScale, None, "", notify=scaleChanged)
|
||||||
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
from portprotonqt.game_card import GameCard
|
from portprotonqt.game_card import GameCard
|
||||||
|
from portprotonqt.search_utils import SearchOptimizer, ThreadedSearch
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller
|
||||||
from PySide6.QtCore import Qt, QTimer
|
from PySide6.QtCore import Qt, QTimer
|
||||||
from portprotonqt.custom_widgets import FlowLayout
|
from portprotonqt.custom_widgets import FlowLayout
|
||||||
@@ -33,6 +34,7 @@ class MainWindowProtocol(Protocol):
|
|||||||
# Required attributes
|
# Required attributes
|
||||||
searchEdit: CustomLineEdit
|
searchEdit: CustomLineEdit
|
||||||
_last_card_width: int
|
_last_card_width: int
|
||||||
|
card_width: int
|
||||||
current_hovered_card: GameCard | None
|
current_hovered_card: GameCard | None
|
||||||
current_focused_card: GameCard | None
|
current_focused_card: GameCard | None
|
||||||
gamesListWidget: QWidget | None
|
gamesListWidget: QWidget | None
|
||||||
@@ -55,16 +57,9 @@ class GameLibraryManager:
|
|||||||
self.pending_deletions = deque()
|
self.pending_deletions = deque()
|
||||||
self.is_filtering = False
|
self.is_filtering = False
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
# Initialize search optimizer
|
||||||
def force_update_cards_library(self):
|
self.search_optimizer = SearchOptimizer()
|
||||||
if self.gamesListWidget and self.gamesListLayout:
|
self.search_thread: ThreadedSearch | None = None
|
||||||
self.gamesListLayout.invalidate()
|
|
||||||
self.gamesListWidget.updateGeometry()
|
|
||||||
widget = self.gamesListWidget
|
|
||||||
QTimer.singleShot(0, lambda: (
|
|
||||||
widget.adjustSize(),
|
|
||||||
widget.updateGeometry()
|
|
||||||
))
|
|
||||||
|
|
||||||
def create_games_library_widget(self):
|
def create_games_library_widget(self):
|
||||||
"""Creates the games library widget with search, grid, and slider."""
|
"""Creates the games library widget with search, grid, and slider."""
|
||||||
@@ -138,6 +133,8 @@ class GameLibraryManager:
|
|||||||
self.card_width = self.sizeSlider.value()
|
self.card_width = self.sizeSlider.value()
|
||||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||||
save_card_size(self.card_width)
|
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():
|
for card in self.game_card_cache.values():
|
||||||
card.update_card_size(self.card_width)
|
card.update_card_size(self.card_width)
|
||||||
self.update_game_grid()
|
self.update_game_grid()
|
||||||
@@ -219,6 +216,10 @@ class GameLibraryManager:
|
|||||||
if games_list is not None:
|
if games_list is not None:
|
||||||
self.filtered_games = games_list
|
self.filtered_games = games_list
|
||||||
self.dirty = True # Full rebuild only for non-filter
|
self.dirty = True # Full rebuild only for non-filter
|
||||||
|
else:
|
||||||
|
# When filtering, we want to update with the current filtered_games
|
||||||
|
# which has already been set by _perform_search
|
||||||
|
pass
|
||||||
self.is_filtering = is_filter
|
self.is_filtering = is_filter
|
||||||
self._pending_update = True
|
self._pending_update = True
|
||||||
|
|
||||||
@@ -227,6 +228,20 @@ class GameLibraryManager:
|
|||||||
else:
|
else:
|
||||||
self._update_game_grid_immediate()
|
self._update_game_grid_immediate()
|
||||||
|
|
||||||
|
def force_update_cards_library(self):
|
||||||
|
if self.gamesListWidget and self.gamesListLayout:
|
||||||
|
# Use singleShot to ensure UI updates happen after all other operations complete
|
||||||
|
# This prevents potential freezing in PySide 6.10.1
|
||||||
|
QTimer.singleShot(0, self._perform_force_update)
|
||||||
|
|
||||||
|
def _perform_force_update(self):
|
||||||
|
"""Perform the actual force update on the layout."""
|
||||||
|
if self.gamesListLayout:
|
||||||
|
self.gamesListLayout.invalidate()
|
||||||
|
if self.gamesListWidget:
|
||||||
|
self.gamesListWidget.adjustSize()
|
||||||
|
self.gamesListWidget.updateGeometry()
|
||||||
|
|
||||||
def _update_game_grid_immediate(self):
|
def _update_game_grid_immediate(self):
|
||||||
"""Updates the game grid with the provided or current game list."""
|
"""Updates the game grid with the provided or current game list."""
|
||||||
if self.gamesListLayout is None or self.gamesListWidget is None:
|
if self.gamesListLayout is None or self.gamesListWidget is None:
|
||||||
@@ -235,8 +250,9 @@ class GameLibraryManager:
|
|||||||
search_text = self.main_window.searchEdit.text().strip().lower()
|
search_text = self.main_window.searchEdit.text().strip().lower()
|
||||||
|
|
||||||
if self.is_filtering:
|
if self.is_filtering:
|
||||||
# Filter mode: do not change layout, only hide/show cards
|
# Filter mode: use the pre-computed filtered_games from optimized search
|
||||||
self._apply_filter_visibility(search_text)
|
# This means we already have the exact games to show
|
||||||
|
self._update_search_results()
|
||||||
else:
|
else:
|
||||||
# Full update: sorting, removal/addition, reorganization
|
# Full update: sorting, removal/addition, reorganization
|
||||||
games_list = self.filtered_games if self.filtered_games else self.games
|
games_list = self.filtered_games if self.filtered_games else self.games
|
||||||
@@ -264,8 +280,9 @@ class GameLibraryManager:
|
|||||||
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
|
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
|
# Quick partition: Sort favorites and non-favorites separately, then merge
|
||||||
fav_games = [g for g in games_list if g[0] in favorites]
|
favorites_set = set(favorites) # Convert to set for O(1) lookup
|
||||||
non_fav_games = [g for g in games_list if g[0] not in favorites]
|
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_fav = sorted(fav_games, key=partition_sort_key)
|
||||||
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
|
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
|
||||||
sorted_games = sorted_fav + sorted_non_fav
|
sorted_games = sorted_fav + sorted_non_fav
|
||||||
@@ -360,8 +377,73 @@ class GameLibraryManager:
|
|||||||
|
|
||||||
self.is_filtering = False # Reset flag in any case
|
self.is_filtering = False # Reset flag in any case
|
||||||
|
|
||||||
|
def _update_search_results(self):
|
||||||
|
"""Update the grid with pre-computed search results."""
|
||||||
|
if self.gamesListLayout is None or self.gamesListWidget is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Batch layout updates
|
||||||
|
self.gamesListWidget.setUpdatesEnabled(False)
|
||||||
|
if self.gamesListLayout is not None:
|
||||||
|
self.gamesListLayout.setEnabled(False) # Disable layout during batch
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create set of keys for current filtered games for fast lookup
|
||||||
|
filtered_keys = {(game[0], game[4]) for game in self.filtered_games} # (name, exec_line)
|
||||||
|
|
||||||
|
# Process existing cards: show cards that are in filtered results, hide others
|
||||||
|
cards_to_hide = []
|
||||||
|
for card_key, card in self.game_card_cache.items():
|
||||||
|
if card_key in filtered_keys:
|
||||||
|
# Card should be visible
|
||||||
|
if not card.isVisible():
|
||||||
|
card.setVisible(True)
|
||||||
|
else:
|
||||||
|
# Card should be hidden
|
||||||
|
if card.isVisible():
|
||||||
|
card.setVisible(False)
|
||||||
|
cards_to_hide.append(card_key)
|
||||||
|
|
||||||
|
# Now add any missing cards that are in filtered results but not in cache
|
||||||
|
cards_to_add = []
|
||||||
|
for game_data in self.filtered_games:
|
||||||
|
game_name = game_data[0]
|
||||||
|
exec_line = game_data[4]
|
||||||
|
game_key = (game_name, exec_line)
|
||||||
|
|
||||||
|
if game_key not in self.game_card_cache:
|
||||||
|
if self.context_menu_manager is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
card = self._create_game_card(game_data)
|
||||||
|
self.game_card_cache[game_key] = card
|
||||||
|
card.setVisible(True) # New cards should be visible
|
||||||
|
cards_to_add.append((game_key, card))
|
||||||
|
|
||||||
|
# Add new cards to layout
|
||||||
|
for _game_key, card in cards_to_add:
|
||||||
|
self.gamesListLayout.addWidget(card)
|
||||||
|
|
||||||
|
# Remove cards that are no longer needed (if any)
|
||||||
|
# Note: we're not removing them completely as they might be needed later
|
||||||
|
# Instead, we just hide them and they'll be reused if needed
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if self.gamesListLayout is not None:
|
||||||
|
self.gamesListLayout.setEnabled(True)
|
||||||
|
self.gamesListWidget.setUpdatesEnabled(True)
|
||||||
|
if self.gamesListLayout is not None:
|
||||||
|
self.gamesListLayout.update()
|
||||||
|
self.gamesListWidget.updateGeometry()
|
||||||
|
self.main_window._last_card_width = self.card_width
|
||||||
|
|
||||||
|
self.force_update_cards_library()
|
||||||
|
|
||||||
def _apply_filter_visibility(self, search_text: str):
|
def _apply_filter_visibility(self, search_text: str):
|
||||||
"""Applies visibility to cards based on search, without changing the layout."""
|
"""Applies visibility to cards based on search, without changing the layout."""
|
||||||
|
# This method is used for simple substring matching
|
||||||
|
# For the new optimized search, we'll use a different approach in update_game_grid
|
||||||
|
# when is_filter=True
|
||||||
visible_count = 0
|
visible_count = 0
|
||||||
for game_key, card in self.game_card_cache.items():
|
for game_key, card in self.game_card_cache.items():
|
||||||
game_name = card.name # Assume GameCard has 'name' attribute
|
game_name = card.name # Assume GameCard has 'name' attribute
|
||||||
@@ -438,9 +520,38 @@ class GameLibraryManager:
|
|||||||
"""Sets the games list and updates the filtered games."""
|
"""Sets the games list and updates the filtered games."""
|
||||||
self.games = games
|
self.games = games
|
||||||
self.filtered_games = self.games
|
self.filtered_games = self.games
|
||||||
|
|
||||||
|
# Build search indices for fast searching
|
||||||
|
self._build_search_indices(games)
|
||||||
|
|
||||||
self.dirty = True # Full resort needed
|
self.dirty = True # Full resort needed
|
||||||
self.update_game_grid()
|
self.update_game_grid()
|
||||||
|
|
||||||
|
def _build_search_indices(self, games: list[tuple]):
|
||||||
|
"""Build search indices for fast searching."""
|
||||||
|
# Prepare items for indexing: (search_key, game_data)
|
||||||
|
# We'll index by game name (index 0) and potentially other fields
|
||||||
|
items = []
|
||||||
|
for game in games:
|
||||||
|
# game is a tuple: (name, description, cover, appid, exec_line, controller_support,
|
||||||
|
# last_launch, formatted_playtime, protondb_tier, anticheat_status,
|
||||||
|
# last_played_timestamp, playtime_seconds, game_source)
|
||||||
|
name = str(game[0]).lower() if game[0] else ""
|
||||||
|
description = str(game[1]).lower() if game[1] else ""
|
||||||
|
|
||||||
|
# Create multiple search entries for better matching
|
||||||
|
items.append((name, game)) # Exact name
|
||||||
|
# Add other searchable fields if needed
|
||||||
|
if description:
|
||||||
|
items.append((description, game))
|
||||||
|
|
||||||
|
# Also add individual words from the name for partial matching
|
||||||
|
for word in name.split():
|
||||||
|
if len(word) > 2: # Only index words longer than 2 characters
|
||||||
|
items.append((word, game))
|
||||||
|
|
||||||
|
self.search_optimizer.build_indices(items)
|
||||||
|
|
||||||
def add_game_incremental(self, game_data: tuple):
|
def add_game_incremental(self, game_data: tuple):
|
||||||
"""Add a single game without full reload."""
|
"""Add a single game without full reload."""
|
||||||
self.games.append(game_data)
|
self.games.append(game_data)
|
||||||
@@ -464,4 +575,54 @@ class GameLibraryManager:
|
|||||||
|
|
||||||
def filter_games_delayed(self):
|
def filter_games_delayed(self):
|
||||||
"""Filters games based on search text and updates the grid."""
|
"""Filters games based on search text and updates the grid."""
|
||||||
self.update_game_grid(is_filter=True)
|
search_text = self.main_window.searchEdit.text().strip().lower()
|
||||||
|
|
||||||
|
if not search_text:
|
||||||
|
# If search is empty, show all games
|
||||||
|
self.filtered_games = self.games
|
||||||
|
self.update_game_grid(is_filter=True)
|
||||||
|
else:
|
||||||
|
# Use the optimized search
|
||||||
|
self._perform_search(search_text)
|
||||||
|
|
||||||
|
def _perform_search(self, search_text: str):
|
||||||
|
"""Perform the actual search using optimized search algorithms."""
|
||||||
|
if not search_text:
|
||||||
|
self.filtered_games = self.games
|
||||||
|
self.update_game_grid(is_filter=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use exact search first
|
||||||
|
exact_result = self.search_optimizer.exact_search(search_text)
|
||||||
|
if exact_result:
|
||||||
|
# If exact match found, show only that game
|
||||||
|
self.filtered_games = [exact_result]
|
||||||
|
self.update_game_grid(is_filter=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Try prefix search
|
||||||
|
prefix_results = self.search_optimizer.prefix_search(search_text)
|
||||||
|
if prefix_results:
|
||||||
|
# Get the actual game data from the prefix matches
|
||||||
|
filtered_games = []
|
||||||
|
for _match_text, game_data in prefix_results:
|
||||||
|
if game_data not in filtered_games: # Avoid duplicates
|
||||||
|
filtered_games.append(game_data)
|
||||||
|
self.filtered_games = filtered_games
|
||||||
|
self.update_game_grid(is_filter=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Finally, try fuzzy search
|
||||||
|
fuzzy_results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=60.0)
|
||||||
|
if fuzzy_results:
|
||||||
|
# Get the actual game data from the fuzzy matches
|
||||||
|
filtered_games = []
|
||||||
|
for _match_text, game_data, _score in fuzzy_results:
|
||||||
|
if game_data not in filtered_games: # Avoid duplicates
|
||||||
|
filtered_games.append(game_data)
|
||||||
|
self.filtered_games = filtered_games
|
||||||
|
self.update_game_grid(is_filter=True)
|
||||||
|
else:
|
||||||
|
# If no results found, show empty list
|
||||||
|
self.filtered_games = []
|
||||||
|
self.update_game_grid(is_filter=True)
|
||||||
|
|||||||
@@ -36,11 +36,22 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
current_theme_name = read_theme_from_config()
|
current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
def finish_with(pixmap: QPixmap):
|
def finish_with(pixmap: QPixmap):
|
||||||
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
# Check if pixmap is valid before attempting to scale it
|
||||||
x = (scaled.width() - width) // 2
|
if pixmap.isNull():
|
||||||
y = (scaled.height() - height) // 2
|
# Create a default placeholder pixmap instead of trying to scale a null pixmap
|
||||||
cropped = scaled.copy(x, y, width, height)
|
placeholder_pixmap = QPixmap(width, height)
|
||||||
callback(cropped)
|
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"))
|
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
||||||
image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
|
image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
|
||||||
@@ -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")
|
local_path = os.path.join(image_folder, f"{appid}.jpg")
|
||||||
if os.path.exists(local_path):
|
if os.path.exists(local_path):
|
||||||
pixmap = QPixmap(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)
|
finish_with(pixmap)
|
||||||
return
|
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)
|
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||||
if placeholder_path and QFile.exists(placeholder_path):
|
if placeholder_path and QFile.exists(placeholder_path):
|
||||||
pixmap.load(placeholder_path)
|
pixmap.load(placeholder_path)
|
||||||
|
if pixmap.isNull():
|
||||||
|
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
||||||
else:
|
else:
|
||||||
pixmap = QPixmap(width, height)
|
pixmap = QPixmap(width, height)
|
||||||
pixmap.fill(QColor("#333333"))
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка обработки URL {cover}: {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:
|
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):
|
if os.path.exists(local_path):
|
||||||
pixmap = QPixmap(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)
|
finish_with(pixmap)
|
||||||
return
|
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)
|
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||||
if placeholder_path and QFile.exists(placeholder_path):
|
if placeholder_path and QFile.exists(placeholder_path):
|
||||||
pixmap.load(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:
|
else:
|
||||||
pixmap = QPixmap(width, height)
|
pixmap = QPixmap(width, height)
|
||||||
pixmap.fill(QColor("#333333"))
|
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):
|
if cover and QFile.exists(cover):
|
||||||
pixmap = QPixmap(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)
|
finish_with(pixmap)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -122,6 +188,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
pixmap = QPixmap()
|
pixmap = QPixmap()
|
||||||
if placeholder_path and QFile.exists(placeholder_path):
|
if placeholder_path and QFile.exists(placeholder_path):
|
||||||
pixmap.load(placeholder_path)
|
pixmap.load(placeholder_path)
|
||||||
|
if pixmap.isNull():
|
||||||
|
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
||||||
else:
|
else:
|
||||||
pixmap = QPixmap(width, height)
|
pixmap = QPixmap(width, height)
|
||||||
pixmap.fill(QColor("#333333"))
|
pixmap.fill(QColor("#333333"))
|
||||||
@@ -131,9 +199,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
painter.end()
|
painter.end()
|
||||||
finish_with(pixmap)
|
finish_with(pixmap)
|
||||||
|
|
||||||
with queue_lock:
|
# Submit the process_image function directly to the executor
|
||||||
image_load_queue.put(process_image)
|
# This avoids the potential blocking issue with queue.get() in PySide 6.10.1
|
||||||
image_executor.submit(lambda: image_load_queue.get()())
|
image_executor.submit(process_image)
|
||||||
|
|
||||||
def round_corners(pixmap, radius):
|
def round_corners(pixmap, radius):
|
||||||
"""
|
"""
|
||||||
@@ -141,7 +209,15 @@ def round_corners(pixmap, radius):
|
|||||||
"""
|
"""
|
||||||
if pixmap.isNull():
|
if pixmap.isNull():
|
||||||
return pixmap
|
return pixmap
|
||||||
|
|
||||||
|
# Check if radius is valid to prevent issues
|
||||||
|
if radius <= 0:
|
||||||
|
return pixmap
|
||||||
|
|
||||||
size = pixmap.size()
|
size = pixmap.size()
|
||||||
|
if size.width() <= 0 or size.height() <= 0:
|
||||||
|
return pixmap
|
||||||
|
|
||||||
rounded = QPixmap(size)
|
rounded = QPixmap(size)
|
||||||
rounded.fill(QColor(0, 0, 0, 0))
|
rounded.fill(QColor(0, 0, 0, 0))
|
||||||
painter = QPainter(rounded)
|
painter = QPainter(rounded)
|
||||||
@@ -244,20 +320,31 @@ class FullscreenDialog(QDialog):
|
|||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
pixmap, caption = self.images[self.current_index]
|
pixmap, caption = self.images[self.current_index]
|
||||||
# Учитываем devicePixelRatio для масштабирования высокого качества
|
# Check if pixmap is valid before attempting to scale it
|
||||||
device_pixel_ratio = get_device_pixel_ratio()
|
if pixmap.isNull():
|
||||||
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
|
# Create a default placeholder pixmap instead of trying to scale a null pixmap
|
||||||
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
|
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
|
# Масштабируем изображение из оригинального pixmap
|
||||||
scaled_pixmap = pixmap.scaled(
|
scaled_pixmap = pixmap.scaled(
|
||||||
target_width,
|
target_width,
|
||||||
target_height,
|
target_height,
|
||||||
Qt.AspectRatioMode.KeepAspectRatio,
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
Qt.TransformationMode.SmoothTransformation
|
Qt.TransformationMode.SmoothTransformation
|
||||||
)
|
)
|
||||||
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
||||||
self.imageLabel.setPixmap(scaled_pixmap)
|
self.imageLabel.setPixmap(scaled_pixmap)
|
||||||
self.captionLabel.setText(caption)
|
self.captionLabel.setText(caption)
|
||||||
self.setWindowTitle(caption)
|
self.setWindowTitle(caption)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-11-24 23:48+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
@@ -76,10 +76,6 @@ msgstr ""
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -124,6 +120,10 @@ msgstr ""
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -252,13 +252,43 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Search"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -326,12 +356,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -347,6 +371,45 @@ msgstr ""
|
|||||||
msgid "Components installed successfully."
|
msgid "Components installed successfully."
|
||||||
msgstr ""
|
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..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -410,9 +473,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
msgid "Installation already in progress."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -432,6 +492,12 @@ msgstr ""
|
|||||||
msgid "Installation error."
|
msgid "Installation error."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refresh Grid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Game library refreshed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -444,6 +510,15 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "A refresh is already in progress..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refreshing..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refreshing game library..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Added '{name}'"
|
msgid "Added '{name}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -494,14 +569,21 @@ msgstr ""
|
|||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
msgid "Are you sure you want to clear prefix '{}'?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Clearing prefix..."
|
||||||
msgid "Prefix '{}' cleared successfully."
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Failed to start prefix clear process."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prefix cleared successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid "Prefix clear failed with exit code {}."
|
||||||
"Prefix '{}' cleared with errors:\n"
|
msgstr ""
|
||||||
"{}"
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Failed to run clear prefix command: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
msgid "Failed to start backup process."
|
||||||
@@ -579,6 +661,9 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -603,6 +688,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -677,6 +768,10 @@ msgstr ""
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Executable not found: {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -735,6 +830,262 @@ msgstr ""
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr ""
|
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 "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 "Specify the Wine prefix to run this game with"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Newest"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Stable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vulkan Backend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
||||||
|
"\n"
|
||||||
|
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
||||||
|
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
||||||
|
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
||||||
|
"driver)\n"
|
||||||
|
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
||||||
|
"Vulkan 1.1+)\n"
|
||||||
|
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
||||||
|
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"
|
msgid "Reboot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-11-24 23:48+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@@ -76,10 +76,6 @@ msgstr ""
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -124,6 +120,10 @@ msgstr ""
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -252,13 +252,43 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Search"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -326,12 +356,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -347,6 +371,45 @@ msgstr ""
|
|||||||
msgid "Components installed successfully."
|
msgid "Components installed successfully."
|
||||||
msgstr ""
|
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..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -410,9 +473,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
msgid "Installation already in progress."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -432,6 +492,12 @@ msgstr ""
|
|||||||
msgid "Installation error."
|
msgid "Installation error."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refresh Grid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Game library refreshed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -444,6 +510,15 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "A refresh is already in progress..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refreshing..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refreshing game library..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Added '{name}'"
|
msgid "Added '{name}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -494,14 +569,21 @@ msgstr ""
|
|||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
msgid "Are you sure you want to clear prefix '{}'?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Clearing prefix..."
|
||||||
msgid "Prefix '{}' cleared successfully."
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Failed to start prefix clear process."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prefix cleared successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid "Prefix clear failed with exit code {}."
|
||||||
"Prefix '{}' cleared with errors:\n"
|
msgstr ""
|
||||||
"{}"
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Failed to run clear prefix command: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
msgid "Failed to start backup process."
|
||||||
@@ -579,6 +661,9 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -603,6 +688,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -677,6 +768,10 @@ msgstr ""
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Executable not found: {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -735,6 +830,262 @@ msgstr ""
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr ""
|
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 "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 "Specify the Wine prefix to run this game with"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Newest"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Stable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vulkan Backend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
||||||
|
"\n"
|
||||||
|
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
||||||
|
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
||||||
|
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
||||||
|
"driver)\n"
|
||||||
|
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
||||||
|
"Vulkan 1.1+)\n"
|
||||||
|
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
||||||
|
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"
|
msgid "Reboot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-11-24 23:48+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -74,10 +74,6 @@ msgstr ""
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -122,6 +118,10 @@ msgstr ""
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -250,13 +250,43 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Search"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -324,12 +354,6 @@ msgstr ""
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -345,6 +369,45 @@ msgstr ""
|
|||||||
msgid "Components installed successfully."
|
msgid "Components installed successfully."
|
||||||
msgstr ""
|
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..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -408,9 +471,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
msgid "Installation already in progress."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -430,6 +490,12 @@ msgstr ""
|
|||||||
msgid "Installation error."
|
msgid "Installation error."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refresh Grid"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Game library refreshed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -442,6 +508,15 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "A refresh is already in progress..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refreshing..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Refreshing game library..."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Added '{name}'"
|
msgid "Added '{name}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -492,14 +567,21 @@ msgstr ""
|
|||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
msgid "Are you sure you want to clear prefix '{}'?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Clearing prefix..."
|
||||||
msgid "Prefix '{}' cleared successfully."
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Failed to start prefix clear process."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Prefix cleared successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid "Prefix clear failed with exit code {}."
|
||||||
"Prefix '{}' cleared with errors:\n"
|
msgstr ""
|
||||||
"{}"
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Failed to run clear prefix command: {}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
msgid "Failed to start backup process."
|
||||||
@@ -577,6 +659,9 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -601,6 +686,12 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -675,6 +766,10 @@ msgstr ""
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Executable not found: {0}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -733,6 +828,262 @@ msgstr ""
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr ""
|
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 "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 "Specify the Wine prefix to run this game with"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Newest"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Stable"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vulkan Backend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
||||||
|
"\n"
|
||||||
|
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
||||||
|
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
||||||
|
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
||||||
|
"driver)\n"
|
||||||
|
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
||||||
|
"Vulkan 1.1+)\n"
|
||||||
|
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
||||||
|
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"
|
msgid "Reboot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-10-12 17:14+0500\n"
|
"POT-Creation-Date: 2025-11-24 23:48+0500\n"
|
||||||
"PO-Revision-Date: 2025-10-12 17:13+0500\n"
|
"PO-Revision-Date: 2025-11-24 23:47+0500\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language: ru_RU\n"
|
"Language: ru_RU\n"
|
||||||
"Language-Team: ru_RU <LL@li.org>\n"
|
"Language-Team: ru_RU <LL@li.org>\n"
|
||||||
@@ -77,10 +77,6 @@ msgstr "Остановлен(а) '{game_name}'"
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr "Legendary не найден по пути {path}"
|
msgstr "Legendary не найден по пути {path}"
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr "start.sh не найден по адресу {path}"
|
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr "Успешно"
|
msgstr "Успешно"
|
||||||
|
|
||||||
@@ -127,6 +123,10 @@ msgstr "'{game_name}' был(а) добавлен(а) в избранное"
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr "'{game_name}' был(а) удалён(а) из избранного"
|
msgstr "'{game_name}' был(а) удалён(а) из избранного"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr "start.sh не найден по адресу {path}"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr "Запустить игру \"{name}\" с помощью PortProton"
|
msgstr "Запустить игру \"{name}\" с помощью PortProton"
|
||||||
@@ -259,13 +259,43 @@ msgstr "Удалить"
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr "Выбрать всё"
|
msgstr "Выбрать всё"
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Open"
|
||||||
msgid "Launching {0}"
|
msgstr "Открыть"
|
||||||
msgstr "Идёт запуск {0}"
|
|
||||||
|
msgid "Select Dir"
|
||||||
|
msgstr "Выбрать папку"
|
||||||
|
|
||||||
|
msgid "Prev Dir"
|
||||||
|
msgstr "Предыдущий каталог"
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Отмена"
|
msgstr "Отмена"
|
||||||
|
|
||||||
|
msgid "Toggle"
|
||||||
|
msgstr "Переключить"
|
||||||
|
|
||||||
|
msgid "Install"
|
||||||
|
msgstr "Установить"
|
||||||
|
|
||||||
|
msgid "Force Install"
|
||||||
|
msgstr "Принудительно установить"
|
||||||
|
|
||||||
|
msgid "Prev Tab"
|
||||||
|
msgstr "Предыдущая вкладка"
|
||||||
|
|
||||||
|
msgid "Next Tab"
|
||||||
|
msgstr "Следующая вкладка"
|
||||||
|
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "Сохранить"
|
||||||
|
|
||||||
|
msgid "Search"
|
||||||
|
msgstr "Поиск"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Launching {0}"
|
||||||
|
msgstr "Идёт запуск {0}"
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr "Проводник"
|
msgstr "Проводник"
|
||||||
|
|
||||||
@@ -333,12 +363,6 @@ msgstr "Шрифты"
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Настройки"
|
msgstr "Настройки"
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr "Принудительно установить"
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr "Установить"
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
msgid "Winetricks not found. Please try again."
|
||||||
msgstr "Winetricks не найден. Повторите попытку."
|
msgstr "Winetricks не найден. Повторите попытку."
|
||||||
|
|
||||||
@@ -354,6 +378,45 @@ msgstr "Установка не удалась. Проверьте журнал
|
|||||||
msgid "Components installed successfully."
|
msgid "Components installed successfully."
|
||||||
msgstr "Компоненты успешно установлены."
|
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..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr "Загрузка игр из Epic Games Store..."
|
msgstr "Загрузка игр из Epic Games Store..."
|
||||||
|
|
||||||
@@ -417,9 +480,6 @@ msgstr "Назад"
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr "Полный экран"
|
msgstr "Полный экран"
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr "Поиск"
|
|
||||||
|
|
||||||
msgid "Installation already in progress."
|
msgid "Installation already in progress."
|
||||||
msgstr "Установка уже выполняется."
|
msgstr "Установка уже выполняется."
|
||||||
|
|
||||||
@@ -439,6 +499,12 @@ msgstr "Установка не удалась."
|
|||||||
msgid "Installation error."
|
msgid "Installation error."
|
||||||
msgstr "Ошибка установки."
|
msgstr "Ошибка установки."
|
||||||
|
|
||||||
|
msgid "Refresh Grid"
|
||||||
|
msgstr "Обновить"
|
||||||
|
|
||||||
|
msgid "Game library refreshed"
|
||||||
|
msgstr "Игровая библиотека обновлена"
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr "Загрузка игр из Steam..."
|
msgstr "Загрузка игр из Steam..."
|
||||||
|
|
||||||
@@ -451,6 +517,15 @@ msgstr "Игровая библиотека"
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr "Найти игры..."
|
msgstr "Найти игры..."
|
||||||
|
|
||||||
|
msgid "A refresh is already in progress..."
|
||||||
|
msgstr "Обновление уже выполняется..."
|
||||||
|
|
||||||
|
msgid "Refreshing..."
|
||||||
|
msgstr "Обновление..."
|
||||||
|
|
||||||
|
msgid "Refreshing game library..."
|
||||||
|
msgstr "Обновление игровой библиотеки..."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Added '{name}'"
|
msgid "Added '{name}'"
|
||||||
msgstr "'{name}' добавлен(а)"
|
msgstr "'{name}' добавлен(а)"
|
||||||
@@ -501,17 +576,22 @@ msgstr "Подтвердите очистку"
|
|||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
msgid "Are you sure you want to clear prefix '{}'?"
|
||||||
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
|
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
|
||||||
|
|
||||||
#, python-brace-format
|
msgid "Clearing prefix..."
|
||||||
msgid "Prefix '{}' cleared successfully."
|
msgstr "Очистка префикса..."
|
||||||
msgstr "Префикс '{}' успешно удален."
|
|
||||||
|
msgid "Failed to start prefix clear process."
|
||||||
|
msgstr "Не удалось запустить процесс очистки префикса."
|
||||||
|
|
||||||
|
msgid "Prefix cleared successfully."
|
||||||
|
msgstr "Префикс удален успешно."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid "Prefix clear failed with exit code {}."
|
||||||
"Prefix '{}' cleared with errors:\n"
|
msgstr "Очистка префикса завершилась с кодом завершения {}."
|
||||||
"{}"
|
|
||||||
msgstr ""
|
#, python-brace-format
|
||||||
"Префикс '{}' очищен с ошибками:\n"
|
msgid "Failed to run clear prefix command: {}"
|
||||||
"{}"
|
msgstr "Не удалось выполнить команду очистки префикса: {}"
|
||||||
|
|
||||||
msgid "Failed to start backup process."
|
msgid "Failed to start backup process."
|
||||||
msgstr "Не удалось запустить процесс резервного копирования."
|
msgstr "Не удалось запустить процесс резервного копирования."
|
||||||
@@ -588,6 +668,9 @@ msgstr "все"
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr "Фильтр игр:"
|
msgstr "Фильтр игр:"
|
||||||
|
|
||||||
|
msgid "Gamepad Type:"
|
||||||
|
msgstr "Тип геймпада:"
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr "Адрес прокси"
|
msgstr "Адрес прокси"
|
||||||
|
|
||||||
@@ -612,6 +695,12 @@ msgstr "Запуск приложения в полноэкранном режи
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr "Режим полноэкранного отображения приложения:"
|
msgstr "Режим полноэкранного отображения приложения:"
|
||||||
|
|
||||||
|
msgid "Minimize to tray on close"
|
||||||
|
msgstr "Сворачивать в трей при закрытии"
|
||||||
|
|
||||||
|
msgid "Application Close Mode:"
|
||||||
|
msgstr "Режим закрытия приложения:"
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
||||||
|
|
||||||
@@ -688,6 +777,10 @@ msgstr "Тема '{0}' применена успешно"
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr "Ошибка при применение темы '{0}'"
|
msgstr "Ошибка при применение темы '{0}'"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "Executable not found: {0}"
|
||||||
|
msgstr "Исполняемый файл не найден: {0}"
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr "Последний запуск"
|
msgstr "Последний запуск"
|
||||||
|
|
||||||
@@ -746,6 +839,329 @@ msgstr "Неправильный формат команды (flatpak)"
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr "Файл не найден: {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 "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 "Specify the Wine prefix to run this game with"
|
||||||
|
msgstr "Укажите префикс Wine для запуска этой игры"
|
||||||
|
|
||||||
|
msgid "Newest"
|
||||||
|
msgstr "Новейший"
|
||||||
|
|
||||||
|
msgid "Stable"
|
||||||
|
msgstr "Стабильный"
|
||||||
|
|
||||||
|
msgid "Vulkan Backend"
|
||||||
|
msgstr "Vulkan рендеринг"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
||||||
|
"\n"
|
||||||
|
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
||||||
|
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
||||||
|
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
||||||
|
"driver)\n"
|
||||||
|
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
||||||
|
"Vulkan 1.1+)\n"
|
||||||
|
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
||||||
|
msgstr ""
|
||||||
|
"Выберите бэкэнд DirectX → Vulkan/OpenGL:\n"
|
||||||
|
"\n"
|
||||||
|
"• Новейший — последние версии DXVK + VKD3D (наилучшая "
|
||||||
|
"совместимость/производительность, требует современных драйверов: AMD Mesa"
|
||||||
|
" 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
||||||
|
"• Стабильный — более старая, хорошо протестированная версия DXVK + VKD3D "
|
||||||
|
"(работает с любыми драйверами Vulkan 1.3+)\n"
|
||||||
|
"• Sarek — экспериментальная версия DXVK-Sarek + VKD3D-Sarek (поддерживает"
|
||||||
|
" более старые драйверы, Vulkan 1.1+)\n"
|
||||||
|
"• WINED3D — резервный вариант OpenGL (наименьшая производительность, "
|
||||||
|
"используйте только в случае сбоя других вариантов)"
|
||||||
|
|
||||||
|
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"
|
msgid "Reboot"
|
||||||
msgstr "Перезагрузить"
|
msgstr "Перезагрузить"
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import urllib.parse
|
|||||||
import time
|
import time
|
||||||
import glob
|
import glob
|
||||||
import re
|
import re
|
||||||
|
import hashlib
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from PySide6.QtCore import QThread, Signal
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.config_utils import get_portproton_location
|
from portprotonqt.config_utils import get_portproton_location
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||||
|
AUTOINSTALL_CACHE_DURATION = 3600 # 1 hour for autoinstall cache
|
||||||
|
|
||||||
def normalize_name(s):
|
def normalize_name(s):
|
||||||
"""
|
"""
|
||||||
@@ -59,6 +62,7 @@ class PortProtonAPI:
|
|||||||
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
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.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
|
||||||
self._topics_data = None
|
self._topics_data = None
|
||||||
|
self._autoinstall_cache = None # New: In-memory cache
|
||||||
|
|
||||||
def _get_game_dir(self, exe_name: str) -> str:
|
def _get_game_dir(self, exe_name: str) -> str:
|
||||||
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
||||||
@@ -231,67 +235,150 @@ class PortProtonAPI:
|
|||||||
logger.error(f"Failed to parse {file_path}: {e}")
|
logger.error(f"Failed to parse {file_path}: {e}")
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
def get_autoinstall_games_async(self, callback: Callable[[list[tuple]], None]) -> None:
|
def _compute_scripts_signature(self, auto_dir: str) -> str:
|
||||||
"""Load auto-install games with user/builtin covers (no async download here)."""
|
"""Compute a hash-based signature of the autoinstall scripts to detect changes."""
|
||||||
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):
|
if not os.path.exists(auto_dir):
|
||||||
callback(games)
|
return ""
|
||||||
return
|
|
||||||
|
|
||||||
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
|
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
|
||||||
if not scripts:
|
# Simple hash: concatenate sorted filenames and hash
|
||||||
callback(games)
|
filenames_str = "".join(sorted([os.path.basename(s) for s in scripts]))
|
||||||
return
|
return hashlib.md5(filenames_str.encode()).hexdigest()
|
||||||
|
|
||||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
def _load_autoinstall_cache(self):
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
"""Load cached autoinstall games if fresh and scripts unchanged."""
|
||||||
base_autoinstall_dir = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
|
if self._autoinstall_cache is not None:
|
||||||
os.makedirs(base_autoinstall_dir, exist_ok=True)
|
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:
|
||||||
|
# Add timeout protection for file operations
|
||||||
|
start_time = time.time()
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
# Check for timeout during signature computation
|
||||||
|
if time.time() - start_time > 3: # 3 second timeout
|
||||||
|
logger.warning("Cache loading took too long, skipping cache")
|
||||||
|
return None
|
||||||
|
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:
|
def _save_autoinstall_cache(self, games):
|
||||||
display_name, exe_name = self.parse_autoinstall_script(script_path)
|
"""Save parsed autoinstall games to cache with scripts signature."""
|
||||||
script_name = os.path.splitext(os.path.basename(script_path))[0]
|
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):
|
def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None:
|
||||||
continue
|
"""Start loading auto-install games in a background thread. Returns the thread for management."""
|
||||||
|
class AutoinstallWorker(QThread):
|
||||||
|
finished = Signal(list)
|
||||||
|
api: "PortProtonAPI"
|
||||||
|
portproton_location: str | None
|
||||||
|
|
||||||
exe_name = os.path.splitext(exe_name)[0] # Без .exe
|
def run(self):
|
||||||
user_game_folder = os.path.join(base_autoinstall_dir, exe_name)
|
import time
|
||||||
os.makedirs(user_game_folder, exist_ok=True)
|
# Check cache in this background thread, not in main thread
|
||||||
|
start_time = time.time()
|
||||||
|
cached_games = self.api._load_autoinstall_cache()
|
||||||
|
# If cache loading took too long (>2 seconds), skip cache and load directly
|
||||||
|
if time.time() - start_time > 2:
|
||||||
|
logger.warning("Cache loading took too long, proceeding without cache")
|
||||||
|
cached_games = None
|
||||||
|
|
||||||
# Поиск обложки
|
if cached_games is not None:
|
||||||
cover_path = ""
|
self.finished.emit(cached_games)
|
||||||
user_files = set(os.listdir(user_game_folder)) if os.path.exists(user_game_folder) else set()
|
return
|
||||||
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:
|
# No cache: Load games from scratch
|
||||||
logger.debug(f"No local cover found for autoinstall {exe_name}")
|
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
|
||||||
|
|
||||||
# Формируем кортеж игры (добавлен exe_name в конец)
|
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
|
||||||
game_tuple = (
|
if not scripts:
|
||||||
display_name, # name
|
self.finished.emit(games)
|
||||||
"", # description
|
return
|
||||||
cover_path, # cover
|
|
||||||
"", # appid
|
|
||||||
f"autoinstall:{script_name}", # exec_line
|
|
||||||
"", # controller_support
|
|
||||||
"Never", # last_launch
|
|
||||||
"0h 0m", # formatted_playtime
|
|
||||||
"", # protondb_tier
|
|
||||||
"", # anticheat_status
|
|
||||||
0, # last_played
|
|
||||||
0, # playtime_seconds
|
|
||||||
"autoinstall", # game_source
|
|
||||||
exe_name # exe_name
|
|
||||||
)
|
|
||||||
games.append(game_tuple)
|
|
||||||
|
|
||||||
callback(games)
|
xdg_data_home = os.getenv(
|
||||||
|
"XDG_DATA_HOME",
|
||||||
|
os.path.join(os.path.expanduser("~"), ".local", "share"),
|
||||||
|
)
|
||||||
|
base_autoinstall_dir = os.path.join(
|
||||||
|
xdg_data_home, "PortProtonQt", "custom_data", "autoinstall"
|
||||||
|
)
|
||||||
|
os.makedirs(base_autoinstall_dir, exist_ok=True)
|
||||||
|
|
||||||
|
for script_path in scripts:
|
||||||
|
display_name, exe_name = self.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):
|
def _load_topics_data(self):
|
||||||
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
||||||
|
|||||||
379
portprotonqt/search_utils.py
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
"""
|
||||||
|
Utility module for search optimizations including Trie, hash tables, and fuzzy matching.
|
||||||
|
"""
|
||||||
|
import concurrent.futures
|
||||||
|
import threading
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
from rapidfuzz import fuzz
|
||||||
|
from threading import Lock
|
||||||
|
from portprotonqt.logger import get_logger
|
||||||
|
from PySide6.QtCore import QThread, QRunnable, Signal, QObject, QTimer
|
||||||
|
import requests
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
class TrieNode:
|
||||||
|
"""Node in the Trie data structure."""
|
||||||
|
def __init__(self):
|
||||||
|
self.children = {}
|
||||||
|
self.is_end_word = False
|
||||||
|
self.payload = None # Store the original data in leaf nodes
|
||||||
|
|
||||||
|
class Trie:
|
||||||
|
"""Trie data structure for efficient prefix-based searching."""
|
||||||
|
def __init__(self):
|
||||||
|
self.root = TrieNode()
|
||||||
|
self._lock = Lock() # Thread safety for concurrent access
|
||||||
|
|
||||||
|
def insert(self, key: str, payload: Any):
|
||||||
|
"""Insert a key with payload into the Trie."""
|
||||||
|
with self._lock:
|
||||||
|
node = self.root
|
||||||
|
for char in key.lower():
|
||||||
|
if char not in node.children:
|
||||||
|
node.children[char] = TrieNode()
|
||||||
|
node = node.children[char]
|
||||||
|
node.is_end_word = True
|
||||||
|
node.payload = payload
|
||||||
|
|
||||||
|
def search_prefix(self, prefix: str) -> list[tuple[str, Any]]:
|
||||||
|
"""Find all entries with the given prefix."""
|
||||||
|
with self._lock:
|
||||||
|
node = self.root
|
||||||
|
for char in prefix.lower():
|
||||||
|
if char not in node.children:
|
||||||
|
return []
|
||||||
|
node = node.children[char]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
self._collect_all(node, prefix.lower(), results)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _collect_all(self, node: TrieNode, current_prefix: str, results: list[tuple[str, Any]]):
|
||||||
|
"""Collect all entries from the current node."""
|
||||||
|
if node.is_end_word:
|
||||||
|
results.append((current_prefix, node.payload))
|
||||||
|
|
||||||
|
for char, child_node in node.children.items():
|
||||||
|
self._collect_all(child_node, current_prefix + char, results)
|
||||||
|
|
||||||
|
class FuzzySearchIndex:
|
||||||
|
"""Index for fuzzy string matching with rapidfuzz."""
|
||||||
|
def __init__(self, items: list[tuple[str, Any]] | None = None):
|
||||||
|
self.items: list[tuple[str, Any]] = items or []
|
||||||
|
self.normalized_items: list[tuple[str, Any]] = []
|
||||||
|
self._lock = Lock()
|
||||||
|
self._build_normalized_index()
|
||||||
|
|
||||||
|
def _build_normalized_index(self):
|
||||||
|
"""Build a normalized index for fuzzy matching."""
|
||||||
|
with self._lock:
|
||||||
|
self.normalized_items = [(self._normalize(item[0]), item[1]) for item in self.items]
|
||||||
|
|
||||||
|
def _normalize(self, s: str) -> str:
|
||||||
|
"""Normalize string for fuzzy matching."""
|
||||||
|
s = s.lower()
|
||||||
|
for ch in ["™", "®"]:
|
||||||
|
s = s.replace(ch, "")
|
||||||
|
for ch in ["-", ":", ","]:
|
||||||
|
s = s.replace(ch, " ")
|
||||||
|
s = " ".join(s.split())
|
||||||
|
for suffix in ["bin", "app"]:
|
||||||
|
if s.endswith(suffix):
|
||||||
|
s = s[:-len(suffix)].strip()
|
||||||
|
keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"}
|
||||||
|
words = s.split()
|
||||||
|
filtered_words = [word for word in words if word not in keywords_to_remove]
|
||||||
|
return " ".join(filtered_words)
|
||||||
|
|
||||||
|
def fuzzy_search(self, query: str, limit: int = 5, min_score: float = 60.0) -> list[tuple[str, Any, float]]:
|
||||||
|
"""Perform fuzzy search using rapidfuzz."""
|
||||||
|
with self._lock:
|
||||||
|
if not query or not self.normalized_items:
|
||||||
|
return []
|
||||||
|
|
||||||
|
query_normalized = self._normalize(query)
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for i, (item_text, item_data) in enumerate(self.normalized_items):
|
||||||
|
score = fuzz.ratio(query_normalized, item_text)
|
||||||
|
if score >= min_score:
|
||||||
|
results.append((self.items[i][0], item_data, score))
|
||||||
|
|
||||||
|
# Sort by score descending
|
||||||
|
results.sort(key=lambda x: x[2], reverse=True)
|
||||||
|
return results[:limit]
|
||||||
|
|
||||||
|
class SearchOptimizer:
|
||||||
|
"""Main search optimization class combining multiple approaches."""
|
||||||
|
def __init__(self):
|
||||||
|
self.hash_index: dict[str, Any] = {}
|
||||||
|
self.trie_index = Trie()
|
||||||
|
self.fuzzy_index = None
|
||||||
|
self._lock = Lock()
|
||||||
|
|
||||||
|
def build_indices(self, items: list[tuple[str, Any]]):
|
||||||
|
"""Build all search indices from items."""
|
||||||
|
with self._lock:
|
||||||
|
self.hash_index = {item[0].lower(): item[1] for item in items}
|
||||||
|
self.trie_index = Trie()
|
||||||
|
for key, value in self.hash_index.items():
|
||||||
|
self.trie_index.insert(key, value)
|
||||||
|
self.fuzzy_index = FuzzySearchIndex(items)
|
||||||
|
|
||||||
|
def exact_search(self, key: str) -> Any | None:
|
||||||
|
"""Perform exact hash-based lookup."""
|
||||||
|
with self._lock:
|
||||||
|
return self.hash_index.get(key.lower())
|
||||||
|
|
||||||
|
def prefix_search(self, prefix: str) -> list[tuple[str, Any]]:
|
||||||
|
"""Perform prefix search using Trie."""
|
||||||
|
with self._lock:
|
||||||
|
return self.trie_index.search_prefix(prefix)
|
||||||
|
|
||||||
|
def fuzzy_search(self, query: str, limit: int = 5, min_score: float = 60.0) -> list[tuple[str, Any, float]]:
|
||||||
|
"""Perform fuzzy search."""
|
||||||
|
if self.fuzzy_index:
|
||||||
|
return self.fuzzy_index.fuzzy_search(query, limit, min_score)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
class RequestRunnable(QRunnable):
|
||||||
|
"""Runnable for executing HTTP requests in a thread."""
|
||||||
|
|
||||||
|
def __init__(self, method: str, url: str, on_success=None, on_error=None, **kwargs):
|
||||||
|
super().__init__()
|
||||||
|
self.method = method
|
||||||
|
self.url = url
|
||||||
|
self.kwargs = kwargs
|
||||||
|
self.result = None
|
||||||
|
self.error = None
|
||||||
|
self.on_success: Callable | None = on_success
|
||||||
|
self.on_error: Callable | None = on_error
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
if self.method.lower() == 'get':
|
||||||
|
self.result = requests.get(self.url, **self.kwargs)
|
||||||
|
elif self.method.lower() == 'post':
|
||||||
|
self.result = requests.post(self.url, **self.kwargs)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported HTTP method: {self.method}")
|
||||||
|
|
||||||
|
# Execute success callback if provided
|
||||||
|
if self.on_success is not None:
|
||||||
|
success_callback = self.on_success # Capture the callback
|
||||||
|
def success_handler():
|
||||||
|
if success_callback is not None: # Re-check to satisfy Pyright
|
||||||
|
success_callback(self.result)
|
||||||
|
QTimer.singleShot(0, success_handler)
|
||||||
|
except Exception as e:
|
||||||
|
self.error = e
|
||||||
|
# Execute error callback if provided
|
||||||
|
if self.on_error is not None:
|
||||||
|
error_callback = self.on_error # Capture the callback
|
||||||
|
captured_error = e # Capture the exception
|
||||||
|
def error_handler():
|
||||||
|
error_callback(captured_error)
|
||||||
|
QTimer.singleShot(0, error_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def run_request_in_thread(method: str, url: str, on_success: Callable | None = None, on_error: Callable | None = None, **kwargs):
|
||||||
|
"""Run HTTP request in a separate thread using Qt's thread system."""
|
||||||
|
runnable = RequestRunnable(method, url, on_success=on_success, on_error=on_error, **kwargs)
|
||||||
|
|
||||||
|
# Use QThreadPool to execute the runnable
|
||||||
|
from PySide6.QtCore import QThreadPool
|
||||||
|
thread_pool = QThreadPool.globalInstance()
|
||||||
|
thread_pool.start(runnable)
|
||||||
|
|
||||||
|
return runnable # Return the runnable to allow for potential cancellation if needed
|
||||||
|
|
||||||
|
|
||||||
|
def run_function_in_thread(func, *args, on_success: Callable | None = None, on_error: Callable | None = None, **kwargs):
|
||||||
|
"""Run a function in a separate thread."""
|
||||||
|
def execute():
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
if on_success:
|
||||||
|
on_success(result)
|
||||||
|
except Exception as e:
|
||||||
|
if on_error:
|
||||||
|
on_error(e)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=execute)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
|
def run_in_thread(func, *args, **kwargs):
|
||||||
|
"""Run a function in a separate thread."""
|
||||||
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||||
|
future = executor.submit(func, *args, **kwargs)
|
||||||
|
return future.result()
|
||||||
|
|
||||||
|
|
||||||
|
def run_in_thread_async(func, *args, callback: Callable | None = None, **kwargs):
|
||||||
|
"""Run a function in a separate thread asynchronously."""
|
||||||
|
import threading
|
||||||
|
def target():
|
||||||
|
try:
|
||||||
|
result = func(*args, **kwargs)
|
||||||
|
if callback:
|
||||||
|
callback(result)
|
||||||
|
except Exception as e:
|
||||||
|
if callback:
|
||||||
|
callback(None) # or handle error in callback
|
||||||
|
logger.error(f"Error in threaded operation: {e}")
|
||||||
|
|
||||||
|
thread = threading.Thread(target=target)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
|
# Threaded search implementation using QThread for performance optimization
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadedSearchWorker(QObject):
|
||||||
|
"""
|
||||||
|
A threaded worker for performing search operations without blocking the UI.
|
||||||
|
"""
|
||||||
|
search_started = Signal()
|
||||||
|
search_finished = Signal(list)
|
||||||
|
search_error = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.search_optimizer = SearchOptimizer()
|
||||||
|
self.games_data = []
|
||||||
|
|
||||||
|
def set_games_data(self, games_data: list):
|
||||||
|
"""Set the games data to be searched."""
|
||||||
|
self.games_data = games_data
|
||||||
|
# Build indices from the games data (name, description, etc.)
|
||||||
|
items = [(game[0], game) for game in games_data] # game[0] is the name
|
||||||
|
self.search_optimizer.build_indices(items)
|
||||||
|
|
||||||
|
def execute_search(self, search_text: str, search_type: str = "auto"):
|
||||||
|
"""
|
||||||
|
Execute search in a separate thread.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_text: Text to search for
|
||||||
|
search_type: Type of search ("exact", "prefix", "fuzzy", "auto")
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.search_started.emit()
|
||||||
|
import time
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
if search_type == "exact" or (search_type == "auto" and len(search_text) > 2):
|
||||||
|
exact_result = self.search_optimizer.exact_search(search_text)
|
||||||
|
if exact_result:
|
||||||
|
results = [exact_result]
|
||||||
|
elif search_type == "prefix":
|
||||||
|
results = self.search_optimizer.prefix_search(search_text)
|
||||||
|
elif search_type == "fuzzy" or search_type == "auto":
|
||||||
|
results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=50.0)
|
||||||
|
else:
|
||||||
|
# Auto-detect search type based on input
|
||||||
|
if len(search_text) < 3:
|
||||||
|
results = self.search_optimizer.prefix_search(search_text)
|
||||||
|
else:
|
||||||
|
# Try exact first, then fuzzy
|
||||||
|
exact_result = self.search_optimizer.exact_search(search_text)
|
||||||
|
if exact_result:
|
||||||
|
results = [exact_result]
|
||||||
|
else:
|
||||||
|
results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=50.0)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
print(f"Search completed in {end_time - start_time:.4f} seconds")
|
||||||
|
|
||||||
|
self.search_finished.emit(results)
|
||||||
|
except Exception as e:
|
||||||
|
self.search_error.emit(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadedSearch(QThread):
|
||||||
|
"""
|
||||||
|
QThread implementation for running search operations in the background.
|
||||||
|
"""
|
||||||
|
search_started = Signal()
|
||||||
|
search_finished = Signal(list)
|
||||||
|
search_error = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.worker = ThreadedSearchWorker()
|
||||||
|
self.search_text = ""
|
||||||
|
self.search_type = "auto"
|
||||||
|
self.games_data = []
|
||||||
|
|
||||||
|
# Connect worker signals to thread signals
|
||||||
|
self.worker.search_started.connect(self.search_started)
|
||||||
|
self.worker.search_finished.connect(self.search_finished)
|
||||||
|
self.worker.search_error.connect(self.search_error)
|
||||||
|
|
||||||
|
def set_search_params(self, search_text: str, games_data: list, search_type: str = "auto"):
|
||||||
|
"""Set parameters for the search operation."""
|
||||||
|
self.search_text = search_text
|
||||||
|
self.games_data = games_data
|
||||||
|
self.search_type = search_type
|
||||||
|
|
||||||
|
def set_games_data(self, games_data: list):
|
||||||
|
"""Set the games data to be searched."""
|
||||||
|
self.games_data = games_data
|
||||||
|
self.worker.set_games_data(games_data)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Run the search operation in the thread."""
|
||||||
|
self.worker.execute_search(self.search_text, self.search_type)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchThreadPool:
|
||||||
|
"""
|
||||||
|
A simple thread pool for managing multiple search operations.
|
||||||
|
"""
|
||||||
|
def __init__(self, max_threads: int = 3):
|
||||||
|
self.max_threads = max_threads
|
||||||
|
self.active_threads = []
|
||||||
|
self.thread_queue = []
|
||||||
|
|
||||||
|
def submit_search(self, search_text: str, games_data: list, search_type: str = "auto",
|
||||||
|
on_start: Callable | None = None, on_finish: Callable | None = None, on_error: Callable | None = None):
|
||||||
|
"""
|
||||||
|
Submit a search operation to the pool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
search_text: Text to search for
|
||||||
|
games_data: List of game data tuples to search in
|
||||||
|
search_type: Type of search ("exact", "prefix", "fuzzy", "auto")
|
||||||
|
on_start: Callback when search starts
|
||||||
|
on_finish: Callback when search finishes (receives results)
|
||||||
|
on_error: Callback when search errors (receives error message)
|
||||||
|
"""
|
||||||
|
search_thread = ThreadedSearch()
|
||||||
|
search_thread.set_search_params(search_text, games_data, search_type)
|
||||||
|
|
||||||
|
# Connect callbacks if provided
|
||||||
|
if on_start:
|
||||||
|
search_thread.search_started.connect(on_start)
|
||||||
|
if on_finish:
|
||||||
|
search_thread.search_finished.connect(on_finish)
|
||||||
|
if on_error:
|
||||||
|
search_thread.search_error.connect(on_error)
|
||||||
|
|
||||||
|
# Start the thread
|
||||||
|
search_thread.start()
|
||||||
|
self.active_threads.append(search_thread)
|
||||||
|
|
||||||
|
# Clean up finished threads
|
||||||
|
self.active_threads = [thread for thread in self.active_threads if thread.isRunning()]
|
||||||
|
|
||||||
|
return search_thread
|
||||||
229
portprotonqt/settings_manager.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
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_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': _("Specify the Wine prefix to run this game with"),
|
||||||
|
'type': 'combo',
|
||||||
|
'options': prefix_options,
|
||||||
|
'default': 'DEFAULT'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Vulkan Backend
|
||||||
|
vulkan_options = [
|
||||||
|
_("Newest"), # → 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 DirectX → Vulkan/OpenGL backend:\n\n"
|
||||||
|
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
||||||
|
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ driver)\n"
|
||||||
|
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, Vulkan 1.1+)\n"
|
||||||
|
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
||||||
|
),
|
||||||
|
'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.localization import get_steam_language
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.dialogs import generate_thumbnail
|
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
|
from collections.abc import Callable
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -23,6 +23,7 @@ import requests
|
|||||||
import random
|
import random
|
||||||
import base64
|
import base64
|
||||||
import glob
|
import glob
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
downloader = Downloader()
|
downloader = Downloader()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -411,6 +412,52 @@ def save_app_details(app_id, data):
|
|||||||
with open(cache_file, "wb") as f:
|
with open(cache_file, "wb") as f:
|
||||||
f.write(orjson.dumps(data))
|
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=10)
|
||||||
|
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 requests.exceptions.Timeout:
|
||||||
|
logger.warning(f"SGDB request timed out for {game_name}")
|
||||||
|
return ""
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning(f"SGDB request error for {game_name}: {e}")
|
||||||
|
return ""
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Unexpected error while fetching SGDB cover for {game_name}: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def check_url_exists(url: str) -> bool:
|
||||||
|
"""Check whether a URL returns HTTP 200."""
|
||||||
|
try:
|
||||||
|
r = requests.head(url, timeout=5)
|
||||||
|
return r.status_code == 200
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
logger.warning(f"URL check timed out for: {url}")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning(f"Request error when checking URL {url}: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Unexpected error when checking URL {url}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
|
def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
|
||||||
"""
|
"""
|
||||||
Asynchronously fetches detailed app info from Steam API.
|
Asynchronously fetches detailed app info from Steam API.
|
||||||
@@ -629,6 +676,11 @@ def get_full_steam_game_info_async(appid: int, callback: Callable[[dict], None])
|
|||||||
title = decode_text(app_info.get("name", ""))
|
title = decode_text(app_info.get("name", ""))
|
||||||
description = decode_text(app_info.get("short_description", ""))
|
description = decode_text(app_info.get("short_description", ""))
|
||||||
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
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_protondb_tier(tier: str):
|
||||||
def on_anticheat_status(anticheat_status: str):
|
def on_anticheat_status(anticheat_status: str):
|
||||||
@@ -722,12 +774,15 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
|
|||||||
game_name = desktop_name or exe_name.capitalize()
|
game_name = desktop_name or exe_name.capitalize()
|
||||||
|
|
||||||
if not matching_app:
|
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):
|
def on_anticheat_status(anticheat_status: str):
|
||||||
callback({
|
callback({
|
||||||
"appid": "",
|
"appid": "",
|
||||||
"name": decode_text(game_name),
|
"name": decode_text(game_name),
|
||||||
"description": "",
|
"description": "",
|
||||||
"cover": "",
|
"cover": cover,
|
||||||
"controller_support": "",
|
"controller_support": "",
|
||||||
"protondb_tier": "",
|
"protondb_tier": "",
|
||||||
"steam_game": "false",
|
"steam_game": "false",
|
||||||
@@ -758,6 +813,11 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
|
|||||||
title = decode_text(app_info.get("name", game_name))
|
title = decode_text(app_info.get("name", game_name))
|
||||||
description = decode_text(app_info.get("short_description", ""))
|
description = decode_text(app_info.get("short_description", ""))
|
||||||
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
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", "")
|
controller_support = app_info.get("controller_support", "")
|
||||||
|
|
||||||
def on_protondb_tier(tier: str):
|
def on_protondb_tier(tier: str):
|
||||||
@@ -957,7 +1017,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}")
|
return (False, f"Executable file not found: {exe_path}")
|
||||||
|
|
||||||
portproton_dir = get_portproton_location()
|
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")
|
logger.error("PortProton directory not found")
|
||||||
return (False, "PortProton directory not found")
|
return (False, "PortProton directory not found")
|
||||||
|
|
||||||
@@ -966,17 +1027,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())
|
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
|
||||||
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
|
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):
|
if not os.path.exists(script_path):
|
||||||
script_content = f"""#!/usr/bin/env bash
|
script_content = f"""#!/usr/bin/env bash
|
||||||
export LD_PRELOAD=
|
export LD_PRELOAD=
|
||||||
export START_FROM_STEAM=1
|
export START_FROM_STEAM=1
|
||||||
"{start_sh_path}" "{exe_path}" "$@"
|
"{start_sh}" "{exe_path}" "$@"
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(script_path, "w", encoding="utf-8") as f:
|
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 PySide6.QtGui import QIcon, QFontDatabase, QPixmap
|
||||||
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
||||||
|
|
||||||
|
# Icon caching for performance optimization
|
||||||
|
_icon_cache = {}
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# Папка, где располагаются все дополнительные темы
|
# Папка, где располагаются все дополнительные темы
|
||||||
@@ -108,34 +111,65 @@ def load_theme_fonts(theme_name):
|
|||||||
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
||||||
return
|
return
|
||||||
|
|
||||||
QFontDatabase.removeAllApplicationFonts()
|
def load_fonts_delayed():
|
||||||
fonts_folder = None
|
global _loaded_theme
|
||||||
if theme_name == "standart":
|
try:
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
# Only remove fonts if this is a theme change (not initial load)
|
||||||
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
current_loaded_theme = _loaded_theme # Capture the current value
|
||||||
else:
|
if current_loaded_theme is not None and current_loaded_theme != theme_name:
|
||||||
for themes_dir in THEMES_DIRS:
|
# Run font removal in the GUI thread with delay
|
||||||
theme_folder = os.path.join(themes_dir, theme_name)
|
QFontDatabase.removeAllApplicationFonts()
|
||||||
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
|
||||||
if os.path.exists(possible_fonts_folder):
|
|
||||||
fonts_folder = possible_fonts_folder
|
|
||||||
break
|
|
||||||
|
|
||||||
if not fonts_folder or not os.path.exists(fonts_folder):
|
import time
|
||||||
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
import os
|
||||||
return
|
start_time = time.time()
|
||||||
|
timeout = 3 # Reduced timeout to 3 seconds for faster loading
|
||||||
|
|
||||||
for filename in os.listdir(fonts_folder):
|
fonts_folder = None
|
||||||
if filename.lower().endswith((".ttf", ".otf")):
|
if theme_name == "standart":
|
||||||
font_path = os.path.join(fonts_folder, filename)
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
||||||
if font_id != -1:
|
|
||||||
families = QFontDatabase.applicationFontFamilies(font_id)
|
|
||||||
logger.info(f"Font {filename} successfully loaded: {families}")
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"Error loading font: {filename}")
|
for themes_dir in THEMES_DIRS:
|
||||||
|
theme_folder = os.path.join(themes_dir, theme_name)
|
||||||
|
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
||||||
|
if os.path.exists(possible_fonts_folder):
|
||||||
|
fonts_folder = possible_fonts_folder
|
||||||
|
break
|
||||||
|
|
||||||
_loaded_theme = theme_name
|
if not fonts_folder or not os.path.exists(fonts_folder):
|
||||||
|
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
font_files = []
|
||||||
|
for filename in os.listdir(fonts_folder):
|
||||||
|
if filename.lower().endswith((".ttf", ".otf")):
|
||||||
|
font_files.append(filename)
|
||||||
|
|
||||||
|
# Limit number of fonts loaded to prevent too much blocking
|
||||||
|
font_files = font_files[:10] # Only load first 10 fonts to prevent too much blocking
|
||||||
|
|
||||||
|
for filename in font_files:
|
||||||
|
if time.time() - start_time > timeout:
|
||||||
|
logger.warning(f"Font loading timed out for theme '{theme_name}' after loading {len(font_files)} fonts")
|
||||||
|
break
|
||||||
|
|
||||||
|
font_path = os.path.join(fonts_folder, filename)
|
||||||
|
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||||
|
if font_id != -1:
|
||||||
|
families = QFontDatabase.applicationFontFamilies(font_id)
|
||||||
|
logger.info(f"Font {filename} successfully loaded: {families}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Error loading font: {filename}")
|
||||||
|
|
||||||
|
# Update the global variable in the main thread
|
||||||
|
_loaded_theme = theme_name
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading fonts for theme '{theme_name}': {e}")
|
||||||
|
|
||||||
|
# Use QTimer to delay font loading until after the UI is rendered
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
QTimer.singleShot(100, load_fonts_delayed) # Delay font loading by 100ms
|
||||||
|
|
||||||
class ThemeWrapper:
|
class ThemeWrapper:
|
||||||
"""
|
"""
|
||||||
@@ -232,6 +266,14 @@ class ThemeManager:
|
|||||||
а если файл не найден, то из стандартной темы.
|
а если файл не найден, то из стандартной темы.
|
||||||
Если as_path=True, возвращает путь к иконке вместо QIcon.
|
Если 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
|
icon_path = None
|
||||||
theme_name = theme_name or self.current_theme_name
|
theme_name = theme_name or self.current_theme_name
|
||||||
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
|
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
|
||||||
@@ -279,12 +321,20 @@ class ThemeManager:
|
|||||||
# Если иконка всё равно не найдена
|
# Если иконка всё равно не найдена
|
||||||
if not icon_path or not os.path.exists(icon_path):
|
if not icon_path or not os.path.exists(icon_path):
|
||||||
logger.error(f"Warning: icon '{icon_name}' not found")
|
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:
|
if as_path:
|
||||||
|
# Cache the path
|
||||||
|
_icon_cache[cache_key] = icon_path
|
||||||
return 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):
|
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 |
@@ -25,6 +25,7 @@ color_e = "#404554"
|
|||||||
color_f = "#ffffff"
|
color_f = "#ffffff"
|
||||||
color_g = "rgba(0, 0, 0, 0)"
|
color_g = "rgba(0, 0, 0, 0)"
|
||||||
color_h = "transparent"
|
color_h = "transparent"
|
||||||
|
color_i = "rgba(40, 42, 51, 0.9)"
|
||||||
|
|
||||||
GAME_CARD_ANIMATION = {
|
GAME_CARD_ANIMATION = {
|
||||||
# Тип анимации при входе и выходе на детальную страницу
|
# Тип анимации при входе и выходе на детальную страницу
|
||||||
@@ -217,54 +218,40 @@ CONTEXT_MENU_STYLE = f"""
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VIRTUAL_KEYBOARD_STYLE = """
|
VIRTUAL_KEYBOARD_STYLE = f"""
|
||||||
VirtualKeyboard {
|
QWidget {{
|
||||||
background-color: rgba(30, 30, 30, 200);
|
background: {color_i};
|
||||||
border-radius: 0px;
|
}}
|
||||||
border: none;
|
QPushButton {{
|
||||||
}
|
|
||||||
QPushButton {
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border: 1px solid #555;
|
border: {border_a} {color_h};
|
||||||
border-top-color: #666;
|
border-radius: {border_radius_a};
|
||||||
border-left-color: #666;
|
|
||||||
border-radius: 3px;
|
|
||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
padding: 4px;
|
padding: 5px;
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #505050, stop:1 #404040);
|
background-color: {color_c};
|
||||||
color: #e0e0e0;
|
color: {color_f};
|
||||||
}
|
}}
|
||||||
QPushButton:hover {
|
QPushButton:hover {{
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #606060, stop:1 #505050);
|
background-color: {color_a};
|
||||||
border: 1px solid #666;
|
border: {border_b} {color_a};
|
||||||
border-top-color: #777;
|
}}
|
||||||
border-left-color: #777;
|
QPushButton:focus {{
|
||||||
}
|
border: {border_b} {color_a};
|
||||||
QPushButton:focus {
|
background-color: {color_a};
|
||||||
border: 2px solid #4a90e2;
|
}}
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #5a5a5a, stop:1 #454545);
|
QPushButton:pressed {{
|
||||||
}
|
background-color: {color_c};
|
||||||
QPushButton:pressed {
|
border: {border_a} {color_h};
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3a3a3a, stop:1 #303030);
|
}}
|
||||||
border: 1px solid #444;
|
QPushButton[checked="true"] {{
|
||||||
border-bottom-color: #555;
|
background-color: {color_a};
|
||||||
border-right-color: #555;
|
color: {color_f};
|
||||||
padding-top: 5px;
|
border: {border_a} {color_h};
|
||||||
padding-bottom: 3px;
|
}}
|
||||||
padding-left: 5px;
|
QPushButton[checked="true"]:focus {{
|
||||||
padding-right: 3px;
|
border: {border_b} {color_f};
|
||||||
}
|
}}
|
||||||
QPushButton[checked="true"] {
|
|
||||||
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #4a90e2, stop:1 #3a7ad2);
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #2a6ac2;
|
|
||||||
border-top-color: #5aa0f2;
|
|
||||||
border-left-color: #5aa0f2;
|
|
||||||
}
|
|
||||||
QPushButton[checked="true"]:focus {
|
|
||||||
border: 2px solid #6aa3f5;
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН), ЛЭЙБЛОВ, КНОПОК
|
# ГЛОБАЛЬНЫЙ СТИЛЬ ДЛЯ ОКНА (ФОН), ЛЭЙБЛОВ, КНОПОК
|
||||||
@@ -660,6 +647,9 @@ PLAY_BUTTON_STYLE = f"""
|
|||||||
QPushButton:pressed {{
|
QPushButton:pressed {{
|
||||||
background: {color_a};
|
background: {color_a};
|
||||||
}}
|
}}
|
||||||
|
QPushButton:focus {{
|
||||||
|
background: {color_a};
|
||||||
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# СТИЛЬ КНОПКИ "ОБЗОР..." В ДИАЛОГЕ "ДОБАВИТЬ ИГРУ"
|
# СТИЛЬ КНОПКИ "ОБЗОР..." В ДИАЛОГЕ "ДОБАВИТЬ ИГРУ"
|
||||||
@@ -968,9 +958,8 @@ SETTINGS_CHECKBOX_STYLE = f"""
|
|||||||
|
|
||||||
WINETRICKS_TAB_STYLE = f"""
|
WINETRICKS_TAB_STYLE = f"""
|
||||||
QTabWidget::pane {{
|
QTabWidget::pane {{
|
||||||
border: 1px solid {color_d};
|
border-top: 1px solid {color_c};
|
||||||
background: {color_b};
|
background: {color_h};
|
||||||
border-radius: {border_radius_a};
|
|
||||||
}}
|
}}
|
||||||
QTabBar::tab {{
|
QTabBar::tab {{
|
||||||
background: {color_c};
|
background: {color_c};
|
||||||
@@ -985,15 +974,118 @@ QTabBar::tab:selected {{
|
|||||||
color: {color_f};
|
color: {color_f};
|
||||||
}}
|
}}
|
||||||
QTabBar::tab:hover {{
|
QTabBar::tab:hover {{
|
||||||
background: {color_e};
|
background: {color_a};
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
WINETRICKS_TABBLE_STYLE = f"""
|
WINETRICKS_TABBLE_STYLE = f"""
|
||||||
QTableWidget {{
|
QComboBox {{
|
||||||
background: {color_c};
|
background: {color_c};
|
||||||
|
border: {border_c} {color_g};
|
||||||
|
border-radius: {border_radius_a};
|
||||||
|
padding-left: 12px;
|
||||||
color: {color_f};
|
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};
|
alternate-background-color: {color_d};
|
||||||
border: {border_a};
|
border: {border_a};
|
||||||
border-radius: {border_radius_a};
|
border-radius: {border_radius_a};
|
||||||
@@ -1009,39 +1101,93 @@ QHeaderView::section {{
|
|||||||
}}
|
}}
|
||||||
QTableWidget::item {{
|
QTableWidget::item {{
|
||||||
padding: 8px;
|
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};
|
background: {color_a};
|
||||||
color: {color_f};
|
color: {color_f};
|
||||||
|
selection-background-color: {color_a};
|
||||||
}}
|
}}
|
||||||
QTableWidget::item:hover {{
|
QTableWidget::item:hover {{
|
||||||
background: {color_e};
|
background: {color_h};
|
||||||
}}
|
}}
|
||||||
QTableWidget::indicator {{
|
QTableWidget::indicator {{
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
border: {border_b} {color_a};
|
border: {border_c} {color_h};
|
||||||
border-radius: {border_radius_a};
|
border-radius: {border_radius_a};
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: {color_b};
|
||||||
}}
|
}}
|
||||||
QTableWidget::indicator:unchecked {{
|
QTableWidget::indicator:unchecked {{
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
image: none;
|
image: none;
|
||||||
}}
|
}}
|
||||||
QTableWidget::indicator:checked {{
|
QTableWidget::indicator:checked {{
|
||||||
background: {color_a};
|
background: {color_b};
|
||||||
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
|
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 {{
|
QTableWidget::indicator:hover {{
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border: {border_b} {color_a};
|
|
||||||
}}
|
|
||||||
QTableWidget::indicator:focus {{
|
|
||||||
border: {border_c} {color_a};
|
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;
|
||||||
|
}}
|
||||||
|
QScrollBar:horizontal {{
|
||||||
|
height: 10px;
|
||||||
|
border: {border_a};
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(20, 20, 20, 0.30);
|
||||||
|
}}
|
||||||
|
QScrollBar::handle:horizontal {{
|
||||||
|
background: #bebebe;
|
||||||
|
border: {border_a};
|
||||||
|
border-radius: 5px;
|
||||||
|
}}
|
||||||
|
QScrollBar::add-line:horizontal {{
|
||||||
|
border: {border_a};
|
||||||
|
background: none;
|
||||||
|
}}
|
||||||
|
QScrollBar::sub-line:horizontal {{
|
||||||
|
border: {border_a};
|
||||||
|
background: none;
|
||||||
|
}}
|
||||||
|
QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal {{
|
||||||
|
border: {border_a};
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
background: none;
|
||||||
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
WINETRICKS_LOG_STYLE = f"""
|
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,
|
from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout,
|
||||||
QSizePolicy, QWidget, QLineEdit)
|
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.keyboard_layouts import keyboard_layouts
|
||||||
from portprotonqt.theme_manager import ThemeManager
|
from portprotonqt.theme_manager import ThemeManager
|
||||||
from portprotonqt.config_utils import read_theme_from_config
|
from portprotonqt.config_utils import read_theme_from_config
|
||||||
@@ -43,6 +44,18 @@ class VirtualKeyboard(QFrame):
|
|||||||
self.margins = 10
|
self.margins = 10
|
||||||
self.num_cols = 14
|
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.initUI()
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
@@ -119,14 +132,44 @@ class VirtualKeyboard(QFrame):
|
|||||||
self.buttons: dict[str, QPushButton] = {}
|
self.buttons: dict[str, QPushButton] = {}
|
||||||
self.update_keyboard()
|
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):
|
def update_keyboard(self):
|
||||||
coords = self._save_focused_coords()
|
coords = self._save_focused_coords()
|
||||||
|
|
||||||
# Очищаем предыдущие кнопки
|
# Очищаем предыдущие кнопки
|
||||||
while self.keyboard_layout.count():
|
while self.keyboard_layout.count():
|
||||||
item = self.keyboard_layout.takeAt(0)
|
item = self.keyboard_layout.takeAt(0)
|
||||||
if item.widget():
|
if item:
|
||||||
item.widget().deleteLater()
|
widget = item.widget()
|
||||||
|
if widget:
|
||||||
|
widget.deleteLater()
|
||||||
|
|
||||||
fixed_w = self.button_width
|
fixed_w = self.button_width
|
||||||
fixed_h = self.button_height
|
fixed_h = self.button_height
|
||||||
@@ -151,6 +194,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
button.setCheckable(True)
|
button.setCheckable(True)
|
||||||
button.setChecked(self.shift_pressed)
|
button.setChecked(self.shift_pressed)
|
||||||
button.clicked.connect(lambda checked: self.on_shift_click(checked))
|
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:
|
else:
|
||||||
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
|
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
|
||||||
|
|
||||||
@@ -163,6 +209,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
shift.setCheckable(True)
|
shift.setCheckable(True)
|
||||||
shift.setChecked(self.shift_pressed)
|
shift.setChecked(self.shift_pressed)
|
||||||
shift.clicked.connect(lambda checked: self.on_shift_click(checked))
|
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)
|
self.keyboard_layout.addWidget(shift, 3, 11, 1, 3)
|
||||||
|
|
||||||
button = QPushButton('CAPS')
|
button = QPushButton('CAPS')
|
||||||
@@ -179,6 +228,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
backspace.setFixedSize(fixed_w, fixed_h)
|
backspace.setFixedSize(fixed_w, fixed_h)
|
||||||
backspace.pressed.connect(self.on_backspace_pressed)
|
backspace.pressed.connect(self.on_backspace_pressed)
|
||||||
backspace.released.connect(self.stop_backspace_repeat)
|
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)
|
self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1)
|
||||||
|
|
||||||
enter = QPushButton('Enter')
|
enter = QPushButton('Enter')
|
||||||
@@ -189,6 +241,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
lang = QPushButton('🌐')
|
lang = QPushButton('🌐')
|
||||||
lang.setFixedSize(fixed_w, fixed_h)
|
lang.setFixedSize(fixed_w, fixed_h)
|
||||||
lang.clicked.connect(self.on_lang_click)
|
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)
|
self.keyboard_layout.addWidget(lang, 4, 0, 1, 1)
|
||||||
|
|
||||||
clear = QPushButton('Clear')
|
clear = QPushButton('Clear')
|
||||||
@@ -219,13 +274,18 @@ class VirtualKeyboard(QFrame):
|
|||||||
hide_button = QPushButton('Hide')
|
hide_button = QPushButton('Hide')
|
||||||
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
|
||||||
hide_button.clicked.connect(self.hide)
|
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)
|
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
|
||||||
|
|
||||||
if coords:
|
if coords:
|
||||||
row, col = coords
|
row, col = coords
|
||||||
item = self.keyboard_layout.itemAtPosition(row, col)
|
item = self.keyboard_layout.itemAtPosition(row, col)
|
||||||
if item and item.widget():
|
if item:
|
||||||
item.widget().setFocus()
|
widget = item.widget()
|
||||||
|
if widget:
|
||||||
|
widget.setFocus()
|
||||||
|
|
||||||
def up_key(self):
|
def up_key(self):
|
||||||
"""Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима"""
|
"""Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима"""
|
||||||
@@ -336,7 +396,8 @@ class VirtualKeyboard(QFrame):
|
|||||||
if self.current_input_widget is not None:
|
if self.current_input_widget is not None:
|
||||||
self.current_input_widget.insert('\t')
|
self.current_input_widget.insert('\t')
|
||||||
self.keyPressed.emit('Tab')
|
self.keyPressed.emit('Tab')
|
||||||
self.current_input_widget.setFocus()
|
if self.current_input_widget:
|
||||||
|
self.current_input_widget.setFocus()
|
||||||
self.highlight_cursor_position()
|
self.highlight_cursor_position()
|
||||||
|
|
||||||
def on_caps_click(self):
|
def on_caps_click(self):
|
||||||
@@ -474,8 +535,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
search_col = current_col + col_span
|
search_col = current_col + col_span
|
||||||
while search_col < num_cols:
|
while search_col < num_cols:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -488,8 +550,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
# Ищем первую кнопку в этой строке
|
# Ищем первую кнопку в этой строке
|
||||||
while search_col < num_cols:
|
while search_col < num_cols:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -501,8 +564,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
search_col = current_col - 1
|
search_col = current_col - 1
|
||||||
while search_col >= 0:
|
while search_col >= 0:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -515,8 +579,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
# Ищем последнюю кнопку в этой строке
|
# Ищем последнюю кнопку в этой строке
|
||||||
while search_col >= 0:
|
while search_col >= 0:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -528,8 +593,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
search_row = current_row + row_span
|
search_row = current_row + row_span
|
||||||
while search_row < num_rows:
|
while search_row < num_rows:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -542,8 +608,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
# Ищем первую кнопку в этом столбце
|
# Ищем первую кнопку в этом столбце
|
||||||
while search_row < num_rows:
|
while search_row < num_rows:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -555,8 +622,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
search_row = current_row - 1
|
search_row = current_row - 1
|
||||||
while search_row >= 0:
|
while search_row >= 0:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -569,8 +637,9 @@ class VirtualKeyboard(QFrame):
|
|||||||
# Ищем последнюю кнопку в этом столбце
|
# Ищем последнюю кнопку в этом столбце
|
||||||
while search_row >= 0:
|
while search_row >= 0:
|
||||||
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
item = self.keyboard_layout.itemAtPosition(search_row, search_col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
next_button = cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
next_button = cast(QPushButton, widget)
|
||||||
next_button.setFocus()
|
next_button.setFocus()
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
@@ -581,6 +650,7 @@ class VirtualKeyboard(QFrame):
|
|||||||
for row in range(self.keyboard_layout.rowCount()):
|
for row in range(self.keyboard_layout.rowCount()):
|
||||||
for col in range(self.keyboard_layout.columnCount()):
|
for col in range(self.keyboard_layout.columnCount()):
|
||||||
item = self.keyboard_layout.itemAtPosition(row, col)
|
item = self.keyboard_layout.itemAtPosition(row, col)
|
||||||
if item and item.widget() and item.widget().isEnabled():
|
widget = item.widget() if item else None
|
||||||
return cast(QPushButton, item.widget())
|
if widget and widget.isEnabled():
|
||||||
|
return cast(QPushButton, widget)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "portprotonqt"
|
name = "portprotonqt"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
description = "A project to rewrite PortProton (PortWINE) using PySide"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = { text = "GPL-3.0" }
|
license = { text = "GPL-3.0" }
|
||||||
@@ -31,11 +31,12 @@ dependencies = [
|
|||||||
"evdev>=1.9.2",
|
"evdev>=1.9.2",
|
||||||
"icoextract>=0.2.0",
|
"icoextract>=0.2.0",
|
||||||
"numpy>=2.2.4",
|
"numpy>=2.2.4",
|
||||||
"orjson>=3.11.3",
|
"orjson>=3.11.4",
|
||||||
"pillow>=11.3.0",
|
"pillow>=12.0.0",
|
||||||
"psutil>=7.1.0",
|
"psutil>=7.1.3",
|
||||||
"pyside6==6.9.1",
|
"pyside6>=6.10.1",
|
||||||
"pyudev>=0.24.3",
|
"pyudev>=0.24.4",
|
||||||
|
"rapidfuzz>=3.14.3",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"tqdm>=4.67.1",
|
"tqdm>=4.67.1",
|
||||||
"vdf>=3.4",
|
"vdf>=3.4",
|
||||||
@@ -103,7 +104,7 @@ ignore = [
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"pre-commit>=4.3.0",
|
"pre-commit>=4.5.0",
|
||||||
"pyaspeller>=2.0.2",
|
"pyaspeller>=2.0.2",
|
||||||
"pyright>=1.1.406",
|
"pyright>=1.1.407",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"pre-commit": {
|
"pre-commit": {
|
||||||
"enabled": true
|
"enabled": false
|
||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"groupName": "Python dependencies"
|
"groupName": "Python dependencies"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["numpy", "setuptools", "python", "pyside6"],
|
"matchPackageNames": ["numpy", "setuptools", "python"],
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
"description": "Disabled due to Python 3.10 incompatibility with numpy>=2.3.2 (requires Python>=3.11)"
|
||||||
},
|
},
|
||||||
|
|||||||