101 Commits

Author SHA1 Message Date
Renovate Bot
043da2cf5d chore(deps): update https://gitea.com/actions/checkout action to v6 2025-11-23 00:01:25 +00:00
2fa10e7db3 feat(settings): added tooltip to desc
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-22 23:34:11 +05:00
b1b9706272 chore(input_manager): clean dialogs code
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-22 22:36:37 +05:00
9c11d33c0a chore(setting): add human readeble value to PW_VULKAN_USE
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-22 19:52:36 +05:00
173e1cb88e fix(settings): fix PW_WINE_USE_LIST
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-22 11:53:24 +05:00
30606c7ec1 Revert "fix: eliminate blocking calls causing startup freezes and UI hangs"
This reverts commit b2a1046f9d.
2025-11-22 11:21:25 +05:00
873e8b050e chore(settings): added disable style to comboboxes
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-22 00:28:15 +05:00
59dad21945 chore(settings): adjust virtual keyboard button width (40 → 50)
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-22 00:16:14 +05:00
b0c4e943ae feat(settings): added blocked style to advanced tab
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-22 00:05:46 +05:00
19e01bba17 Fix: normalize disabled value for PW_AMD_VULKAN_USE
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-21 23:43:36 +05:00
836e6cdd36 feat(settings): added initial gamepad navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-21 00:08:02 +05:00
b2a1046f9d fix: eliminate blocking calls causing startup freezes and UI hangs
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-20 10:55:35 +05:00
80a2c06b5a feat: added refresh button
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 17:07:58 +05:00
f0a4ace735 perf: add config and icon caching to reduce I/O and improve UI responsiveness
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 16:57:40 +05:00
7dfaee6831 feat(settings): added proton, 3d_api and prefixes settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 13:35:35 +05:00
5481cd80d7 chore: added null pixmaps check
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 12:25:01 +05:00
a016cfa810 chore: convert list to set for optimize
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 12:13:42 +05:00
8fc097ccaf chore: remove broken styles
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-16 12:03:18 +05:00
ad3eeb6e06 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:39:05 +05:00
92631cd2c6 chore: separate settings list to new module
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:39:03 +05:00
4477679f2d chore: replace emulataion buttons to xbox + start
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:38:07 +05:00
Renovate Bot
b6644eeee5 fix(deps): update dependency pillow to v12 2025-11-15 21:38:07 +05:00
Renovate Bot
2e921226c4 chore(deps): update https://gitea.com/actions/setup-python action to v6 2025-11-15 21:38:06 +05:00
Renovate Bot
4fc1ea73d3 chore(deps): update https://gitea.com/actions/setup-node action to v6 2025-11-15 21:38:06 +05:00
Renovate Bot
3c15cbe495 chore(deps): update archlinux:base-devel docker digest to 943bdad 2025-11-15 21:38:06 +05:00
fed6aafed5 feat: trigger emulation by Xbox + B
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-15 21:38:06 +05:00
2e8be13437 WINETRICKS_TABBLE_STYLE more fixes 2025-11-15 16:22:59 +07:00
ea272c29b6 WINETRICKS_TABBLE_STYLE reworked 2025-11-13 15:43:13 +07:00
17262f6c9f Play Button & Settings Button in row 2025-11-07 12:17:36 +07:00
e07f3f06bc chore(build): return QtSvg to appimage
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-04 12:28:09 +05:00
16a3f4e09a chore(build): added udev rule to allow create virtual devices
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-04 11:14:17 +05:00
a448ba29b0 feat(input_manager): added mouse emulation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-03 12:34:27 +05:00
06e55db54d feat(settings): update styles
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-02 16:05:22 +05:00
5fce23f261 chore: disable pre-commit auto update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-02 15:31:01 +05:00
Renovate Bot
96ad40d625 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.3 2025-11-02 00:01:26 +00:00
Gitea Actions
a30f6f2e74 chore: update steam apps list 2025-11-01T00:01:57Z 2025-11-01 00:01:58 +00:00
0231073b19 feat(settings): added advanced
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-30 16:27:45 +05:00
dec24429f5 chore: separate start.sh to new function
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-28 15:34:01 +05:00
4a758f3b3c chore: use flatpak run for flatpak not start.sh
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 23:13:48 +05:00
0853dd1579 chore: use CLI for clear pfx
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 22:36:14 +05:00
bbb87c0455 feat(settings): added icon to button thanks to @Dervart
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 12:17:00 +05:00
b32a71a125 feat(settings): block settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-27 12:16:54 +05:00
Renovate Bot
bddf9f850a chore(deps): update pre-commit hook astral-sh/uv-pre-commit to v0.9.5 2025-10-26 00:01:29 +00:00
Renovate Bot
a9c3cfa167 chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.2 2025-10-26 00:01:19 +00:00
7675bc4cdc feat: added initial exe settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-26 00:12:00 +05:00
ffa203f019 feat: restore instance from tray
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-22 15:46:57 +05:00
3eed25ecee feat: update grid on update_favorite_icon
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-21 20:41:21 +05:00
3736bb279e feat: use SGDB for cover too
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-20 13:07:09 +05:00
Renovate Bot
b59ee5ae8e chore(deps): update archlinux:base-devel docker digest to 87a967f 2025-10-19 12:07:20 +00:00
33176590fd feat: Make autoinstall games loading asynchronous with caching
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-19 17:03:26 +05:00
8046065929 refactor(gamepad): replace busy-wait with threading.Event for monitor readiness
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-19 11:00:22 +05:00
Renovate Bot
fbad5add6c chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.1 2025-10-19 00:01:23 +00:00
438e9737ea chore(release): drop sha256 sums
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 21:07:37 +05:00
2d39a4c740 fix: fix CloseEvent on native package
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 21:06:23 +05:00
567203b0b0 chore: bump to 0.1.8
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 18:22:32 +05:00
502cbc5030 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 18:20:50 +05:00
9b61215152 chore(theme): update screenshots
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 18:17:47 +05:00
10d3fe8ab4 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 13:40:56 +05:00
a568ad9ef8 fix(add_game_dialog): prevent overwriting manually entered game name
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 13:09:58 +05:00
f074843fc8 fix: prevent udev monitor hang by using non-blocking poll with timeout
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 12:53:47 +05:00
4ab078b93e fix: sync card_width between GameLibraryManager and MainWindow to prevent config overwrite
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 12:17:17 +05:00
7df6ad3b80 feat(autoinstalls): added slider
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-17 13:55:17 +05:00
464ad0fe9c chore: optimize and clean code
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-17 13:09:02 +05:00
cde92885d4 feat(virtual_keybord): added gamepad hint
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-17 00:04:47 +05:00
120c7b319c fix: improve gamepad detection using udev ID_INPUT_JOYSTICK property 2025-10-16 23:20:48 +05:00
596aed0077 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 14:54:30 +05:00
6fc6cb1e02 feat: added minimize to tray
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 14:53:08 +05:00
186e28a19b fix(gamepad): resolve MonitorObserver blocking issue causing application hang
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 14:43:49 +05:00
28e4d1e77c Revert "chore: broke autorelease for tasting purpose"
This reverts commit fff1f888c4.
2025-10-16 14:11:36 +05:00
fff1f888c4 chore: broke autorelease for tasting purpose
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 12:48:21 +05:00
fdd5a0a3d5 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 10:44:30 +05:00
792e52d981 feat(dialogs): added controller hints
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-16 10:39:24 +05:00
84d5e46a74 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 22:53:08 +05:00
4bc764d568 partially revert b1047ba18e
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 22:31:35 +05:00
9a18aa037e feat(autoinstall): no restart on autoinstall finished
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 21:58:40 +05:00
ed62d2d1c4 fix: resolve lambda variable capture issue in switchTab method
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 21:47:14 +05:00
accc9b18b6 chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 15:31:56 +05:00
82249d7eab feat(settings): Added Gamepad type settings
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 15:30:31 +05:00
476c896940 chore(TODO): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-15 12:44:01 +05:00
b1047ba18e fix: fix card overlap on display_filter change
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-13 12:14:54 +05:00
987199d8e6 chore(release): enable node experimental-fetch
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-13 11:52:43 +05:00
Renovate Bot
ef1acd4581 chore(deps): update archlinux:base-devel docker digest to 06ab929 2025-10-12 17:46:27 +00:00
96f884904c chore: bump ver to v0.1.7
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:33:56 +05:00
b856a2afae chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:33:12 +05:00
55ef0030e6 feat: added version and commit on WindowTitle
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:31:23 +05:00
8aaeaa4824 chore(localization): add localization for auto-install progress status message
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:14:06 +05:00
f55372b480 fix(autoinstall): fix scrollbar sticking to the right edge
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 17:10:44 +05:00
4d6f32f053 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 15:25:04 +05:00
a2f5141b20 chore localization update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 15:21:14 +05:00
e3cb2857e7 fix(pyright): fix pyright errors
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 15:14:02 +05:00
efe8a35832 feat(autoinstall): rework gamepad navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 14:57:43 +05:00
61fae97dad fix(autoinstall): fix virtual keyboard open
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 14:45:52 +05:00
5442100f64 feat: use GameCard on autonstall tab
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 13:56:18 +05:00
2d6ef84798 chore: rename metadata to use pw_create_unique_exe
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 12:14:31 +05:00
Renovate Bot
f4aee15b5d chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.14.0 2025-10-12 00:01:35 +00:00
87a65108a5 feat(autoinstall): added covers
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-12 00:48:09 +05:00
bb617708ac feat: initial add of autoinstall tab
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-11 19:19:47 +05:00
1cf332cd87 feat(winetab): added progress bar
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-11 13:24:58 +05:00
577ad4d3a3 feat: adapt WineTab to new cli
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-10 23:07:48 +05:00
ef3f2d6e96 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 21:01:30 +05:00
657d7728a6 fix(gamepad): exit fullscreen on disconnect only if auto-fullscreen enabled and fullscreen disabled
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-09 20:59:51 +05:00
79 changed files with 17995 additions and 2623 deletions

View File

@@ -12,7 +12,7 @@ jobs:
name: Build AppImage
runs-on: ubuntu-22.04
steps:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Install required dependencies
run: |
@@ -62,7 +62,7 @@ jobs:
- name: Install build dependencies
run: |
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
python3-build pyproject-rpm-macros python3-setuptools \
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment
@@ -73,7 +73,7 @@ jobs:
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
- name: Checkout repo
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Copy fedora.spec
run: |
@@ -94,7 +94,7 @@ jobs:
name: Build Arch Package
runs-on: ubuntu-22.04
container:
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
volumes:
- /usr:/usr-host
- /opt:/opt-host
@@ -134,7 +134,7 @@ jobs:
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4

View File

@@ -8,7 +8,7 @@ on:
env:
# Common version, will be used for tagging the release
VERSION: 0.1.6
VERSION: 0.1.8
PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt"
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
@@ -119,7 +119,7 @@ jobs:
- name: Install build dependencies
run: |
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
python3-build pyproject-rpm-macros python3-setuptools \
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment
@@ -180,10 +180,12 @@ jobs:
- name: Release
uses: https://gitea.com/actions/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
body_path: changelog.txt
token: ${{ env.GITEA_TOKEN }}
tag_name: v${{ env.VERSION }}
prerelease: true
files: release/**/*
sha256sum: true
sha256sum: false

View File

@@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Set up Python
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: https://gitea.com/actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
with:
python-version-file: "pyproject.toml"

View File

@@ -18,7 +18,7 @@ jobs:
fedora: ${{ steps.check.outputs.fedora }}
arch: ${{ steps.check.outputs.arch }}
steps:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
with:
fetch-depth: 0
@@ -63,7 +63,7 @@ jobs:
needs: changes
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
steps:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Install required dependencies
run: |
@@ -115,7 +115,7 @@ jobs:
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
- name: Checkout repo
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Copy fedora-git.spec
run: |
@@ -138,7 +138,7 @@ jobs:
needs: changes
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
container:
image: archlinux:base-devel@sha256:b3809917ab5a7840d42237f5f92d92660cd036bd75ae343e7825e6a24401f166
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
volumes:
- /usr:/usr-host
- /opt:/opt-host
@@ -178,7 +178,7 @@ jobs:
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4

View File

@@ -20,10 +20,10 @@ jobs:
name: Check code
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
node-version: 20

View File

@@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Set up Python
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
uses: https://gitea.com/actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
with:
python-version-file: "pyproject.toml"

View File

@@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
steps:
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Set up Node.js
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
uses: https://gitea.com/actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
node-version: 20

View File

@@ -11,12 +11,12 @@ repos:
- id: check-yaml
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.8.22
rev: 0.9.5
hooks:
- id: uv-lock
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.2
rev: v0.14.3
hooks:
- id: ruff-check

View File

@@ -3,14 +3,42 @@
Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [0.1.8] - 2025-10-18
### Added
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
- В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет
- К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада
- Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы)
### Changed
- При завершении автоустановки приложение больше не перезапускается
- Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название
- Обновлены и дополнены скриншоты темы
### Fixed
- Исправлено наложение карточек при смене фильтра игр
- Исправлена невозможность запуска приложения без подключёного геймпада
- Исправлена невозможность установки компонентов Winetricks через геймпад
- Ресиверы и виртуальные устройства больше не считаются за геймпад
### Contributors
- @Vector_null
---
## [0.1.7] - 2025-10-12
### Added
- Возможность скроллинга библиотеки мышью или пальцем
- Импорт и экспорт бекапа префикса
- Диалог для управление Winetricks
- Кнопки для удаления префикса, wine или proton
- Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке
- Все настройки Wine с оригинального PortProton
- Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке и автоустановках
- Вкладка автоустановок
- В заголовке окна теперь отображается версия приложения и хеш коммита если запуск идёт с гита
### Changed
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
@@ -22,8 +50,12 @@
- Исправлено зависание при поиске игр
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
- Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада
- Исправлен выход из полноэкранного режима при отключении геймпада подключённого по USB даже если настройка "Режим полноэкранного отображения приложения при подключении геймпада" выключена
- При сохранении настроек теперь не меняется размер окна
### Contributors
- @wmigor (Igor Akulov)
- @Vector_null
---

15
TODO.md
View File

@@ -1,6 +1,6 @@
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
- [X] Добавить возможность управления с геймпада
- [ ] Добавить возможность управления с тачскрина
- [X] Добавить возможность управления с тачскрина (Формально и так есть)
- [X] Добавить возможность управления с мыши и клавиатуры
- [X] Добавить систему тем [Документация](documentation/theme_guide)
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
@@ -11,18 +11,18 @@
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
- [X] Получать описания и названия игр из базы данных Steam
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
- [X] Получать обложки для игр из CDN Steam
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
- [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
- [X] Избавиться от вызовов yad
- [X] Реализовать собственный системный трей вместо использования трея PortProton
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
- [X] Добавить экранную клавиатуру в поиск
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
- [X] Добавить индикацию запуска приложения
- [X] Достигнуть паритета функциональности с Ingame
- [ ] Достигнуть паритета функциональности с PortProton
- [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов)
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
- [X] Добавить переводы в переопределения
@@ -49,7 +49,7 @@
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
- [X] Добавить систему избранного для карточек
- [X] Заменить все `print` на `logging`
- [ ] Привести все логи к единому языку
- [X] Привести все логи к единому языку
- [X] Уменьшить количество подстановок в переводах
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
@@ -62,7 +62,6 @@
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
- [ ] Доделать светлую тему
- [ ] Добавить подсказки к управлению с геймпада
- [X] Добавить подсказки к управлению с геймпада
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры

View File

@@ -6,11 +6,12 @@ script:
- uv pip install --no-cache-dir ../
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
- cp -r share AppDir/usr
- cp -r lib AppDir/usr
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetwork*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
- shopt -s extglob
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Network*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
AppDir:
path: ./AppDir
after_bundle:
@@ -36,7 +37,7 @@ AppDir:
id: ru.linux_gaming.PortProtonQt
name: PortProtonQt
icon: ru.linux_gaming.PortProtonQt
version: 0.1.6
version: 0.1.8
exec: usr/bin/python3
exec_args: "-m portprotonqt.app $@"
apt:

View File

@@ -1,5 +1,5 @@
pkgname=portprotonqt
pkgver=0.1.6
pkgver=0.1.8
pkgrel=1
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
arch=('any')
@@ -20,4 +20,5 @@ package() {
cd "$srcdir/PortProtonQt"
python -m installer --destdir="$pkgdir" dist/*.whl
cp -r build-aux/share "$pkgdir/usr/"
cp -r build-aux/lib "$pkgdir/usr/"
}

View File

@@ -25,4 +25,5 @@ package() {
cd "$srcdir/PortProtonQt"
python -m installer --destdir="$pkgdir" dist/*.whl
cp -r build-aux/share "$pkgdir/usr/"
cp -r build-aux/lib "$pkgdir/usr/"
}

View File

@@ -22,6 +22,7 @@ BuildRequires: python3-build
BuildRequires: pyproject-rpm-macros
BuildRequires: python3dist(setuptools)
BuildRequires: git
BuildRequires: systemd-rpm-macros
%description
%{summary}
@@ -69,11 +70,13 @@ cd %{oname}
%pyproject_install
%pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
%{_bindir}/%{pypi_name}
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_udevrulesdir}/60-portprotonqt.rules
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%{bash_completions_dir}/portprotonqt

View File

@@ -1,5 +1,5 @@
%global pypi_name portprotonqt
%global pypi_version 0.1.6
%global pypi_version 0.1.8
%global oname PortProtonQt
%global _python_no_extras_requires 1
@@ -19,6 +19,7 @@ BuildRequires: python3-build
BuildRequires: pyproject-rpm-macros
BuildRequires: python3dist(setuptools)
BuildRequires: git
BuildRequires: systemd-rpm-macros
%description
%{summary}
@@ -68,11 +69,13 @@ cd %{oname}
%pyproject_install
%pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name} -f %{pyproject_files}
%{_bindir}/%{pypi_name}
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
%{_udevrulesdir}/60-portprotonqt.rules
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
%{bash_completions_dir}/portprotonqt

View File

@@ -0,0 +1 @@
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"

View File

@@ -1021,7 +1021,7 @@
},
{
"normalized_name": "farlight 84",
"status": "Supported"
"status": "Denied"
},
{
"normalized_name": "riders republic",
@@ -1436,8 +1436,8 @@
"status": "Broken"
},
{
"normalized_name": "blue protocol",
"status": "Broken"
"normalized_name": "blue protocol star resonance",
"status": "Running"
},
{
"normalized_name": "dark and darker",

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,108 @@
[
{
"normalized_title": "split/second",
"slug": "split-second"
},
{
"normalized_title": "warzone 2100",
"slug": "warzone-2100"
},
{
"normalized_title": "foundation",
"slug": "foundation"
},
{
"normalized_title": "земский собор [демо]",
"slug": "zemskij-sobor-demo"
},
{
"normalized_title": "crusader kings 3",
"slug": "crusader-kings-3"
},
{
"normalized_title": "nadir a grimdark deck builder",
"slug": "nadir-a-grimdark-deck-builder"
},
{
"normalized_title": "oriental empires",
"slug": "oriental-empires"
},
{
"normalized_title": "vampire the masquerade bloodlines 2",
"slug": "vampire-the-masquerade-bloodlines-2"
},
{
"normalized_title": "escape from duckov",
"slug": "escape-from-duckov"
},
{
"normalized_title": "xiii",
"slug": "xiii"
},
{
"normalized_title": "saints row 2",
"slug": "saints-row-2"
},
{
"normalized_title": "frozenheim",
"slug": "frozenheim"
},
{
"normalized_title": "saints row (2022)",
"slug": "saints-row-2022"
},
{
"normalized_title": "iron harvest",
"slug": "iron-harvest"
},
{
"normalized_title": "tom clancy's splinter cell blacklist",
"slug": "tom-clancys-splinter-cell-blacklist"
},
{
"normalized_title": "painkiller overdose",
"slug": "painkiller-overdose"
},
{
"normalized_title": "ancestors legacy",
"slug": "ancestors-legacy"
},
{
"normalized_title": "bye sweet carole",
"slug": "bye-sweet-carole"
},
{
"normalized_title": "painkiller black",
"slug": "painkiller-black-edition"
},
{
"normalized_title": "hogwarts legacy",
"slug": "hogwarts-legacy"
},
{
"normalized_title": "active matter",
"slug": "active-matter"
},
{
"normalized_title": "tom clancy's splinter cell",
"slug": "tom-clancys-splinter-cell"
},
{
"normalized_title": "sniper ghost warrior",
"slug": "sniper-ghost-warrior"
},
{
"normalized_title": "fate undiscovered realms",
"slug": "fate-undiscovered-realms"
},
{
"normalized_title": "dying light the beast deluxe",
"slug": "dying-light-the-beast-deluxe-edition"
},
{
"normalized_title": "spellforce platinum",
"slug": "spellforce-platinum-edition"
},
{
"normalized_title": "dirt rally 2.0 game of the year",
"slug": "dirt-rally-2-0-game-of-the-year-edition"
@@ -271,10 +375,6 @@
"normalized_title": "steins;gate the distant valhalla",
"slug": "steins-gate-the-distant-valhalla"
},
{
"normalized_title": "hogwarts legacy",
"slug": "hogwarts-legacy"
},
{
"normalized_title": "osu!",
"slug": "osu"

Binary file not shown.

View File

@@ -17,17 +17,31 @@ import json
class PySide6DependencyAnalyzer:
def __init__(self):
def __init__(self, project_root: Path = None):
# Системные библиотеки, которые нужно всегда оставлять
self.system_libs = {
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
'libQt6Svg'
}
self.critical_modules = {
'QtSvg',
}
self.real_dependencies = {}
self.used_modules_code = set()
self.used_modules_ldd = set()
self.all_required_modules = set()
# Определяем корень проекта
if project_root is None:
# Корень проекта - две директории выше от скрипта
self.project_root = Path(__file__).parent.parent
else:
self.project_root = project_root
self.venv_path = self.project_root / ".venv"
self.build_path = self.project_root / "build-aux"
def find_python_files(self, directory: Path) -> List[Path]:
"""Находит все Python файлы в директории"""
@@ -44,24 +58,61 @@ class PySide6DependencyAnalyzer:
"""Находит все PySide6 библиотеки (.so файлы)"""
libs = {}
# Поиск в единственной локации
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
print(f"Поиск PySide6 библиотек в: {search_path}")
# Ищем venv в корне проекта
venv_candidates = [
self.venv_path, # .venv
self.project_root / "venv",
self.project_root / ".virtualenv",
]
if search_path.exists():
# Ищем .so файлы модулей
for so_file in search_path.glob("Qt*.*.so"):
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
if module_name.startswith('Qt'):
libs[module_name] = so_file
pyside6_path = None
# Также ищем в подпапках
for subdir in search_path.iterdir():
if subdir.is_dir() and subdir.name.startswith('Qt'):
for so_file in subdir.glob("*.so*"):
if 'Qt' in so_file.name:
libs[subdir.name] = so_file
break
# Пробуем найти PySide6 в venv
for venv in venv_candidates:
if venv.exists():
# Ищем Python версию
lib_path = venv / "lib"
if lib_path.exists():
for python_dir in lib_path.iterdir():
if python_dir.name.startswith('python'):
candidate = python_dir / "site-packages" / "PySide6"
if candidate.exists():
pyside6_path = candidate
print(f"Найден PySide6 в: {candidate}")
break
if pyside6_path:
break
if not pyside6_path:
print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...")
# Если не нашли в venv, пробуем в AppDir
if base_path:
appdir_candidate = base_path / "AppDir/usr/local/lib"
if appdir_candidate.exists():
for python_dir in appdir_candidate.iterdir():
if python_dir.name.startswith('python'):
candidate = python_dir / "dist-packages" / "PySide6"
if candidate.exists():
pyside6_path = candidate
print(f"Найден PySide6 в AppDir: {candidate}")
break
if not pyside6_path:
return libs
# Ищем .so файлы модулей
for so_file in pyside6_path.glob("Qt*.*.so"):
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
if module_name.startswith('Qt'):
libs[module_name] = so_file
# Также ищем в подпапках
for subdir in pyside6_path.iterdir():
if subdir.is_dir() and subdir.name.startswith('Qt'):
for so_file in subdir.glob("*.so*"):
if 'Qt' in so_file.name:
libs[subdir.name] = so_file
break
return libs
@@ -257,8 +308,10 @@ class PySide6DependencyAnalyzer:
# Модули для удаления
if removable_modules:
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
if removable_filtered:
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)])
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
# Генерируем команду для удаления нативных библиотек с сохранением нужных
required_libs = set()
@@ -276,39 +329,82 @@ class PySide6DependencyAnalyzer:
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
])
# Заменяем блок очистки в рецепте
import re
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
# Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
# Паттерн: после " - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:"
pattern = r'( - cp -r lib AppDir/usr\n)((?: - (?:rm|shopt).*\n)*?)(?=AppDir:)'
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
match = re.search(pattern, recipe_content)
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
if not match:
print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте")
print("Добавляем команды очистки перед блоком AppDir:")
# Просто вставим команды перед AppDir:
appdir_pos = recipe_content.find('AppDir:')
if appdir_pos != -1:
new_content = (
recipe_content[:appdir_pos] +
'\n'.join(cleanup_lines) + '\n' +
recipe_content[appdir_pos:]
)
return new_content
else:
print("ОШИБКА: Не найден блок AppDir: в рецепте")
return ""
# Создаем замену - группа 1 (cp -r lib) + новые команды очистки
replacement = r'\1' + '\n'.join(cleanup_lines) + '\n'
updated_recipe = re.sub(pattern, replacement, recipe_content, count=1)
return updated_recipe
def main():
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
parser.add_argument('project_path', help='Путь к проекту для анализа')
parser.add_argument('project_path', nargs='?', default='.',
help='Путь к проекту для анализа (по умолчанию: текущая директория)')
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
args = parser.parse_args()
project_path = Path(args.project_path)
project_path = Path(args.project_path).resolve()
if not project_path.exists():
print(f"Ошибка: путь {project_path} не существует")
sys.exit(1)
appdir_path = Path(args.appdir) if args.appdir else None
appdir_path = Path(args.appdir).resolve() if args.appdir else None
if appdir_path and not appdir_path.exists():
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
appdir_path = None
analyzer = PySide6DependencyAnalyzer()
# Определяем корень проекта
# Если запущен из подпапки проекта, ищем корень
project_root = project_path
if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists():
project_root = project_path
else:
# Пытаемся найти корень проекта
current = project_path
while current != current.parent:
if (current / ".git").exists() or (current / "pyproject.toml").exists():
project_root = current
break
current = current.parent
print(f"Корень проекта: {project_root}")
analyzer = PySide6DependencyAnalyzer(project_root=project_root)
# Если указан custom venv путь
if args.venv:
analyzer.venv_path = Path(args.venv).resolve()
print(f"Использую указанный venv: {analyzer.venv_path}")
results = analyzer.analyze_project(project_path, appdir_path)
# Сохраняем в анализатор для генерации команд
@@ -347,13 +443,13 @@ def main():
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
if args.verbose and results['real_dependencies']:
Devlin(f"\nРеальные зависимости (ldd):")
print(f"\nРеальные зависимости (ldd):")
for module, deps in results['real_dependencies'].items():
if deps:
print(f" {module}{', '.join(deps)}")
# Обновляем AppImage рецепт
recipe_path = Path("../build-aux/AppImageBuilder.yml")
recipe_path = analyzer.build_path / "AppImageBuilder.yml"
if recipe_path.exists():
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
if updated_recipe:

View File

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

View File

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

View File

@@ -1,17 +1,46 @@
import sys
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
import os
import subprocess
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo, QTimer, Qt
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon
from PySide6.QtNetwork import QLocalServer, QLocalSocket
from portprotonqt.main_window import MainWindow
from portprotonqt.config_utils import save_fullscreen_config
from portprotonqt.config_utils import (
save_fullscreen_config,
read_fullscreen_config,
get_portproton_start_command
)
from portprotonqt.logger import get_logger, setup_logger
from portprotonqt.cli import parse_args
__app_id__ = "ru.linux_gaming.PortProtonQt"
__app_name__ = "PortProtonQt"
__app_version__ = "0.1.6"
__app_version__ = "0.1.8"
def get_version():
try:
commit = subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
stderr=subprocess.DEVNULL,
).decode("utf-8").strip()
return f"{__app_version__} ({commit})"
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
return __app_version__
def main():
os.environ["PW_CLI"] = "1"
os.environ["PROCESS_LOG"] = "1"
os.environ["START_FROM_STEAM"] = "1"
start_sh = get_portproton_start_command()
if start_sh is None:
return
subprocess.run(start_sh + ["cli", "--initial"])
app = QApplication(sys.argv)
app.setWindowIcon(QIcon.fromTheme(__app_id__))
app.setDesktopFileName(__app_id__)
@@ -19,40 +48,116 @@ def main():
app.setApplicationVersion(__app_version__)
args = parse_args()
# Setup logger with specified debug level
setup_logger(args.debug_level)
# Reinitialize logger after setup to ensure it uses the new configuration
logger = get_logger(__name__)
# --- Single-instance logic ---
server_name = __app_id__
socket = QLocalSocket()
socket.connectToServer(server_name)
if socket.waitForConnected(200):
# Второй экземпляр — передаём команду первому
fullscreen = args.fullscreen or read_fullscreen_config()
msg = b"show:fullscreen" if fullscreen else b"show"
socket.write(msg)
socket.flush()
socket.waitForBytesWritten(500)
socket.disconnectFromServer()
logger.info("Restored existing instance from tray")
return
# Если старый сокет остался — удалить
QLocalServer.removeServer(server_name)
local_server = QLocalServer()
if not local_server.listen(server_name):
logger.warning(f"Failed to start local server: {local_server.errorString()}")
return
# --- Qt translations ---
system_locale = QLocale.system()
qt_translator = QTranslator()
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
app.installTranslator(qt_translator)
else:
logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language")
logger.warning(
f"Qt translations for {system_locale.name()} not found in {translations_path}, using English"
)
window = MainWindow(app_name=__app_name__)
# --- Main Window ---
version = get_version()
window = MainWindow(app_name=__app_name__, version=version)
if args.fullscreen:
logger.info("Launching in fullscreen mode due to --fullscreen flag")
# --- Handle incoming connections ---
def handle_new_connection():
conn = local_server.nextPendingConnection()
if not conn:
return
if conn.waitForReadyRead(1000):
data = conn.readAll().data()
msg = bytes(data).decode("utf-8", errors="ignore")
logger.info(f"IPC message received: {msg}")
def restore_window():
try:
if msg.startswith("show"):
if hasattr(window, "restore_from_tray"):
window.restore_from_tray() # type: ignore[attr-defined]
else:
window.showNormal()
window.raise_()
window.activateWindow()
window.setWindowState(
window.windowState() & ~Qt.WindowState.WindowMinimized | Qt.WindowState.WindowActive
)
if ":fullscreen" in msg:
logger.info("Switching to fullscreen via IPC")
save_fullscreen_config(True)
window.showFullScreen()
else:
logger.info("Switching to normal window via IPC")
save_fullscreen_config(False)
window.showNormal()
except Exception as e:
logger.warning(f"Failed to restore window: {e}")
# Выполняем в основном потоке
QTimer.singleShot(0, restore_window)
conn.disconnectFromServer()
local_server.newConnection.connect(handle_new_connection)
# --- Initial fullscreen state ---
launch_fullscreen = args.fullscreen or read_fullscreen_config()
if launch_fullscreen:
logger.info(
f"Launching in fullscreen mode ({'--fullscreen' if args.fullscreen else 'config'})"
)
save_fullscreen_config(True)
window.showFullScreen()
else:
logger.info("Launching in normal mode")
save_fullscreen_config(False)
window.showNormal()
# --- Cleanup ---
def cleanup_on_exit():
nonlocal window
app.aboutToQuit.disconnect()
if window:
window.close()
app.quit()
try:
local_server.close()
QLocalServer.removeServer(server_name)
if window:
window.close()
except Exception as e:
logger.warning(f"Cleanup error: {e}")
app.aboutToQuit.connect(cleanup_on_exit)
window.show()
sys.exit(app.exec())
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,11 +1,17 @@
import os
import configparser
import shutil
import subprocess
from portprotonqt.logger import get_logger
logger = get_logger(__name__)
_portproton_location = None
_portproton_start_sh = None
# Configuration cache for performance optimization
_config_cache = {}
_config_last_modified = {}
# Paths to configuration files
CONFIG_FILE = os.path.join(
@@ -26,13 +32,35 @@ THEMES_DIRS = [
]
def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails."""
cp = configparser.ConfigParser()
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails.
Uses caching to avoid repeated file reads for better performance.
"""
# Check if file exists
if not os.path.exists(config_file):
logger.debug(f"Configuration file {config_file} not found")
return None
# Get file modification time
try:
current_mtime = os.path.getmtime(config_file)
except OSError:
logger.warning(f"Failed to get modification time for {config_file}")
return None
# Check if we have a cached version that's still valid
if config_file in _config_cache and config_file in _config_last_modified:
if _config_last_modified[config_file] == current_mtime:
logger.debug(f"Using cached config for {config_file}")
return _config_cache[config_file]
# Read and parse the config file
cp = configparser.ConfigParser()
try:
cp.read(config_file, encoding="utf-8")
# Update cache
_config_cache[config_file] = cp
_config_last_modified[config_file] = current_mtime
logger.debug(f"Config file {config_file} loaded and cached")
return cp
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
logger.warning(f"Invalid configuration file format: {e}")
@@ -41,6 +69,14 @@ def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
logger.warning(f"Failed to read configuration file: {e}")
return None
def invalidate_config_cache(config_file: str = CONFIG_FILE):
"""Invalidates the cached configuration for the specified file."""
if config_file in _config_cache:
del _config_cache[config_file]
if config_file in _config_last_modified:
del _config_last_modified[config_file]
logger.debug(f"Config cache invalidated for {config_file}")
def read_config():
"""Reads the configuration file and returns a dictionary of parameters.
Example line in config (no sections):
@@ -75,6 +111,8 @@ def save_theme_to_config(theme_name):
cp["Appearance"]["theme"] = theme_name
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_time_config():
"""Reads time settings from the [Time] section of the configuration file.
@@ -94,6 +132,8 @@ def save_time_config(detail_level):
cp["Time"]["detail_level"] = detail_level
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_file_content(file_path):
"""Reads the content of a file and returns it as a string."""
@@ -101,14 +141,14 @@ def read_file_content(file_path):
return f.read().strip()
def get_portproton_location():
"""Returns the path to the PortProton directory.
Checks the cached path first. If not set, reads from PORTPROTON_CONFIG_FILE.
If the path is invalid, uses the default directory.
"""
"""Возвращает путь к PortProton каталогу (строку) или None."""
global _portproton_location
if _portproton_location is not None:
return _portproton_location
location = None
if os.path.isfile(PORTPROTON_CONFIG_FILE):
try:
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
@@ -116,19 +156,46 @@ def get_portproton_location():
_portproton_location = location
logger.info(f"PortProton path from configuration: {location}")
return _portproton_location
logger.warning(f"Invalid PortProton path in configuration: {location}, using default path")
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
except (OSError, PermissionError) as e:
logger.warning(f"Failed to read PortProton configuration file: {e}, using default path")
logger.warning(f"Failed to read PortProton configuration file: {e}")
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
if os.path.isdir(default_dir):
_portproton_location = default_dir
logger.info(f"Using flatpak PortProton directory: {default_dir}")
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
if os.path.isdir(default_flatpak_dir):
_portproton_location = default_flatpak_dir
logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
return _portproton_location
logger.warning("PortProton configuration and flatpak directory not found")
logger.warning("PortProton configuration and Flatpak directory not found")
return None
def get_portproton_start_command():
"""Возвращает список команд для запуска PortProton (start.sh или flatpak run)."""
portproton_path = get_portproton_location()
if not portproton_path:
return None
try:
result = subprocess.run(
["flatpak", "list"],
capture_output=True,
text=True,
check=False
)
if "ru.linux_gaming.PortProton" in result.stdout:
logger.info("Detected Flatpak installation")
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
except Exception:
pass
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
if os.path.exists(start_sh_path):
return [start_sh_path]
logger.warning("Neither flatpak nor start.sh found for PortProton")
return None
def parse_desktop_entry(file_path):
"""Reads and parses a .desktop file using configparser.
Returns None if the [Desktop Entry] section is missing.
@@ -176,6 +243,30 @@ def save_card_size(card_width):
cp["Cards"]["card_width"] = str(card_width)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_auto_card_size():
"""Reads the card size (width) for Auto Install from the [Cards] section.
Returns 250 if the parameter is not set.
"""
cp = read_config_safely(CONFIG_FILE)
if cp is None or not cp.has_section("Cards") or not cp.has_option("Cards", "auto_card_width"):
save_auto_card_size(250)
return 250
return cp.getint("Cards", "auto_card_width", fallback=250)
def save_auto_card_size(card_width):
"""Saves the card size (width) for Auto Install to the [Cards] section."""
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
if "Cards" not in cp:
cp["Cards"] = {}
cp["Cards"]["auto_card_width"] = str(card_width)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_sort_method():
"""Reads the sort method from the [Games] section.
@@ -195,6 +286,8 @@ def save_sort_method(sort_method):
cp["Games"]["sort_method"] = sort_method
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_display_filter():
"""Reads the display_filter parameter from the [Games] section.
@@ -214,6 +307,8 @@ def save_display_filter(filter_value):
cp["Games"]["display_filter"] = filter_value
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_favorites():
"""Reads the list of favorite games from the [Favorites] section.
@@ -239,6 +334,8 @@ def save_favorites(favorites):
cp["Favorites"]["games"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_rumble_config():
"""Reads the gamepad rumble setting from the [Gamepad] section.
@@ -258,6 +355,29 @@ def save_rumble_config(rumble_enabled):
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_gamepad_type():
"""Reads the gamepad type from the [Gamepad] section.
Returns 'xbox' if the parameter is missing.
"""
cp = read_config_safely(CONFIG_FILE)
if cp is None or not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "type"):
save_gamepad_type("xbox")
return "xbox"
return cp.get("Gamepad", "type", fallback="xbox").lower()
def save_gamepad_type(gpad_type):
"""Saves the gamepad type to the [Gamepad] section."""
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
if "Gamepad" not in cp:
cp["Gamepad"] = {}
cp["Gamepad"]["type"] = gpad_type
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def ensure_default_proxy_config():
"""Ensures the [Proxy] section exists in the configuration file.
@@ -302,6 +422,8 @@ def save_proxy_config(proxy_url="", proxy_user="", proxy_password=""):
cp["Proxy"]["proxy_password"] = proxy_password
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_fullscreen_config():
"""Reads the fullscreen mode setting from the [Display] section.
@@ -321,6 +443,8 @@ def save_fullscreen_config(fullscreen):
cp["Display"]["fullscreen"] = str(fullscreen)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_window_geometry() -> tuple[int, int]:
"""Reads the window width and height from the [MainWindow] section.
@@ -342,6 +466,8 @@ def save_window_geometry(width: int, height: int):
cp["MainWindow"]["height"] = str(height)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def reset_config():
"""Resets the configuration file by deleting it.
@@ -351,6 +477,8 @@ def reset_config():
try:
os.remove(CONFIG_FILE)
logger.info("Configuration file %s deleted", CONFIG_FILE)
# Invalidate cache after deletion
invalidate_config_cache()
except Exception as e:
logger.warning(f"Failed to delete configuration file: {e}")
@@ -365,6 +493,9 @@ def clear_cache():
except Exception as e:
logger.warning(f"Failed to delete cache: {e}")
# Also clear our internal config cache
invalidate_config_cache()
def read_auto_fullscreen_gamepad():
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
Returns False if the parameter is missing.
@@ -383,6 +514,8 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_favorite_folders():
"""Reads the list of favorite folders from the [FavoritesFolders] section.
@@ -408,3 +541,26 @@ def save_favorite_folders(folders):
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()
def read_minimize_to_tray():
"""Reads the minimize-to-tray setting from the [Display] section.
Returns True if the parameter is missing (default: minimize to tray).
"""
cp = read_config_safely(CONFIG_FILE)
if cp is None or not cp.has_section("Display") or not cp.has_option("Display", "minimize_to_tray"):
save_minimize_to_tray(True)
return True
return cp.getboolean("Display", "minimize_to_tray", fallback=True)
def save_minimize_to_tray(minimize_to_tray):
"""Saves the minimize-to-tray setting to the [Display] section."""
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
if "Display" not in cp:
cp["Display"] = {}
cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
cp.write(configfile)
# Invalidate cache after saving
invalidate_config_cache()

View File

@@ -11,7 +11,7 @@ from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QLineEdit, QApplicati
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
from portprotonqt.localization import _
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders, get_portproton_start_command
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
@@ -406,16 +406,7 @@ class ContextMenuManager:
)
return
# Construct EGS launch command
wrapper = "flatpak run ru.linux_gaming.PortProton"
start_sh_path = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
if self.portproton_location and ".var" not in self.portproton_location:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
self.signals.show_warning_dialog.emit(
_("Error"),
_("start.sh not found at {path}").format(path=start_sh_path)
)
return
wrapper = get_portproton_start_command()
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
else:
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)

View File

Before

Width:  |  Height:  |  Size: 634 KiB

After

Width:  |  Height:  |  Size: 634 KiB

View File

Before

Width:  |  Height:  |  Size: 315 KiB

After

Width:  |  Height:  |  Size: 315 KiB

View File

Before

Width:  |  Height:  |  Size: 978 KiB

After

Width:  |  Height:  |  Size: 978 KiB

View File

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

View File

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 391 KiB

View File

Before

Width:  |  Height:  |  Size: 710 KiB

After

Width:  |  Height:  |  Size: 710 KiB

View File

Before

Width:  |  Height:  |  Size: 627 KiB

After

Width:  |  Height:  |  Size: 627 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ from portprotonqt.localization import get_egs_language, _
from portprotonqt.logger import get_logger
from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
from portprotonqt.config_utils import get_portproton_location
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
from portprotonqt.steam_api import (
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
@@ -254,14 +254,7 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba
return
# Determine wrapper
wrapper = "flatpak run ru.linux_gaming.PortProton"
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
if portproton_dir is not None and ".var" not in portproton_dir:
wrapper = start_sh_path
if not os.path.exists(start_sh_path):
logger.error(f"start.sh not found at {start_sh_path}")
callback((False, f"start.sh not found at {start_sh_path}"))
return
wrapper = get_portproton_start_command()
# Create launch script
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")

View File

@@ -1,5 +1,5 @@
from PySide6.QtGui import QPainter, QColor, QDesktopServices
from PySide6.QtCore import Signal, Property, Qt, QUrl
from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
from collections.abc import Callable
from portprotonqt.image_utils import load_pixmap_async, round_corners
@@ -102,7 +102,7 @@ class GameCard(QFrame):
self.favoriteLabel = ClickableLabel(self.coverWidget)
self.favoriteLabel.clicked.connect(self.toggle_favorite)
self.is_favorite = self.name in read_favorites()
self.is_favorite = self.name in set(read_favorites())
self.update_favorite_icon()
self.favoriteLabel.raise_()
@@ -203,7 +203,7 @@ class GameCard(QFrame):
self.update_cover_pixmap()
def update_cover_pixmap(self):
if self.base_pixmap:
if self.base_pixmap and not self.base_pixmap.isNull():
scaled_width = int(self.base_card_width * self._scale)
scaled_pixmap = self.base_pixmap.scaled(scaled_width, int(scaled_width * 1.5), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
@@ -404,14 +404,22 @@ class GameCard(QFrame):
self.favoriteLabel.setText("")
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
parent = self.parent()
while parent:
if hasattr(parent, 'game_library_manager'):
QTimer.singleShot(0, parent.game_library_manager.update_game_grid) # type: ignore[attr-defined]
break
parent = parent.parent()
def toggle_favorite(self):
favorites = read_favorites()
favorites_set = set(favorites)
if self.is_favorite:
if self.name in favorites:
if self.name in favorites_set:
favorites.remove(self.name)
self.is_favorite = False
else:
if self.name not in favorites:
if self.name not in favorites_set:
favorites.append(self.name)
self.is_favorite = True
save_favorites(favorites)

View File

@@ -33,6 +33,7 @@ class MainWindowProtocol(Protocol):
# Required attributes
searchEdit: CustomLineEdit
_last_card_width: int
card_width: int
current_hovered_card: GameCard | None
current_focused_card: GameCard | None
gamesListWidget: QWidget | None
@@ -128,6 +129,8 @@ class GameLibraryManager:
self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{self.card_width} px")
save_card_size(self.card_width)
self.main_window.card_width = self.card_width
self.main_window._last_card_width = self.card_width
for card in self.game_card_cache.values():
card.update_card_size(self.card_width)
self.update_game_grid()
@@ -217,6 +220,16 @@ class GameLibraryManager:
else:
self._update_game_grid_immediate()
def force_update_cards_library(self):
if self.gamesListWidget and self.gamesListLayout:
self.gamesListLayout.invalidate()
self.gamesListWidget.updateGeometry()
widget = self.gamesListWidget
QTimer.singleShot(0, lambda: (
widget.adjustSize(),
widget.updateGeometry()
))
def _update_game_grid_immediate(self):
"""Updates the game grid with the provided or current game list."""
if self.gamesListLayout is None or self.gamesListWidget is None:
@@ -254,8 +267,9 @@ class GameLibraryManager:
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
# Quick partition: Sort favorites and non-favorites separately, then merge
fav_games = [g for g in games_list if g[0] in favorites]
non_fav_games = [g for g in games_list if g[0] not in favorites]
favorites_set = set(favorites) # Convert to set for O(1) lookup
fav_games = [g for g in games_list if g[0] in favorites_set]
non_fav_games = [g for g in games_list if g[0] not in favorites_set]
sorted_fav = sorted(fav_games, key=partition_sort_key)
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
sorted_games = sorted_fav + sorted_non_fav
@@ -346,6 +360,8 @@ class GameLibraryManager:
self.gamesListWidget.updateGeometry()
self.main_window._last_card_width = self.card_width
self.force_update_cards_library()
self.is_filtering = False # Reset flag in any case
def _apply_filter_visibility(self, search_text: str):

View File

@@ -36,11 +36,22 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
current_theme_name = read_theme_from_config()
def finish_with(pixmap: QPixmap):
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
x = (scaled.width() - width) // 2
y = (scaled.height() - height) // 2
cropped = scaled.copy(x, y, width, height)
callback(cropped)
# Check if pixmap is valid before attempting to scale it
if pixmap.isNull():
# Create a default placeholder pixmap instead of trying to scale a null pixmap
placeholder_pixmap = QPixmap(width, height)
placeholder_pixmap.fill(QColor("#333333"))
painter = QPainter(placeholder_pixmap)
painter.setPen(QPen(QColor("white")))
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
painter.end()
callback(placeholder_pixmap)
else:
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
x = (scaled.width() - width) // 2
y = (scaled.height() - height) // 2
cropped = scaled.copy(x, y, width, height)
callback(cropped)
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
@@ -58,6 +69,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
local_path = os.path.join(image_folder, f"{appid}.jpg")
if os.path.exists(local_path):
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
@@ -69,6 +83,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
@@ -83,11 +99,19 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
except Exception as e:
logger.error(f"Ошибка обработки URL {cover}: {e}")
if cover and cover.startswith(("http://", "https://")):
# SteamGridDB (SGDB)
if cover and cover.startswith("https://cdn2.steamgriddb.com"):
try:
local_path = os.path.join(image_folder, f"{app_name}.jpg")
parts = cover.split("/")
filename = parts[-1] if parts else "sgdb_cover.png"
# SGDB ссылки содержат уникальный хеш в названии — используем как имя
local_path = os.path.join(image_folder, filename)
if os.path.exists(local_path):
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
@@ -99,6 +123,45 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
painter = QPainter(pixmap)
painter.setPen(QPen(QColor("white")))
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
painter.end()
finish_with(pixmap)
logger.info("Downloading SGDB cover for %s -> %s", app_name or "unknown", filename)
downloader.download_async(cover, local_path, timeout=5, callback=on_downloaded)
return
except Exception as e:
logger.error(f"Ошибка обработки SGDB URL {cover}: {e}")
if cover and cover.startswith(("http://", "https://")):
try:
local_path = os.path.join(image_folder, f"{app_name}.jpg")
if os.path.exists(local_path):
pixmap = QPixmap(local_path)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {local_path}")
finish_with(pixmap)
return
def on_downloaded(result: str | None):
pixmap = QPixmap()
if result and os.path.exists(result):
pixmap.load(result)
if pixmap.isNull():
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
@@ -115,6 +178,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
if cover and QFile.exists(cover):
pixmap = QPixmap(cover)
# Check if the pixmap loaded successfully
if pixmap.isNull():
logger.warning(f"Failed to load image from {cover}")
finish_with(pixmap)
return
@@ -122,6 +188,8 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
pixmap = QPixmap()
if placeholder_path and QFile.exists(placeholder_path):
pixmap.load(placeholder_path)
if pixmap.isNull():
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
else:
pixmap = QPixmap(width, height)
pixmap.fill(QColor("#333333"))
@@ -141,7 +209,15 @@ def round_corners(pixmap, radius):
"""
if pixmap.isNull():
return pixmap
# Check if radius is valid to prevent issues
if radius <= 0:
return pixmap
size = pixmap.size()
if size.width() <= 0 or size.height() <= 0:
return pixmap
rounded = QPixmap(size)
rounded.fill(QColor(0, 0, 0, 0))
painter = QPainter(rounded)
@@ -244,20 +320,31 @@ class FullscreenDialog(QDialog):
QApplication.processEvents()
pixmap, caption = self.images[self.current_index]
# Учитываем devicePixelRatio для масштабирования высокого качества
device_pixel_ratio = get_device_pixel_ratio()
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
# Check if pixmap is valid before attempting to scale it
if pixmap.isNull():
# Create a default placeholder pixmap instead of trying to scale a null pixmap
placeholder_pixmap = QPixmap(self.FIXED_WIDTH - 80, self.FIXED_HEIGHT)
placeholder_pixmap.fill(QColor("#333333"))
painter = QPainter(placeholder_pixmap)
painter.setPen(QPen(QColor("white")))
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
painter.end()
self.imageLabel.setPixmap(placeholder_pixmap)
else:
# Учитываем devicePixelRatio для масштабирования высокого качества
device_pixel_ratio = get_device_pixel_ratio()
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
# Масштабируем изображение из оригинального pixmap
scaled_pixmap = pixmap.scaled(
target_width,
target_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
self.imageLabel.setPixmap(scaled_pixmap)
# Масштабируем изображение из оригинального pixmap
scaled_pixmap = pixmap.scaled(
target_width,
target_height,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation
)
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
self.imageLabel.setPixmap(scaled_pixmap)
self.captionLabel.setText(caption)
self.setWindowTitle(caption)

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@@ -76,10 +76,6 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -124,6 +120,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""
@@ -252,13 +252,37 @@ msgstr ""
msgid "Select All"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgid "Open"
msgstr ""
msgid "Select Dir"
msgstr ""
msgid "Prev Dir"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Toggle"
msgstr ""
msgid "Install"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Prev Tab"
msgstr ""
msgid "Next Tab"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgstr ""
msgid "File Explorer"
msgstr ""
@@ -326,12 +350,6 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Install"
msgstr ""
msgid "Winetricks not found. Please try again."
msgstr ""
@@ -347,6 +365,39 @@ msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Exe Settings"
msgstr ""
msgid "Main"
msgstr ""
msgid "Advanced"
msgstr ""
msgid "Setting"
msgstr ""
msgid "Value"
msgstr ""
msgid "Description"
msgstr ""
msgid "disabled"
msgstr ""
msgid "Info"
msgstr ""
msgid "No changes to apply."
msgstr ""
msgid "Failed to apply changes. Check logs."
msgstr ""
msgid "Settings updated successfully."
msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
@@ -395,9 +446,6 @@ msgstr ""
msgid "Auto Install"
msgstr ""
msgid "Emulators"
msgstr ""
msgid "Wine Settings"
msgstr ""
@@ -416,6 +464,25 @@ msgstr ""
msgid "Search"
msgstr ""
msgid "Installation already in progress."
msgstr ""
msgid "Failed to start installation."
msgstr ""
#, python-brace-format
msgid "Processed {} installation..."
msgstr ""
msgid "Installation completed successfully."
msgstr ""
msgid "Installation failed."
msgstr ""
msgid "Installation error."
msgstr ""
msgid "Loading Steam games..."
msgstr ""
@@ -432,12 +499,6 @@ msgstr ""
msgid "Added '{name}'"
msgstr ""
msgid "Here you can configure automatic game installation..."
msgstr ""
msgid "List of available emulators and their configuration..."
msgstr ""
msgid "Compatibility tool:"
msgstr ""
@@ -450,12 +511,6 @@ msgstr ""
msgid "Registry Editor"
msgstr ""
msgid "Control Panel"
msgstr ""
msgid "Task Manager"
msgstr ""
msgid "Command Prompt"
msgstr ""
@@ -477,6 +532,36 @@ msgstr ""
msgid "Clear Prefix"
msgstr ""
msgid "Launching tool..."
msgstr ""
msgid "Failed to start process."
msgstr ""
msgid "Confirm Clear"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
msgid "Clearing prefix..."
msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr ""
#, python-brace-format
msgid "Prefix clear failed with exit code {}."
msgstr ""
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr ""
msgid "Failed to start backup process."
msgstr ""
@@ -552,6 +637,9 @@ msgstr ""
msgid "Games Display Filter:"
msgstr ""
msgid "Gamepad Type:"
msgstr ""
msgid "Proxy URL"
msgstr ""
@@ -576,6 +664,12 @@ msgstr ""
msgid "Application Fullscreen Mode:"
msgstr ""
msgid "Minimize to tray on close"
msgstr ""
msgid "Application Close Mode:"
msgstr ""
msgid "Auto Fullscreen on Gamepad connected"
msgstr ""
@@ -650,6 +744,10 @@ msgstr ""
msgid "Error applying theme '{0}'"
msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH"
msgstr ""
@@ -708,6 +806,232 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr ""
msgid "Enable futex-based in-process synchronization primitives."
msgstr ""
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr ""
msgid "Enable vkd3d support - Ray Tracing"
msgstr ""
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr ""
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr ""
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr ""
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr ""
msgid "Disguise all NVIDIA GPU features"
msgstr ""
msgid "Run the application in WINE virtual desktop"
msgstr ""
msgid "Run the application in a terminal"
msgstr ""
msgid "Disable startup mode and WINE version selector window"
msgstr ""
msgid "Use system GameMode for performance optimization"
msgstr ""
msgid "Enable forced use of third-party DirectX libraries"
msgstr ""
msgid "Fix pink-tinted video playback in some games"
msgstr ""
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr ""
msgid "Force US keyboard layout"
msgstr ""
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr ""
msgid "Use WINE shader caching"
msgstr ""
msgid "Force use of built-in DXGI library"
msgstr ""
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr ""
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr ""
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr ""
msgid "Disable desktop compositing for performance"
msgstr ""
msgid "Use container launch mode (recommended default)"
msgstr ""
msgid "Force DirectInput protocol instead of XInput"
msgstr ""
msgid "Enable experimental native Wayland support"
msgstr ""
msgid "Enable HDR settings under native Wayland"
msgstr ""
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr ""
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr ""
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr ""
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr ""
msgid "Use async dxvk-sarek (experimental)"
msgstr ""
msgid "Windows version"
msgstr ""
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
msgid "DLL Overrides"
msgstr ""
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
msgid "Launch Arguments"
msgstr ""
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
msgid "CPU Cores Limit"
msgstr ""
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
msgid "OpenGL Version"
msgstr ""
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
msgid "VKD3D Feature Level"
msgstr ""
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
msgid "Window Mode"
msgstr ""
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
msgid "AMD Vulkan Driver"
msgstr ""
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
msgid "NUMA Node"
msgstr ""
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
msgid "Reboot"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@@ -76,10 +76,6 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -124,6 +120,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""
@@ -252,13 +252,37 @@ msgstr ""
msgid "Select All"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgid "Open"
msgstr ""
msgid "Select Dir"
msgstr ""
msgid "Prev Dir"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Toggle"
msgstr ""
msgid "Install"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Prev Tab"
msgstr ""
msgid "Next Tab"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgstr ""
msgid "File Explorer"
msgstr ""
@@ -326,12 +350,6 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Install"
msgstr ""
msgid "Winetricks not found. Please try again."
msgstr ""
@@ -347,6 +365,39 @@ msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Exe Settings"
msgstr ""
msgid "Main"
msgstr ""
msgid "Advanced"
msgstr ""
msgid "Setting"
msgstr ""
msgid "Value"
msgstr ""
msgid "Description"
msgstr ""
msgid "disabled"
msgstr ""
msgid "Info"
msgstr ""
msgid "No changes to apply."
msgstr ""
msgid "Failed to apply changes. Check logs."
msgstr ""
msgid "Settings updated successfully."
msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
@@ -395,9 +446,6 @@ msgstr ""
msgid "Auto Install"
msgstr ""
msgid "Emulators"
msgstr ""
msgid "Wine Settings"
msgstr ""
@@ -416,6 +464,25 @@ msgstr ""
msgid "Search"
msgstr ""
msgid "Installation already in progress."
msgstr ""
msgid "Failed to start installation."
msgstr ""
#, python-brace-format
msgid "Processed {} installation..."
msgstr ""
msgid "Installation completed successfully."
msgstr ""
msgid "Installation failed."
msgstr ""
msgid "Installation error."
msgstr ""
msgid "Loading Steam games..."
msgstr ""
@@ -432,12 +499,6 @@ msgstr ""
msgid "Added '{name}'"
msgstr ""
msgid "Here you can configure automatic game installation..."
msgstr ""
msgid "List of available emulators and their configuration..."
msgstr ""
msgid "Compatibility tool:"
msgstr ""
@@ -450,12 +511,6 @@ msgstr ""
msgid "Registry Editor"
msgstr ""
msgid "Control Panel"
msgstr ""
msgid "Task Manager"
msgstr ""
msgid "Command Prompt"
msgstr ""
@@ -477,6 +532,36 @@ msgstr ""
msgid "Clear Prefix"
msgstr ""
msgid "Launching tool..."
msgstr ""
msgid "Failed to start process."
msgstr ""
msgid "Confirm Clear"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
msgid "Clearing prefix..."
msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr ""
#, python-brace-format
msgid "Prefix clear failed with exit code {}."
msgstr ""
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr ""
msgid "Failed to start backup process."
msgstr ""
@@ -552,6 +637,9 @@ msgstr ""
msgid "Games Display Filter:"
msgstr ""
msgid "Gamepad Type:"
msgstr ""
msgid "Proxy URL"
msgstr ""
@@ -576,6 +664,12 @@ msgstr ""
msgid "Application Fullscreen Mode:"
msgstr ""
msgid "Minimize to tray on close"
msgstr ""
msgid "Application Close Mode:"
msgstr ""
msgid "Auto Fullscreen on Gamepad connected"
msgstr ""
@@ -650,6 +744,10 @@ msgstr ""
msgid "Error applying theme '{0}'"
msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH"
msgstr ""
@@ -708,6 +806,232 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr ""
msgid "Enable futex-based in-process synchronization primitives."
msgstr ""
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr ""
msgid "Enable vkd3d support - Ray Tracing"
msgstr ""
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr ""
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr ""
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr ""
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr ""
msgid "Disguise all NVIDIA GPU features"
msgstr ""
msgid "Run the application in WINE virtual desktop"
msgstr ""
msgid "Run the application in a terminal"
msgstr ""
msgid "Disable startup mode and WINE version selector window"
msgstr ""
msgid "Use system GameMode for performance optimization"
msgstr ""
msgid "Enable forced use of third-party DirectX libraries"
msgstr ""
msgid "Fix pink-tinted video playback in some games"
msgstr ""
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr ""
msgid "Force US keyboard layout"
msgstr ""
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr ""
msgid "Use WINE shader caching"
msgstr ""
msgid "Force use of built-in DXGI library"
msgstr ""
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr ""
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr ""
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr ""
msgid "Disable desktop compositing for performance"
msgstr ""
msgid "Use container launch mode (recommended default)"
msgstr ""
msgid "Force DirectInput protocol instead of XInput"
msgstr ""
msgid "Enable experimental native Wayland support"
msgstr ""
msgid "Enable HDR settings under native Wayland"
msgstr ""
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr ""
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr ""
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr ""
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr ""
msgid "Use async dxvk-sarek (experimental)"
msgstr ""
msgid "Windows version"
msgstr ""
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
msgid "DLL Overrides"
msgstr ""
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
msgid "Launch Arguments"
msgstr ""
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
msgid "CPU Cores Limit"
msgstr ""
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
msgid "OpenGL Version"
msgstr ""
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
msgid "VKD3D Feature Level"
msgstr ""
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
msgid "Window Mode"
msgstr ""
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
msgid "AMD Vulkan Driver"
msgstr ""
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
msgid "NUMA Node"
msgstr ""
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
msgid "Reboot"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -74,10 +74,6 @@ msgstr ""
msgid "Legendary executable not found at {path}"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
msgid "Success"
msgstr ""
@@ -122,6 +118,10 @@ msgstr ""
msgid "Removed '{game_name}' from favorites"
msgstr ""
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr ""
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr ""
@@ -250,13 +250,37 @@ msgstr ""
msgid "Select All"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgid "Open"
msgstr ""
msgid "Select Dir"
msgstr ""
msgid "Prev Dir"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Toggle"
msgstr ""
msgid "Install"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Prev Tab"
msgstr ""
msgid "Next Tab"
msgstr ""
#, python-brace-format
msgid "Launching {0}"
msgstr ""
msgid "File Explorer"
msgstr ""
@@ -324,12 +348,6 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "Force Install"
msgstr ""
msgid "Install"
msgstr ""
msgid "Winetricks not found. Please try again."
msgstr ""
@@ -345,6 +363,39 @@ msgstr ""
msgid "Components installed successfully."
msgstr ""
msgid "Exe Settings"
msgstr ""
msgid "Main"
msgstr ""
msgid "Advanced"
msgstr ""
msgid "Setting"
msgstr ""
msgid "Value"
msgstr ""
msgid "Description"
msgstr ""
msgid "disabled"
msgstr ""
msgid "Info"
msgstr ""
msgid "No changes to apply."
msgstr ""
msgid "Failed to apply changes. Check logs."
msgstr ""
msgid "Settings updated successfully."
msgstr ""
msgid "Loading Epic Games Store games..."
msgstr ""
@@ -393,9 +444,6 @@ msgstr ""
msgid "Auto Install"
msgstr ""
msgid "Emulators"
msgstr ""
msgid "Wine Settings"
msgstr ""
@@ -414,6 +462,25 @@ msgstr ""
msgid "Search"
msgstr ""
msgid "Installation already in progress."
msgstr ""
msgid "Failed to start installation."
msgstr ""
#, python-brace-format
msgid "Processed {} installation..."
msgstr ""
msgid "Installation completed successfully."
msgstr ""
msgid "Installation failed."
msgstr ""
msgid "Installation error."
msgstr ""
msgid "Loading Steam games..."
msgstr ""
@@ -430,12 +497,6 @@ msgstr ""
msgid "Added '{name}'"
msgstr ""
msgid "Here you can configure automatic game installation..."
msgstr ""
msgid "List of available emulators and their configuration..."
msgstr ""
msgid "Compatibility tool:"
msgstr ""
@@ -448,12 +509,6 @@ msgstr ""
msgid "Registry Editor"
msgstr ""
msgid "Control Panel"
msgstr ""
msgid "Task Manager"
msgstr ""
msgid "Command Prompt"
msgstr ""
@@ -475,6 +530,36 @@ msgstr ""
msgid "Clear Prefix"
msgstr ""
msgid "Launching tool..."
msgstr ""
msgid "Failed to start process."
msgstr ""
msgid "Confirm Clear"
msgstr ""
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr ""
msgid "Clearing prefix..."
msgstr ""
msgid "Failed to start prefix clear process."
msgstr ""
msgid "Prefix cleared successfully."
msgstr ""
#, python-brace-format
msgid "Prefix clear failed with exit code {}."
msgstr ""
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr ""
msgid "Failed to start backup process."
msgstr ""
@@ -550,6 +635,9 @@ msgstr ""
msgid "Games Display Filter:"
msgstr ""
msgid "Gamepad Type:"
msgstr ""
msgid "Proxy URL"
msgstr ""
@@ -574,6 +662,12 @@ msgstr ""
msgid "Application Fullscreen Mode:"
msgstr ""
msgid "Minimize to tray on close"
msgstr ""
msgid "Application Close Mode:"
msgstr ""
msgid "Auto Fullscreen on Gamepad connected"
msgstr ""
@@ -648,6 +742,10 @@ msgstr ""
msgid "Error applying theme '{0}'"
msgstr ""
#, python-brace-format
msgid "Executable not found: {0}"
msgstr ""
msgid "LAST LAUNCH"
msgstr ""
@@ -706,6 +804,232 @@ msgstr ""
msgid "File not found: {0}"
msgstr ""
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr ""
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr ""
msgid "Enable futex-based in-process synchronization primitives."
msgstr ""
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr ""
msgid "Enable vkd3d support - Ray Tracing"
msgstr ""
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr ""
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr ""
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr ""
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr ""
msgid "Disguise all NVIDIA GPU features"
msgstr ""
msgid "Run the application in WINE virtual desktop"
msgstr ""
msgid "Run the application in a terminal"
msgstr ""
msgid "Disable startup mode and WINE version selector window"
msgstr ""
msgid "Use system GameMode for performance optimization"
msgstr ""
msgid "Enable forced use of third-party DirectX libraries"
msgstr ""
msgid "Fix pink-tinted video playback in some games"
msgstr ""
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr ""
msgid "Force US keyboard layout"
msgstr ""
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr ""
msgid "Use WINE shader caching"
msgstr ""
msgid "Force use of built-in DXGI library"
msgstr ""
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr ""
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr ""
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr ""
msgid "Disable desktop compositing for performance"
msgstr ""
msgid "Use container launch mode (recommended default)"
msgstr ""
msgid "Force DirectInput protocol instead of XInput"
msgstr ""
msgid "Enable experimental native Wayland support"
msgstr ""
msgid "Enable HDR settings under native Wayland"
msgstr ""
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr ""
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr ""
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr ""
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr ""
msgid "Use async dxvk-sarek (experimental)"
msgstr ""
msgid "Windows version"
msgstr ""
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
msgid "DLL Overrides"
msgstr ""
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
msgid "Launch Arguments"
msgstr ""
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
msgid "CPU Cores Limit"
msgstr ""
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
msgid "OpenGL Version"
msgstr ""
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
msgid "VKD3D Feature Level"
msgstr ""
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
msgid "Locale"
msgstr ""
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
msgid "Window Mode"
msgstr ""
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
msgid "AMD Vulkan Driver"
msgstr ""
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
msgid "NUMA Node"
msgstr ""
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
msgid "Reboot"
msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-10-09 16:37+0500\n"
"PO-Revision-Date: 2025-10-09 16:37+0500\n"
"POT-Creation-Date: 2025-11-11 17:00+0500\n"
"PO-Revision-Date: 2025-11-11 17:00+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@@ -77,10 +77,6 @@ msgstr "Остановлен(а) '{game_name}'"
msgid "Legendary executable not found at {path}"
msgstr "Legendary не найден по пути {path}"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
msgid "Success"
msgstr "Успешно"
@@ -127,6 +123,10 @@ msgstr "'{game_name}' был(а) добавлен(а) в избранное"
msgid "Removed '{game_name}' from favorites"
msgstr "'{game_name}' был(а) удалён(а) из избранного"
#, python-brace-format
msgid "start.sh not found at {path}"
msgstr "start.sh не найден по адресу {path}"
#, python-brace-format
msgid "Launch game \"{name}\" with PortProton"
msgstr "Запустить игру \"{name}\" с помощью PortProton"
@@ -259,13 +259,37 @@ msgstr "Удалить"
msgid "Select All"
msgstr "Выбрать всё"
#, python-brace-format
msgid "Launching {0}"
msgstr "Идёт запуск {0}"
msgid "Open"
msgstr "Открыть"
msgid "Select Dir"
msgstr "Выбрать папку"
msgid "Prev Dir"
msgstr "Предыдущий каталог"
msgid "Cancel"
msgstr "Отмена"
msgid "Toggle"
msgstr "Переключить"
msgid "Install"
msgstr "Установить"
msgid "Force Install"
msgstr "Принудительно установить"
msgid "Prev Tab"
msgstr "Предыдущая вкладка"
msgid "Next Tab"
msgstr "Следующая вкладка"
#, python-brace-format
msgid "Launching {0}"
msgstr "Идёт запуск {0}"
msgid "File Explorer"
msgstr "Проводник"
@@ -333,12 +357,6 @@ msgstr "Шрифты"
msgid "Settings"
msgstr "Настройки"
msgid "Force Install"
msgstr "Принудительно установить"
msgid "Install"
msgstr "Установить"
msgid "Winetricks not found. Please try again."
msgstr "Winetricks не найден. Повторите попытку."
@@ -354,6 +372,39 @@ msgstr "Установка не удалась. Проверьте журнал
msgid "Components installed successfully."
msgstr "Компоненты успешно установлены."
msgid "Exe Settings"
msgstr "Настройки EXE"
msgid "Main"
msgstr "Основные"
msgid "Advanced"
msgstr "Расширенные"
msgid "Setting"
msgstr "Параметр"
msgid "Value"
msgstr "Значение"
msgid "Description"
msgstr "Описание"
msgid "disabled"
msgstr "отключено"
msgid "Info"
msgstr "Информация"
msgid "No changes to apply."
msgstr "Изменений для применения нет."
msgid "Failed to apply changes. Check logs."
msgstr "Не удалось применить изменения. Проверьте логи."
msgid "Settings updated successfully."
msgstr "Настройки успешно обновлены."
msgid "Loading Epic Games Store games..."
msgstr "Загрузка игр из Epic Games Store..."
@@ -402,9 +453,6 @@ msgstr "Библиотека"
msgid "Auto Install"
msgstr "Автоустановка"
msgid "Emulators"
msgstr "Эмуляторы"
msgid "Wine Settings"
msgstr "Настройки wine"
@@ -423,6 +471,25 @@ msgstr "Полный экран"
msgid "Search"
msgstr "Поиск"
msgid "Installation already in progress."
msgstr "Установка уже выполняется."
msgid "Failed to start installation."
msgstr "Не удалось запустить установку."
#, python-brace-format
msgid "Processed {} installation..."
msgstr "В процессе установки {}..."
msgid "Installation completed successfully."
msgstr "Установка завершена успешно."
msgid "Installation failed."
msgstr "Установка не удалась."
msgid "Installation error."
msgstr "Ошибка установки."
msgid "Loading Steam games..."
msgstr "Загрузка игр из Steam..."
@@ -439,12 +506,6 @@ msgstr "Найти игры..."
msgid "Added '{name}'"
msgstr "'{name}' добавлен(а)"
msgid "Here you can configure automatic game installation..."
msgstr "Здесь можно настроить автоматическую установку игр..."
msgid "List of available emulators and their configuration..."
msgstr "Список доступных эмуляторов и их настройка..."
msgid "Compatibility tool:"
msgstr "Инструмент совместимости:"
@@ -457,12 +518,6 @@ msgstr "Конфигурация Wine"
msgid "Registry Editor"
msgstr "Редактор реестра"
msgid "Control Panel"
msgstr "Панель управления"
msgid "Task Manager"
msgstr "Диспетчер задач"
msgid "Command Prompt"
msgstr "Командная строка"
@@ -484,6 +539,36 @@ msgstr "Удалить Префикс"
msgid "Clear Prefix"
msgstr "Очистить Префикс"
msgid "Launching tool..."
msgstr "Запуск инструмента..."
msgid "Failed to start process."
msgstr "Не удалось запустить процесс."
msgid "Confirm Clear"
msgstr "Подтвердите очистку"
#, python-brace-format
msgid "Are you sure you want to clear prefix '{}'?"
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
msgid "Clearing prefix..."
msgstr "Очистка префикса..."
msgid "Failed to start prefix clear process."
msgstr "Не удалось запустить процесс очистки префикса."
msgid "Prefix cleared successfully."
msgstr "Префикс удален успешно."
#, python-brace-format
msgid "Prefix clear failed with exit code {}."
msgstr "Очистка префикса завершилась с кодом завершения {}."
#, python-brace-format
msgid "Failed to run clear prefix command: {}"
msgstr "Не удалось выполнить команду очистки префикса: {}"
msgid "Failed to start backup process."
msgstr "Не удалось запустить процесс резервного копирования."
@@ -559,6 +644,9 @@ msgstr "все"
msgid "Games Display Filter:"
msgstr "Фильтр игр:"
msgid "Gamepad Type:"
msgstr "Тип геймпада:"
msgid "Proxy URL"
msgstr "Адрес прокси"
@@ -583,6 +671,12 @@ msgstr "Запуск приложения в полноэкранном режи
msgid "Application Fullscreen Mode:"
msgstr "Режим полноэкранного отображения приложения:"
msgid "Minimize to tray on close"
msgstr "Сворачивать в трей при закрытии"
msgid "Application Close Mode:"
msgstr "Режим закрытия приложения:"
msgid "Auto Fullscreen on Gamepad connected"
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
@@ -659,6 +753,10 @@ msgstr "Тема '{0}' применена успешно"
msgid "Error applying theme '{0}'"
msgstr "Ошибка при применение темы '{0}'"
#, python-brace-format
msgid "Executable not found: {0}"
msgstr "Исполняемый файл не найден: {0}"
msgid "LAST LAUNCH"
msgstr "Последний запуск"
@@ -717,6 +815,288 @@ msgstr "Неправильный формат команды (flatpak)"
msgid "File not found: {0}"
msgstr "Файл не найден: {0}"
msgid ""
"Using FPS and system load monitoring (Turns on and off by the key "
"combination - right Shift + F12)"
msgstr ""
"Использование мониторинга FPS и нагрузки системы (включается и "
"выключается комбинацией клавиш - правая Shift + F12)"
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
msgstr "Принудительное использование системных настроек MANGOHUD (GOverlay и т.д.)"
msgid ""
"Enable vkBasalt by default to improve graphics in games running on "
"Vulkan. (The HOME hotkey disables vkbasalt)"
msgstr ""
"Включить vkBasalt по умолчанию для улучшения графики в играх на Vulkan. "
"(Горячая клавиша HOME отключает vkbasalt)"
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
msgstr "Принудительное использование системных настроек VKBASALT (GOverlay и т.д.)"
msgid ""
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
msgstr ""
"Включить dgVoodoo2. Принудительное использование всех библиотек dgVoodoo2"
" (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) на всех 3D API."
msgid ""
"Super + F : Toggle fullscreen\n"
"Super + N : Toggle nearest neighbour filtering\n"
"Super + U : Toggle FSR upscaling\n"
"Super + Y : Toggle NIS upscaling\n"
"Super + I : Increase FSR sharpness by 1\n"
"Super + O : Decrease FSR sharpness by 1\n"
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
"Super + G : Toggle keyboard grab\n"
"Super + C : Update clipboard"
msgstr ""
"Super + F: Переключить полноэкранный режим\n"
"Super + N: Переключить фильтрацию ближайшего соседа\n"
"Super + U: Переключить апскейлинг FSR\n"
"Super + Y: Переключить апскейлинг NIS\n"
"Super + I: Увеличить резкость FSR на 1\n"
"Super + O: Уменьшить резкость FSR на 1\n"
"Super + S: Сделать скриншот (сейчас сохраняется в "
"/tmp/gamescope_DATE.png)\n"
"Super + G: Переключить захват клавиатуры\n"
"Super + C: Обновить буфер обмена"
msgid "Enable in-process synchronization primitives based on eventfd."
msgstr "Включить примитивы синхронизации в процессе на основе eventfd."
msgid "Enable futex-based in-process synchronization primitives."
msgstr "Включить примитивы синхронизации в процессе на основе futex."
msgid "Enable in-process synchronization via the Linux ntsync driver."
msgstr "Включить синхронизацию в процессе через драйвер ntsync в Linux."
msgid "Enable vkd3d support - Ray Tracing"
msgstr "Включить поддержку vkd3d — трассировка лучей"
msgid "Enable DLSS on supported NVIDIA graphics cards"
msgstr "Включить DLSS на поддерживаемых видеокартах NVIDIA"
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
msgstr "Включить OptiScaler (замена апскейлера / генератора кадров)"
msgid "Enable Lossless Scaling frame generation (experimental)"
msgstr "Включить генерацию кадров Lossless Scaling (экспериментально)"
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
msgstr "Апскейлинг FSR в полноэкранном режиме с ProtonGE ниже родного разрешения"
msgid "Disguise all NVIDIA GPU features"
msgstr "Маскировать все функции GPU NVIDIA"
msgid "Run the application in WINE virtual desktop"
msgstr "Запускать приложение в виртуальном рабочем столе WINE"
msgid "Run the application in a terminal"
msgstr "Запускать приложение в терминале"
msgid "Disable startup mode and WINE version selector window"
msgstr "Отключить окно выбора режима запуска и версии WINE"
msgid "Use system GameMode for performance optimization"
msgstr "Использовать системный GameMode для оптимизации производительности"
msgid "Enable forced use of third-party DirectX libraries"
msgstr "Включить принудительное использование сторонних библиотек DirectX"
msgid "Fix pink-tinted video playback in some games"
msgstr "Исправить розовый оттенок видео в некоторых играх"
msgid "Reduce PulseAudio latency to fix intermittent sound"
msgstr "Уменьшить задержку PulseAudio для исправления прерывистого звука"
msgid "Force US keyboard layout"
msgstr "Принудительно использовать раскладку клавиатуры US"
msgid "Use GStreamer for in-game clips (WMF support)"
msgstr "Использовать GStreamer для внутриигровых клипов (поддержка WMF)"
msgid "Use WINE shader caching"
msgstr "Использовать кэширование шейдеров WINE"
msgid "Force use of built-in DXGI library"
msgstr "Принудительно использовать встроенную библиотеку DXGI"
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
msgstr "Включить среды выполнения Easy Anti-Cheat и BattlEye"
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
msgstr "Использовать системные слои Vulkan (MangoHud, vkBasalt, OBS и т.д.)"
msgid "Enable OBS Studio capture via obs-vkcapture"
msgstr "Включить захват OBS Studio через obs-vkcapture"
msgid "Disable desktop compositing for performance"
msgstr "Отключить композицию рабочего стола для производительности"
msgid "Use container launch mode (recommended default)"
msgstr "Использовать режим запуска в контейнере (рекомендуемый по умолчанию)"
msgid "Force DirectInput protocol instead of XInput"
msgstr "Принудительно использовать протокол DirectInput вместо XInput"
msgid "Enable experimental native Wayland support"
msgstr "Включить экспериментальную нативную поддержку Wayland"
msgid "Enable HDR settings under native Wayland"
msgstr "Включить настройки HDR под нативным Wayland"
msgid "Use Gallium Zink (OpenGL via Vulkan)"
msgstr "Использовать Gallium Zink (OpenGL через Vulkan)"
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
msgstr "Использовать Gallium Nine (нативный DirectX 9 для Mesa)"
msgid "Use WineD3D Vulkan backend (Damavand)"
msgstr "Использовать бэкенд Vulkan WineD3D (Damavand)"
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
msgstr "Использовать встроенные dxvk/vkd3d из Wine/Proton"
msgid "Use async dxvk-sarek (experimental)"
msgstr "Использовать асинхронный dxvk-sarek (экспериментально)"
msgid "Windows version"
msgstr "Версия Windows"
msgid ""
"Changing the WINDOWS emulation version may be required to run older "
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
msgstr ""
"Изменение версии эмуляции WINDOWS может потребоваться для запуска старых "
"игр. Версии WINDOWS ниже 10 не поддерживают новые игры с DirectX 12"
msgid "DLL Overrides"
msgstr "Переопределения DLL"
msgid ""
"Forced to use/disable the library only for the given application.\n"
"\n"
"A brief instruction:\n"
"* libraries are written WITHOUT the .dll file extension\n"
"* libraries are separated by semicolons - ;\n"
"* library=n - use the WINDOWS (third-party) library\n"
"* library=b - use WINE (built-in) library\n"
"* library=n,b - use WINDOWS library and then WINE\n"
"* library=b,n - use WINE library and then WINDOWS\n"
"* library= - disable the use of this library\n"
"\n"
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgstr ""
"Принудительное использование/отключение библиотеки только для данного "
"приложения.\n"
"\n"
"Краткая инструкция:\n"
"* библиотеки пишутся БЕЗ расширения .dll\n"
"* библиотеки разделяются точкой с запятой - ;\n"
"* library=n — использовать библиотеку WINDOWS (стороннюю)\n"
"* library=b — использовать библиотеку WINE (встроенную)\n"
"* library=n,b — использовать библиотеку WINDOWS, затем WINE\n"
"* library=b,n — использовать библиотеку WINE, затем WINDOWS\n"
"* library= — отключить использование этой библиотеки\n"
"\n"
"Пример: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
msgid "Launch Arguments"
msgstr "Аргументы запуска"
msgid ""
"Adding an argument after the .exe file, just like you would add an "
"argument in a shortcut on a WINDOWS system.\n"
"\n"
"Example: -dx11 -skipintro 1"
msgstr ""
"Добавление аргумента после файла .exe, как вы бы добавили аргумент в "
"ярлыке на системе WINDOWS.\n"
"\n"
"Пример: -dx11 -skipintro 1"
msgid "CPU Cores Limit"
msgstr "Ограничение ядер CPU"
msgid ""
"Limiting the number of CPU cores is useful for Unity games (It is "
"recommended to set the value equal to 8)"
msgstr ""
"Ограничение количества ядер CPU полезно для игр на Unity (рекомендуется "
"установить значение равным 8)"
msgid "OpenGL Version"
msgstr "Версия OpenGL"
msgid ""
"You can select the required OpenGL version, some games require a forced "
"Compatibility Profile (COMP)."
msgstr ""
"Вы можете выбрать требуемую версию OpenGL, некоторые игры требуют "
"принудительного профиля совместимости (COMP)."
msgid "VKD3D Feature Level"
msgstr "Уровень возможностей VKD3D"
msgid "You can set a forced feature level VKD3D for games on DirectX12"
msgstr ""
"Вы можете установить принудительный уровень возможностей VKD3D для игр на"
" DirectX12"
msgid "Locale"
msgstr "Локаль"
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
msgstr ""
"Принудительно установить определённую локаль для приложения. Исправляет "
"проблемы с кодировкой в устаревшем ПО"
msgid "Window Mode"
msgstr "Режим окна"
msgid ""
"Window mode (for Vulkan and OpenGL):\n"
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
"immediate - Unlimited frame rate + tearing.\n"
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
" rate."
msgstr ""
"Режим окна (для Vulkan и OpenGL):\n"
"fifo — Первый вошёл, первый вышел. Ограничивает частоту кадров + без "
"разрывов. (VSync)\n"
"immediate — Неограниченная частота кадров + разрывы.\n"
"mailbox — Трёхбуферная. Неограниченная частота кадров + без разрывов.\n"
"relaxed — То же, что fifo, но позволяет разрывы при частоте ниже частоты "
"обновления монитора."
msgid "AMD Vulkan Driver"
msgstr "Драйвер Vulkan AMD"
msgid ""
"Select needed AMD vulkan implementation. Choosing which implementation of"
" vulkan will be used to run the game"
msgstr ""
"Выберите нужную реализацию Vulkan AMD. Выбор, какая реализация Vulkan "
"будет использоваться для запуска игры"
msgid "NUMA Node"
msgstr "Узел NUMA"
msgid ""
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
" single node reduces memory-access latency and limits costly core-to-core"
" switches."
msgstr ""
"Узел NUMA для аффинности CPU. В многоядерных системах CPU разделены на "
"узлы NUMA, каждый со своей локальной памятью и ядрами. Привязка игры к "
"одному узлу уменьшает задержку доступа к памяти и ограничивает "
"дорогостоящие переключения между ядрами."
msgid "Reboot"
msgstr "Перезагрузить"

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,18 @@ import orjson
import requests
import urllib.parse
import time
import glob
import re
import hashlib
from collections.abc import Callable
from PySide6.QtCore import QThread, Signal
from portprotonqt.downloader import Downloader
from portprotonqt.logger import get_logger
from portprotonqt.config_utils import get_portproton_location
logger = get_logger(__name__)
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
AUTOINSTALL_CACHE_DURATION = 3600 # 1 hour for autoinstall cache
def normalize_name(s):
"""
@@ -52,7 +58,11 @@ class PortProtonAPI:
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
os.makedirs(self.custom_data_dir, exist_ok=True)
self.portproton_location = get_portproton_location()
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
self._topics_data = None
self._autoinstall_cache = None # New: In-memory cache
def _get_game_dir(self, exe_name: str) -> str:
game_dir = os.path.join(self.custom_data_dir, exe_name)
@@ -68,40 +78,6 @@ class PortProtonAPI:
logger.debug(f"Failed to check file at {url}: {e}")
return False
def download_game_assets(self, exe_name: str, timeout: int = 5) -> dict[str, str | None]:
game_dir = self._get_game_dir(exe_name)
results: dict[str, str | None] = {"cover": None, "metadata": None}
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
cover_url_base = f"{self.base_url}/{exe_name}/cover"
metadata_url = f"{self.base_url}/{exe_name}/metadata.txt"
for ext in cover_extensions:
cover_url = f"{cover_url_base}{ext}"
if self._check_file_exists(cover_url, timeout):
local_cover_path = os.path.join(game_dir, f"cover{ext}")
result = self.downloader.download(cover_url, local_cover_path, timeout=timeout)
if result:
results["cover"] = result
logger.info(f"Downloaded cover for {exe_name} to {result}")
break
else:
logger.error(f"Failed to download cover for {exe_name} from {cover_url}")
else:
logger.debug(f"No cover found for {exe_name} with extension {ext}")
if self._check_file_exists(metadata_url, timeout):
local_metadata_path = os.path.join(game_dir, "metadata.txt")
result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout)
if result:
results["metadata"] = result
logger.info(f"Downloaded metadata for {exe_name} to {result}")
else:
logger.error(f"Failed to download metadata for {exe_name} from {metadata_url}")
else:
logger.debug(f"No metadata found for {exe_name}")
return results
def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None:
game_dir = self._get_game_dir(exe_name)
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
@@ -163,6 +139,236 @@ class PortProtonAPI:
if callback:
callback(results)
def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None:
"""Download only autoinstall cover image (PNG only, no metadata)."""
xdg_data_home = os.getenv("XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share"))
autoinstall_root = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
user_game_folder = os.path.join(autoinstall_root, exe_name)
if not os.path.isdir(user_game_folder):
try:
os.mkdir(user_game_folder)
except FileExistsError:
pass
cover_url = f"{self.base_url}/{exe_name}/cover.png"
local_cover_path = os.path.join(user_game_folder, "cover.png")
def on_cover_downloaded(local_path: str | None):
if local_path:
logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}")
else:
logger.debug(f"No autoinstall cover downloaded for {exe_name}")
if callback:
callback(local_path)
if self._check_file_exists(cover_url, timeout):
self.downloader.download_async(
cover_url,
local_cover_path,
timeout=timeout,
callback=on_cover_downloaded
)
else:
logger.debug(f"No autoinstall cover found for {exe_name}")
if callback:
callback(None)
def parse_autoinstall_script(self, file_path: str) -> tuple[str | None, str | None]:
"""Extract display_name from # name comment and exe_name from autoinstall bash script."""
try:
with open(file_path, encoding='utf-8') as f:
content = f.read()
# Skip emulators
if re.search(r'#\s*type\s*:\s*emulators', content, re.IGNORECASE):
return None, None
display_name = None
exe_name = None
# Extract display_name from "# name:" comment
name_match = re.search(r'#\s*name\s*:\s*(.+)', content, re.IGNORECASE)
if name_match:
display_name = name_match.group(1).strip()
# --- pw_create_unique_exe ---
pw_match = re.search(r'pw_create_unique_exe(?:\s+["\']([^"\']+)["\'])?', content)
if pw_match:
arg = pw_match.group(1)
if arg:
exe_name = arg.strip()
if not exe_name.lower().endswith(".exe"):
exe_name += ".exe"
else:
export_match = re.search(
r'export\s+PORTWINE_CREATE_SHORTCUT_NAME\s*=\s*["\']([^"\']+)["\']',
content, re.IGNORECASE)
if export_match:
exe_name = f"{export_match.group(1).strip()}.exe"
else:
portwine_match = None
for line in content.splitlines():
stripped = line.strip()
if stripped.startswith("#"):
continue
if "portwine_exe" in stripped and "=" in stripped:
portwine_match = stripped
break
if portwine_match:
exe_expr = portwine_match.split("=", 1)[1].strip().strip("'\" ")
exe_candidates = re.findall(r'[-\w\s/\\\.]+\.exe', exe_expr)
if exe_candidates:
exe_name = os.path.basename(exe_candidates[-1].strip())
# Fallback
if not display_name and exe_name:
display_name = exe_name
return display_name, exe_name
except Exception as e:
logger.error(f"Failed to parse {file_path}: {e}")
return None, None
def _compute_scripts_signature(self, auto_dir: str) -> str:
"""Compute a hash-based signature of the autoinstall scripts to detect changes."""
if not os.path.exists(auto_dir):
return ""
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
# Simple hash: concatenate sorted filenames and hash
filenames_str = "".join(sorted([os.path.basename(s) for s in scripts]))
return hashlib.md5(filenames_str.encode()).hexdigest()
def _load_autoinstall_cache(self):
"""Load cached autoinstall games if fresh and scripts unchanged."""
if self._autoinstall_cache is not None:
return self._autoinstall_cache
cache_dir = get_cache_dir()
cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json")
if os.path.exists(cache_file):
try:
mod_time = os.path.getmtime(cache_file)
if time.time() - mod_time < AUTOINSTALL_CACHE_DURATION:
with open(cache_file, "rb") as f:
data = orjson.loads(f.read())
# Check signature
cached_signature = data.get("scripts_signature", "")
current_signature = self._compute_scripts_signature(
os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
)
if cached_signature != current_signature:
logger.info("Scripts signature mismatch; invalidating cache")
return None
self._autoinstall_cache = data["games"]
logger.info(f"Loaded {len(self._autoinstall_cache)} cached autoinstall games")
return self._autoinstall_cache
except Exception as e:
logger.error(f"Failed to load autoinstall cache: {e}")
return None
def _save_autoinstall_cache(self, games):
"""Save parsed autoinstall games to cache with scripts signature."""
try:
cache_dir = get_cache_dir()
cache_file = os.path.join(cache_dir, "autoinstall_games_cache.json")
auto_dir = os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
scripts_signature = self._compute_scripts_signature(auto_dir)
data = {"games": games, "scripts_signature": scripts_signature, "timestamp": time.time()}
with open(cache_file, "wb") as f:
f.write(orjson.dumps(data))
logger.debug(f"Saved {len(games)} autoinstall games to cache with signature {scripts_signature}")
except Exception as e:
logger.error(f"Failed to save autoinstall cache: {e}")
def start_autoinstall_games_load(self, callback: Callable[[list[tuple]], None]) -> QThread | None:
"""Start loading auto-install games in a background thread. Returns the thread for management."""
# Check cache first (sync, fast)
cached_games = self._load_autoinstall_cache()
if cached_games is not None:
# Emit via callback immediately if cached
QThread.msleep(0) # Yield to Qt event loop
callback(cached_games)
return None # No thread needed
# No cache: Start background thread
class AutoinstallWorker(QThread):
finished = Signal(list)
api: "PortProtonAPI"
portproton_location: str | None
def run(self):
games = []
auto_dir = os.path.join(
self.portproton_location or "", "data", "scripts", "pw_autoinstall"
) if self.portproton_location else ""
if not os.path.exists(auto_dir):
self.finished.emit(games)
return
scripts = sorted(glob.glob(os.path.join(auto_dir, "*")))
if not scripts:
self.finished.emit(games)
return
xdg_data_home = os.getenv(
"XDG_DATA_HOME",
os.path.join(os.path.expanduser("~"), ".local", "share"),
)
base_autoinstall_dir = os.path.join(
xdg_data_home, "PortProtonQt", "custom_data", "autoinstall"
)
os.makedirs(base_autoinstall_dir, exist_ok=True)
for script_path in scripts:
display_name, exe_name = self.api.parse_autoinstall_script(script_path)
script_name = os.path.splitext(os.path.basename(script_path))[0]
if not (display_name and exe_name):
continue
exe_name = os.path.splitext(exe_name)[0]
user_game_folder = os.path.join(base_autoinstall_dir, exe_name)
os.makedirs(user_game_folder, exist_ok=True)
# Find cover
cover_path = ""
user_files = (
set(os.listdir(user_game_folder))
if os.path.exists(user_game_folder)
else set()
)
for ext in [".jpg", ".png", ".jpeg", ".bmp"]:
candidate = f"cover{ext}"
if candidate in user_files:
cover_path = os.path.join(user_game_folder, candidate)
break
if not cover_path:
logger.debug(f"No local cover found for autoinstall {exe_name}")
game_tuple = (
display_name, "", cover_path, "", f"autoinstall:{script_name}",
"", "Never", "0h 0m", "", "", 0, 0, "autoinstall", exe_name
)
games.append(game_tuple)
self.api._save_autoinstall_cache(games)
self.api._autoinstall_cache = games
self.finished.emit(games)
worker = AutoinstallWorker()
worker.api = self
worker.portproton_location = self.portproton_location
worker.finished.connect(lambda games: callback(games))
worker.start()
logger.info("Started background load of autoinstall games")
return worker
def _load_topics_data(self):
"""Load and cache linux_gaming_topics_min.json from the archive."""
if self._topics_data is not None:

View File

@@ -0,0 +1,241 @@
def get_toggle_settings():
"""Get predefined toggle settings with descriptions."""
from portprotonqt.localization import _
return {
'PW_MANGOHUD': _("Using FPS and system load monitoring (Turns on and off by the key combination - right Shift + F12)"),
'PW_MANGOHUD_USER_CONF': _("Forced use of MANGOHUD system settings (GOverlay, etc.)"),
'PW_VKBASALT': _("Enable vkBasalt by default to improve graphics in games running on Vulkan. (The HOME hotkey disables vkbasalt)"),
'PW_VKBASALT_USER_CONF': _("Forced use of VKBASALT system settings (GOverlay, etc.)"),
'PW_DGVOODOO2': _("Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) on all 3D API."),
'PW_GAMESCOPE': _("Super + F : Toggle fullscreen\nSuper + N : Toggle nearest neighbour filtering\nSuper + U : Toggle FSR upscaling\nSuper + Y : Toggle NIS upscaling\nSuper + I : Increase FSR sharpness by 1\nSuper + O : Decrease FSR sharpness by 1\nSuper + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\nSuper + G : Toggle keyboard grab\nSuper + C : Update clipboard"),
'PW_USE_ESYNC': _("Enable in-process synchronization primitives based on eventfd."),
'PW_USE_FSYNC': _("Enable futex-based in-process synchronization primitives."),
'PW_USE_NTSYNC': _("Enable in-process synchronization via the Linux ntsync driver."),
'PW_USE_RAY_TRACING': _("Enable vkd3d support - Ray Tracing"),
'PW_USE_NVAPI_AND_DLSS': _("Enable DLSS on supported NVIDIA graphics cards"),
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
'PW_HIDE_NVIDIA_GPU': _("Disguise all NVIDIA GPU features"),
'PW_VIRTUAL_DESKTOP': _("Run the application in WINE virtual desktop"),
'PW_USE_TERMINAL': _("Run the application in a terminal"),
'PW_GUI_DISABLED_CS': _("Disable startup mode and WINE version selector window"),
'PW_USE_GAMEMODE': _("Use system GameMode for performance optimization"),
'PW_USE_D3D_EXTRAS': _("Enable forced use of third-party DirectX libraries"),
'PW_FIX_VIDEO_IN_GAME': _("Fix pink-tinted video playback in some games"),
'PW_REDUCE_PULSE_LATENCY': _("Reduce PulseAudio latency to fix intermittent sound"),
'PW_USE_US_LAYOUT': _("Force US keyboard layout"),
'PW_USE_GSTREAMER': _("Use GStreamer for in-game clips (WMF support)"),
'PW_USE_SHADER_CACHE': _("Use WINE shader caching"),
'PW_USE_WINE_DXGI': _("Force use of built-in DXGI library"),
'PW_USE_EAC_AND_BE': _("Enable Easy Anti-Cheat and BattlEye runtimes"),
'PW_USE_SYSTEM_VK_LAYERS': _("Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"),
'PW_USE_OBS_VKCAPTURE': _("Enable OBS Studio capture via obs-vkcapture"),
'PW_DISABLE_COMPOSITING': _("Disable desktop compositing for performance"),
'PW_USE_RUNTIME': _("Use container launch mode (recommended default)"),
'PW_DINPUT_PROTOCOL': _("Force DirectInput protocol instead of XInput"),
'PW_USE_NATIVE_WAYLAND': _("Enable experimental native Wayland support"),
'PW_USE_DXVK_HDR': _("Enable HDR settings under native Wayland"),
'PW_USE_GALLIUM_ZINK': _("Use Gallium Zink (OpenGL via Vulkan)"),
'PW_USE_GALLIUM_NINE': _("Use Gallium Nine (native DirectX 9 for Mesa)"),
'PW_USE_WINED3D_VULKAN': _("Use WineD3D Vulkan backend (Damavand)"),
'PW_USE_SUPPLIED_DXVK_VKD3D': _("Use bundled dxvk/vkd3d from Wine/Proton"),
'PW_USE_SAREK_ASYNC': _("Use async dxvk-sarek (experimental)")
}
def get_advanced_settings(disabled_text, logical_core_options, locale_options,
amd_vulkan_drivers, is_amd, numa_nodes, dist_options=None, prefix_options=None):
"""Get advanced settings configuration."""
from portprotonqt.localization import _
advanced_settings = []
if dist_options is None:
dist_options = []
if prefix_options is None:
prefix_options = []
# 1. Wine Version
advanced_settings.append({
'key': 'PW_WINE_USE',
'name': _("Wine Version"),
'description': _("Select the Wine or Proton version to use for this executable."),
'type': 'combo',
'options': dist_options,
'default': ''
})
# 2. Prefix Name
advanced_settings.append({
'key': 'PW_PREFIX_NAME',
'name': _("Prefix Name"),
'description': _("Select the Wine prefix to use."),
'type': 'combo',
'options': prefix_options,
'default': 'DEFAULT'
})
# 3. Vulkan Backend
vulkan_options = [
_("Auto latest DXVK + VKD3D (recommended)"), # → 6
_("Stable proven DXVK + VKD3D"), # → 2
_("Sarek experimental DXVK-Sarek + VKD3D-Sarek"), # → 1
_("WINED3D OpenGL (fallback only)") # → 0
]
# Маппинг: отображаемый текст → реальное значение в ppdb
vulkan_value_map = {
vulkan_options[0]: "6",
vulkan_options[1]: "2",
vulkan_options[2]: "1",
vulkan_options[3]: "0",
}
advanced_settings.append({
'key': 'PW_VULKAN_USE',
'name': _("Vulkan Backend"),
'description': _(
"Select the rendering backend for translating DirectX → Vulkan/OpenGL:\n\n"
"• Auto latest DXVK + VKD3D (recommended)\n"
" The newest versions from the developers. Give the best compatibility and performance in modern games.\n"
" Require up-to-date drivers:\n"
" AMD: Mesa 25.0+ or proprietary AMDVLK 2024.Q4+\n"
" NVIDIA: driver 550.54.14 or newer\n"
" Intel: Mesa 24.2+\n\n"
"• Stable proven DXVK + VKD3D\n"
" Older but extremely well-tested versions. Work on any drivers that support Vulkan 1.3+.\n"
" The best choice if you have problems with the newest versions.\n\n"
"• Sarek experimental DXVK-Sarek + VKD3D-Sarek\n"
" Work even on older drivers and video cards that support at least Vulkan 1.1.\n\n"
"• WINED3D OpenGL translation (fallback)\n"
" No DXVK/VKD3D used. DirectX is translated to OpenGL via built-in WineD3D.\n"
" Works on absolutely any hardware, but performance is significantly lower.\n"
" Use only as a last resort when nothing else starts."
),
'type': 'combo',
'options': vulkan_options,
'default': '6',
'_value_map': vulkan_value_map
})
# 4. Windows version
advanced_settings.append({
'key': 'PW_WINDOWS_VER',
'name': _("Windows version"),
'description': _("Changing the WINDOWS emulation version may be required to run older games. WINDOWS versions below 10 do not support new games with DirectX 12"),
'type': 'combo',
'options': ['11', '10', '7', 'XP'],
'default': '10'
})
# 5. DLL Overrides
advanced_settings.append({
'key': 'WINEDLLOVERRIDES',
'name': _("DLL Overrides"),
'description': _("Forced to use/disable the library only for the given application.\n\nA brief instruction:\n* libraries are written WITHOUT the .dll file extension\n* libraries are separated by semicolons - ;\n* library=n - use the WINDOWS (third-party) library\n* library=b - use WINE (built-in) library\n* library=n,b - use WINDOWS library and then WINE\n* library=b,n - use WINE library and then WINDOWS\n* library= - disable the use of this library\n\nExample: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"),
'type': 'text',
'default': ''
})
# 6. Launch arguments
advanced_settings.append({
'key': 'LAUNCH_PARAMETERS',
'name': _("Launch Arguments"),
'description': _("Adding an argument after the .exe file, just like you would add an argument in a shortcut on a WINDOWS system.\n\nExample: -dx11 -skipintro 1"),
'type': 'text',
'default': ''
})
# 7. CPU cores limit
advanced_settings.append({
'key': 'PW_WINE_CPU_TOPOLOGY',
'name': _("CPU Cores Limit"),
'description': _("Limiting the number of CPU cores is useful for Unity games (It is recommended to set the value equal to 8)"),
'type': 'combo',
'options': [disabled_text] + logical_core_options,
'default': disabled_text
})
# 8. OpenGL version
advanced_settings.append({
'key': 'PW_MESA_GL_VERSION_OVERRIDE',
'name': _("OpenGL Version"),
'description': _("You can select the required OpenGL version, some games require a forced Compatibility Profile (COMP)."),
'type': 'combo',
'options': [disabled_text, '4.6COMPAT', '4.5COMPAT', '4.3COMPAT', '4.1COMPAT', '3.3COMPAT', '3.2COMPAT'],
'default': disabled_text
})
# 9. VKD3D feature level
advanced_settings.append({
'key': 'PW_VKD3D_FEATURE_LEVEL',
'name': _("VKD3D Feature Level"),
'description': _("You can set a forced feature level VKD3D for games on DirectX12"),
'type': 'combo',
'options': [disabled_text, '12_2', '12_1', '12_0', '11_1', '11_0'],
'default': disabled_text
})
# 10. Locale
advanced_settings.append({
'key': 'PW_LOCALE_SELECT',
'name': _("Locale"),
'description': _("Force certain locale for an app. Fixes encoding issues in legacy software"),
'type': 'combo',
'options': [disabled_text] + locale_options,
'default': disabled_text
})
# 11. Present mode
advanced_settings.append({
'key': 'PW_MESA_VK_WSI_PRESENT_MODE',
'name': _("Window Mode"),
'description': _("Window mode (for Vulkan and OpenGL):\nfifo - First in, first out. Limits the frame rate + no tearing. (VSync)\nimmediate - Unlimited frame rate + tearing.\nmailbox - Triple buffering. Unlimited frame rate + no tearing.\nrelaxed - Same as fifo but allows tearing when below the monitors refresh rate."),
'type': 'combo',
'options': [disabled_text, 'fifo', 'immediate', 'mailbox', 'relaxed'],
'default': disabled_text
})
# 12. AMD Vulkan driver
amd_options = [disabled_text] + amd_vulkan_drivers if is_amd and amd_vulkan_drivers else [disabled_text]
advanced_settings.append({
'key': 'PW_AMD_VULKAN_USE',
'name': _("AMD Vulkan Driver"),
'description': _("Select needed AMD vulkan implementation. Choosing which implementation of vulkan will be used to run the game"),
'type': 'combo',
'options': amd_options,
'default': disabled_text
})
# 13. NUMA node
numa_ids = sorted(numa_nodes.keys())
numa_options = [disabled_text] + numa_ids if len(numa_ids) > 1 else [disabled_text]
advanced_settings.append({
'key': 'PW_CPU_NUMA_NODE_INDEX',
'name': _("NUMA Node"),
'description': _("NUMA node for CPU affinity. In multi-core systems, CPUs are split into NUMA nodes, each with its own local memory and cores. Binding a game to a single node reduces memory-access latency and limits costly core-to-core switches."),
'type': 'combo',
'options': numa_options,
'default': disabled_text
})
return advanced_settings
# Keys that should be recognized as advanced settings
ADVANCED_SETTING_KEYS = [
'PW_WINE_USE',
'PW_PREFIX_NAME',
'PW_VULKAN_USE',
'PW_WINDOWS_VER',
'WINEDLLOVERRIDES',
'LAUNCH_PARAMETERS',
'PW_WINE_CPU_TOPOLOGY',
'PW_MESA_GL_VERSION_OVERRIDE',
'PW_VKD3D_FEATURE_LEVEL',
'PW_LOCALE_SELECT',
'PW_MESA_VK_WSI_PRESENT_MODE',
'PW_AMD_VULKAN_USE',
'PW_CPU_NUMA_NODE_INDEX',
]

View File

@@ -13,7 +13,7 @@ from portprotonqt.logger import get_logger
from portprotonqt.localization import get_steam_language
from portprotonqt.downloader import Downloader
from portprotonqt.dialogs import generate_thumbnail
from portprotonqt.config_utils import get_portproton_location
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
from collections.abc import Callable
import re
import shutil
@@ -23,6 +23,7 @@ import requests
import random
import base64
import glob
import urllib.parse
downloader = Downloader()
logger = get_logger(__name__)
@@ -411,6 +412,39 @@ def save_app_details(app_id, data):
with open(cache_file, "wb") as f:
f.write(orjson.dumps(data))
def fetch_sgdb_cover(game_name: str) -> str:
"""
Fetch a cover image URL from steamgrid.usebottles.com for the given game.
The API returns a single string (quoted URL).
"""
try:
encoded = urllib.parse.quote(game_name)
url = f"https://steamgrid.usebottles.com/api/search/{encoded}"
resp = requests.get(url, timeout=5)
if resp.status_code != 200:
logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code)
return ""
text = resp.text.strip()
# Убираем возможные кавычки вокруг строки
if text.startswith('"') and text.endswith('"'):
text = text[1:-1]
if text:
logger.info("Fetched SGDB cover for %s: %s", game_name, text)
return text
except Exception as e:
logger.warning("Failed to fetch SGDB cover for %s: %s", game_name, e)
return ""
def check_url_exists(url: str) -> bool:
"""Check whether a URL returns HTTP 200."""
try:
r = requests.head(url, timeout=3)
return r.status_code == 200
except Exception:
return False
def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
"""
Asynchronously fetches detailed app info from Steam API.
@@ -629,6 +663,11 @@ def get_full_steam_game_info_async(appid: int, callback: Callable[[dict], None])
title = decode_text(app_info.get("name", ""))
description = decode_text(app_info.get("short_description", ""))
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
if not check_url_exists(cover):
logger.info("Steam cover not found for %s, trying SGDB", title)
alt_cover = fetch_sgdb_cover(title)
if alt_cover:
cover = alt_cover
def on_protondb_tier(tier: str):
def on_anticheat_status(anticheat_status: str):
@@ -722,12 +761,15 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
game_name = desktop_name or exe_name.capitalize()
if not matching_app:
cover = fetch_sgdb_cover(game_name) or ""
logger.info("Using SGDB cover for non-Steam game '%s': %s", game_name, cover)
def on_anticheat_status(anticheat_status: str):
callback({
"appid": "",
"name": decode_text(game_name),
"description": "",
"cover": "",
"cover": cover,
"controller_support": "",
"protondb_tier": "",
"steam_game": "false",
@@ -758,6 +800,11 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
title = decode_text(app_info.get("name", game_name))
description = decode_text(app_info.get("short_description", ""))
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
if not check_url_exists(cover):
logger.info("Steam cover not found for %s, trying SGDB", title)
alt_cover = fetch_sgdb_cover(title)
if alt_cover:
cover = alt_cover
controller_support = app_info.get("controller_support", "")
def on_protondb_tier(tier: str):
@@ -957,7 +1004,8 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
return (False, f"Executable file not found: {exe_path}")
portproton_dir = get_portproton_location()
if not portproton_dir:
start_sh = get_portproton_start_command()
if not portproton_dir or not start_sh:
logger.error("PortProton directory not found")
return (False, "PortProton directory not found")
@@ -966,17 +1014,12 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
if not os.path.exists(start_sh_path):
logger.error(f"start.sh not found at {start_sh_path}")
return (False, f"start.sh not found at {start_sh_path}")
if not os.path.exists(script_path):
script_content = f"""#!/usr/bin/env bash
export LD_PRELOAD=
export START_FROM_STEAM=1
"{start_sh_path}" "{exe_path}" "$@"
"{start_sh}" "{exe_path}" "$@"
"""
try:
with open(script_path, "w", encoding="utf-8") as f:

View File

@@ -5,6 +5,9 @@ from portprotonqt.logger import get_logger
from PySide6.QtGui import QIcon, QFontDatabase, QPixmap
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
# Icon caching for performance optimization
_icon_cache = {}
logger = get_logger(__name__)
# Папка, где располагаются все дополнительные темы
@@ -232,6 +235,14 @@ class ThemeManager:
а если файл не найден, то из стандартной темы.
Если as_path=True, возвращает путь к иконке вместо QIcon.
"""
# Create cache key
cache_key = f"{icon_name}_{theme_name or self.current_theme_name}_{as_path}"
# Check if we already have this icon cached
if cache_key in _icon_cache:
logger.debug(f"Using cached icon for {icon_name}")
return _icon_cache[cache_key]
icon_path = None
theme_name = theme_name or self.current_theme_name
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
@@ -279,12 +290,20 @@ class ThemeManager:
# Если иконка всё равно не найдена
if not icon_path or not os.path.exists(icon_path):
logger.error(f"Warning: icon '{icon_name}' not found")
return QIcon() if not as_path else None
result = QIcon() if not as_path else None
# Cache the result even if it's None
_icon_cache[cache_key] = result
return result
if as_path:
# Cache the path
_icon_cache[cache_key] = icon_path
return icon_path
return QIcon(icon_path)
# Create QIcon and cache it
icon = QIcon(icon_path)
_icon_cache[cache_key] = icon
return icon
def get_theme_image(self, image_name, theme_name=None):
"""

View File

@@ -0,0 +1 @@
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m8.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -968,9 +968,8 @@ SETTINGS_CHECKBOX_STYLE = f"""
WINETRICKS_TAB_STYLE = f"""
QTabWidget::pane {{
border: 1px solid {color_d};
background: {color_b};
border-radius: {border_radius_a};
border-top: 1px solid {color_c};
background: {color_h};
}}
QTabBar::tab {{
background: {color_c};
@@ -985,15 +984,118 @@ QTabBar::tab:selected {{
color: {color_f};
}}
QTabBar::tab:hover {{
background: {color_e};
background: {color_a};
}}
"""
WINETRICKS_TABBLE_STYLE = f"""
QTableWidget {{
QComboBox {{
background: {color_c};
border: {border_c} {color_g};
border-radius: {border_radius_a};
padding-left: 12px;
color: {color_f};
gridline-color: {color_d};
font-family: '{font_family}';
font-size: {font_size_a};
min-width: 120px;
combobox-popup: 0;
}}
QComboBox:on {{
background: {color_b};
border: {border_c} {color_a};
border-bottom-style: none;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-left-radius: 0px;
border-bottom-right-radius: 0px;
}}
QComboBox:hover {{
border: {border_c} {color_a};
background: {color_a};
}}
/* Состояние фокуса */
QComboBox:focus {{
border: {border_c} {color_a};
background-color: {color_a};
}}
QComboBox:disabled {{
background: #2a2c35;
border: {border_c} #2a2c35;
color: #777a84;
}}
QComboBox::drop-down {{
subcontrol-origin: padding;
subcontrol-position: center right;
border-left: {border_b} rgba(255, 255, 255, 0.05);
padding: 12px;
height: 12px;
width: 12px;
}}
QComboBox::down-arrow {{
image: url({theme_manager.get_icon("down", current_theme_name, as_path=True)});
padding: 12px;
height: 12px;
width: 12px;
}}
QComboBox::down-arrow:on {{
image: url({theme_manager.get_icon("up", current_theme_name, as_path=True)});
padding: 12px;
height: 12px;
width: 12px;
}}
/* Список при открытом комбобоксе */
QComboBox QAbstractItemView {{
outline: none;
background: {color_c};
border: {border_c} {color_a};
border-top-style: none;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}}
QListView {{
background: {color_c};
}}
QListView::item {{
padding: 7px 7px 7px 12px;
margin: 3px;
border-radius: {border_radius_a};
color: {color_f};
}}
QListView::item:hover {{
background: {color_b};
}}
QListView::item:selected {{
background: {color_b};
}}
/* Выделение в списке при фокусе на элементе */
QListView::item:focus {{
background: {color_a};
color: {color_f};
}}
QLineEdit {{
background: {color_c};
border: {border_c} rgba(255, 255, 255, 0.01);
border-radius: {border_radius_a};
height: 34px;
padding-left: 12px;
color: {color_f};
font-family: '{font_family}';
font-size: {font_size_a};
}}
QLineEdit:hover {{
background: {color_c};
border: {border_c} {color_a};
}}
QLineEdit:focus {{
border: {border_c} {color_a};
background-color: {color_e};
}}
QTableWidget {{
background: {color_h};
color: {color_f};
gridline-color: {color_h};
alternate-background-color: {color_d};
border: {border_a};
border-radius: {border_radius_a};
@@ -1009,39 +1111,68 @@ QHeaderView::section {{
}}
QTableWidget::item {{
padding: 8px;
border-bottom: 1px solid {color_d};
border-bottom: {border_a } {color_c};
height: 36px;
}}
QTableWidget::item:selected {{
QTableWidget::item:selected,
QTableWidget::item:focus,
QTableWidget::item:selected:focus {{
background: {color_a};
color: {color_f};
selection-background-color: {color_a};
}}
QTableWidget::item:hover {{
background: {color_e};
background: {color_h};
}}
QTableWidget::indicator {{
width: 24px;
height: 24px;
border: {border_b} {color_a};
border: {border_c} {color_h};
border-radius: {border_radius_a};
background: rgba(255, 255, 255, 0.1);
background: {color_b};
}}
QTableWidget::indicator:unchecked {{
background: rgba(255, 255, 255, 0.1);
image: none;
}}
QTableWidget::indicator:checked {{
background: {color_a};
background: {color_b};
image: url({theme_manager.get_icon("check", current_theme_name, as_path=True)});
border: {border_b} {color_f};
border: {border_c} {color_a};
}}
QTableWidget::indicator:hover {{
background: rgba(255, 255, 255, 0.2);
border: {border_b} {color_a};
}}
QTableWidget::indicator:focus {{
border: {border_c} {color_a};
}}
{SCROLL_AREA_STYLE}
QTableWidget::indicator:focus {{
background: rgba(255, 255, 255, 0.2);
border: {border_c} {color_a};
}}
QScrollBar:vertical {{
width: 10px;
border: {border_a};
border-radius: 5px;
background: rgba(20, 20, 20, 0.30);
}}
QScrollBar::handle:vertical {{
background: #bebebe;
border: {border_a};
border-radius: 5px;
}}
QScrollBar::add-line:vertical {{
border: {border_a};
background: none;
}}
QScrollBar::sub-line:vertical {{
border: {border_a};
background: none;
}}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {{
border: {border_a};
width: 3px;
height: 3px;
background: none;
}}
"""
WINETRICKS_LOG_STYLE = f"""

View File

@@ -1,7 +1,8 @@
from typing import cast
from typing import cast, Any
from PySide6.QtWidgets import (QFrame, QVBoxLayout, QPushButton, QGridLayout,
QSizePolicy, QWidget, QLineEdit)
from PySide6.QtCore import Qt, Signal, QProcess
from PySide6.QtCore import Qt, Signal, QProcess, QSize
from PySide6.QtGui import QPixmap, QIcon
from portprotonqt.keyboard_layouts import keyboard_layouts
from portprotonqt.theme_manager import ThemeManager
from portprotonqt.config_utils import read_theme_from_config
@@ -43,6 +44,18 @@ class VirtualKeyboard(QFrame):
self.margins = 10
self.num_cols = 14
# Find input_manager and main_window
self.input_manager: Any = None
self.main_window: Any = None
parent_widget: QWidget | None = self._parent
while parent_widget:
if hasattr(parent_widget, 'input_manager'):
self.input_manager = cast(Any, parent_widget).input_manager
self.main_window = cast(Any, parent_widget)
parent_widget = cast(QWidget | None, parent_widget.parent())
self.current_theme_name = read_theme_from_config()
self.initUI()
self.hide()
@@ -119,6 +132,34 @@ class VirtualKeyboard(QFrame):
self.buttons: dict[str, QPushButton] = {}
self.update_keyboard()
def set_gamepad_icon(self, button, icon_type, gtype=''):
"""Set gamepad icon on button based on type"""
if icon_type in ['back', 'add_game']:
icon_name = self.main_window.get_button_icon(icon_type, gtype)
else: # nav left/right
if icon_type in ['left', 'right']:
direction = icon_type
icon_name = self.main_window.get_nav_icon(direction, gtype)
else:
direction = 'left' if icon_type == 'left' else 'right'
icon_name = self.main_window.get_nav_icon(direction, gtype)
icon_path = theme_manager.get_theme_image(icon_name, self.current_theme_name)
pixmap = QPixmap()
if icon_path:
pixmap.load(str(icon_path))
if not pixmap.isNull():
button.setIcon(QIcon(pixmap))
button.setIconSize(QSize(20, 20))
return
else:
# Fallback to placeholder
placeholder = theme_manager.get_theme_image("placeholder", self.current_theme_name)
if placeholder:
button.setIcon(QIcon(placeholder))
button.setIconSize(QSize(20, 20))
return
def update_keyboard(self):
coords = self._save_focused_coords()
@@ -151,6 +192,9 @@ class VirtualKeyboard(QFrame):
button.setCheckable(True)
button.setChecked(self.shift_pressed)
button.clicked.connect(lambda checked: self.on_shift_click(checked))
# Add gamepad icon for Shift (RB/R)
gtype = self.input_manager.gamepad_type
self.set_gamepad_icon(button, 'right', gtype)
else:
button.clicked.connect(lambda checked=False, k=key: self.on_button_click(k))
@@ -163,6 +207,9 @@ class VirtualKeyboard(QFrame):
shift.setCheckable(True)
shift.setChecked(self.shift_pressed)
shift.clicked.connect(lambda checked: self.on_shift_click(checked))
# Add gamepad icon for Shift (RB/R)
gtype = self.input_manager.gamepad_type
self.set_gamepad_icon(shift, 'right', gtype)
self.keyboard_layout.addWidget(shift, 3, 11, 1, 3)
button = QPushButton('CAPS')
@@ -179,6 +226,9 @@ class VirtualKeyboard(QFrame):
backspace.setFixedSize(fixed_w, fixed_h)
backspace.pressed.connect(self.on_backspace_pressed)
backspace.released.connect(self.stop_backspace_repeat)
# Add gamepad icon for Backspace (X/Triangle)
gtype = self.input_manager.gamepad_type
self.set_gamepad_icon(backspace, 'add_game', gtype)
self.keyboard_layout.addWidget(backspace, 0, 13, 1, 1)
enter = QPushButton('Enter')
@@ -189,6 +239,9 @@ class VirtualKeyboard(QFrame):
lang = QPushButton('🌐')
lang.setFixedSize(fixed_w, fixed_h)
lang.clicked.connect(self.on_lang_click)
# Add gamepad icon for Lang (LB/L)
gtype = self.input_manager.gamepad_type
self.set_gamepad_icon(lang, 'left', gtype)
self.keyboard_layout.addWidget(lang, 4, 0, 1, 1)
clear = QPushButton('Clear')
@@ -219,6 +272,9 @@ class VirtualKeyboard(QFrame):
hide_button = QPushButton('Hide')
hide_button.setFixedSize(fixed_w * 2 + self.spacing, fixed_h)
hide_button.clicked.connect(self.hide)
# Add gamepad icon for Hide (B/Circle)
gtype = self.input_manager.gamepad_type
self.set_gamepad_icon(hide_button, 'back', gtype)
self.keyboard_layout.addWidget(hide_button, 4, 12, 1, 2)
if coords:

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "portprotonqt"
version = "0.1.6"
version = "0.1.8"
description = "A project to rewrite PortProton (PortWINE) using PySide"
readme = "README.md"
license = { text = "GPL-3.0" }
@@ -32,7 +32,7 @@ dependencies = [
"icoextract>=0.2.0",
"numpy>=2.2.4",
"orjson>=3.11.3",
"pillow>=11.3.0",
"pillow>=12.0.0",
"psutil>=7.1.0",
"pyside6==6.9.1",
"pyudev>=0.24.3",

View File

@@ -8,7 +8,7 @@
"enabled": true
},
"pre-commit": {
"enabled": true
"enabled": false
},
"packageRules": [
{

978
uv.lock generated

File diff suppressed because it is too large Load Diff