Compare commits
2 Commits
debian-tes
...
preloader
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a4284191b | |||
| d05f2fccd6 |
@@ -1,4 +1,4 @@
|
|||||||
name: Nightly Build - AppImage, Debian, Arch, Fedora
|
name: Nightly Build - AppImage, Arch, Fedora
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -8,37 +8,11 @@ env:
|
|||||||
PACKAGE: "portprotonqt"
|
PACKAGE: "portprotonqt"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-debian:
|
|
||||||
name: Build Debian Package
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
||||||
|
|
||||||
- name: Install required dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y python3-all python3-setuptools python3-build python3-installer dh-python debhelper devscripts build-essential python3-dev pybuild-plugin-pyproject
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
# Copy Debian packages to a consistent location for upload
|
|
||||||
mkdir -p ./dist
|
|
||||||
cp ../*.deb ./dist/ || true
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: PortProtonQt-Debian
|
|
||||||
path: |
|
|
||||||
dist/*.deb
|
|
||||||
|
|
||||||
build-appimage:
|
build-appimage:
|
||||||
name: Build AppImage
|
name: Build AppImage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Install required dependencies
|
- name: Install required dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -88,7 +62,7 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||||
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
python3-build pyproject-rpm-macros python3-setuptools \
|
||||||
redhat-rpm-config nodejs npm
|
redhat-rpm-config nodejs npm
|
||||||
|
|
||||||
- name: Setup rpmbuild environment
|
- name: Setup rpmbuild environment
|
||||||
@@ -99,7 +73,7 @@ jobs:
|
|||||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Copy fedora.spec
|
- name: Copy fedora.spec
|
||||||
run: |
|
run: |
|
||||||
@@ -120,7 +94,7 @@ jobs:
|
|||||||
name: Build Arch Package
|
name: Build Arch Package
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
|
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
@@ -129,7 +103,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Prepare container
|
- name: Prepare container
|
||||||
run: |
|
run: |
|
||||||
pacman -Syuu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
pacman -Sy --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
||||||
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
||||||
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
||||||
yes | pacman -Scc
|
yes | pacman -Scc
|
||||||
@@ -160,7 +134,7 @@ jobs:
|
|||||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Upload Arch package
|
- name: Upload Arch package
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Build AppImage, Debian, Arch and Fedora Packages
|
name: Build AppImage, Arch and Fedora Packages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -8,38 +8,12 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
# Common version, will be used for tagging the release
|
# Common version, will be used for tagging the release
|
||||||
VERSION: 0.1.9
|
VERSION: 0.1.6
|
||||||
PKGDEST: "/tmp/portprotonqt"
|
PKGDEST: "/tmp/portprotonqt"
|
||||||
PACKAGE: "portprotonqt"
|
PACKAGE: "portprotonqt"
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-debian:
|
|
||||||
name: Build Debian Package
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install required dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y python3-all python3-setuptools python3-build python3-installer dh-python debhelper devscripts build-essential python3-dev pybuild-plugin-pyproject
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
# Copy Debian packages to a consistent location for upload
|
|
||||||
mkdir -p ./dist
|
|
||||||
cp ../*.deb ./dist/ || true
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: PortProtonQt-Debian
|
|
||||||
path: |
|
|
||||||
dist/*.deb
|
|
||||||
|
|
||||||
build-appimage:
|
build-appimage:
|
||||||
name: Build AppImage
|
name: Build AppImage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@@ -90,7 +64,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Prepare container
|
- name: Prepare container
|
||||||
run: |
|
run: |
|
||||||
pacman -Syuu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
pacman -Sy --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm
|
||||||
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
sed -i 's/#MAKEFLAGS="-j2"/MAKEFLAGS="-j$(nproc) -l$(nproc)"/g' /etc/makepkg.conf
|
||||||
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
sed -i 's/OPTIONS=(.*)/OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge lto)/g' /etc/makepkg.conf
|
||||||
yes | pacman -Scc
|
yes | pacman -Scc
|
||||||
@@ -145,7 +119,7 @@ jobs:
|
|||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
dnf install -y git rpmdevtools python3-devel python3-wheel python3-pip \
|
||||||
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \
|
python3-build pyproject-rpm-macros python3-setuptools \
|
||||||
redhat-rpm-config nodejs npm
|
redhat-rpm-config nodejs npm
|
||||||
|
|
||||||
- name: Setup rpmbuild environment
|
- name: Setup rpmbuild environment
|
||||||
@@ -175,7 +149,7 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
name: Create and Publish Release
|
name: Create and Publish Release
|
||||||
needs: [build-debian, build-appimage, build-arch, build-fedora]
|
needs: [build-appimage, build-arch, build-fedora]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v4
|
- uses: https://gitea.com/actions/checkout@v4
|
||||||
@@ -206,12 +180,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: https://gitea.com/actions/gitea-release-action@v1
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
env:
|
|
||||||
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
|
|
||||||
with:
|
with:
|
||||||
body_path: changelog.txt
|
body_path: changelog.txt
|
||||||
token: ${{ env.GITEA_TOKEN }}
|
token: ${{ env.GITEA_TOKEN }}
|
||||||
tag_name: v${{ env.VERSION }}
|
tag_name: v${{ env.VERSION }}
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: release/**/*
|
files: release/**/*
|
||||||
sha256sum: false
|
sha256sum: true
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: https://gitea.com/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
name: Build Check - AppImage, Debian, Arch, Fedora
|
name: Build Check - AppImage, Arch, Fedora
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'build-aux/**'
|
- 'build-aux/**'
|
||||||
- 'debian/**'
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PKGDEST: "/tmp/portprotonqt"
|
PKGDEST: "/tmp/portprotonqt"
|
||||||
@@ -16,11 +15,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
appimage: ${{ steps.check.outputs.appimage }}
|
appimage: ${{ steps.check.outputs.appimage }}
|
||||||
debian: ${{ steps.check.outputs.debian }}
|
|
||||||
fedora: ${{ steps.check.outputs.fedora }}
|
fedora: ${{ steps.check.outputs.fedora }}
|
||||||
arch: ${{ steps.check.outputs.arch }}
|
arch: ${{ steps.check.outputs.arch }}
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -45,13 +43,6 @@ jobs:
|
|||||||
echo "appimage=false" >> $GITHUB_OUTPUT
|
echo "appimage=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check Debian directory
|
|
||||||
if grep -q "debian/" changed_files.txt || ls debian/ 1> /dev/null 2>&1; then
|
|
||||||
echo "debian=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "debian=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check Fedora spec files (only fedora-git.spec)
|
# Check Fedora spec files (only fedora-git.spec)
|
||||||
if grep -q "build-aux/fedora-git.spec" changed_files.txt; then
|
if grep -q "build-aux/fedora-git.spec" changed_files.txt; then
|
||||||
echo "fedora=true" >> $GITHUB_OUTPUT
|
echo "fedora=true" >> $GITHUB_OUTPUT
|
||||||
@@ -66,38 +57,13 @@ jobs:
|
|||||||
echo "arch=false" >> $GITHUB_OUTPUT
|
echo "arch=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build-debian:
|
|
||||||
name: Build Debian Package
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
needs: changes
|
|
||||||
if: needs.changes.outputs.debian == 'true' || github.event_name == 'workflow_dispatch'
|
|
||||||
steps:
|
|
||||||
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
||||||
|
|
||||||
- name: Install required dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y python3-all python3-setuptools python3-build python3-installer dh-python debhelper devscripts build-essential python3-dev pybuild-plugin-pyproject
|
|
||||||
|
|
||||||
- name: Build Debian package
|
|
||||||
run: |
|
|
||||||
dpkg-buildpackage -us -uc -b
|
|
||||||
ls -la ../*.deb
|
|
||||||
|
|
||||||
- name: Upload Debian package
|
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: PortProtonQt-Debian
|
|
||||||
path: |
|
|
||||||
../*.deb
|
|
||||||
|
|
||||||
build-appimage:
|
build-appimage:
|
||||||
name: Build AppImage
|
name: Build AppImage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Install required dependencies
|
- name: Install required dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -149,7 +115,7 @@ jobs:
|
|||||||
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
echo '%_topdir /home/rpmbuild' > /home/rpmbuild/.rpmmacros
|
||||||
|
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Copy fedora-git.spec
|
- name: Copy fedora-git.spec
|
||||||
run: |
|
run: |
|
||||||
@@ -172,7 +138,7 @@ jobs:
|
|||||||
needs: changes
|
needs: changes
|
||||||
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
container:
|
container:
|
||||||
image: archlinux:base-devel@sha256:943bdad9e9d0d23503f24797b44ce2cc1531bf101e18b3e7fb8c8776190dc45e
|
image: archlinux:base-devel@sha256:0589aa8f31d8f64c630a2d1cc0b4c3847a1a63c988abd63d78d3c9bd94764f64
|
||||||
volumes:
|
volumes:
|
||||||
- /usr:/usr-host
|
- /usr:/usr-host
|
||||||
- /opt:/opt-host
|
- /opt:/opt-host
|
||||||
@@ -212,7 +178,7 @@ jobs:
|
|||||||
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Upload Arch package
|
- name: Upload Arch package
|
||||||
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
uses: https://gitea.com/actions/gitea-upload-artifact@v4
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ jobs:
|
|||||||
name: Check code
|
name: Check code
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: https://gitea.com/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: https://gitea.com/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6
|
uses: https://gitea.com/actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
|
container: ghcr.io/renovatebot/renovate:latest@sha256:dd5721b9a686a40d81687643e4b71b82a0ca31fb653fd727538af69104fd388d
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
- uses: https://gitea.com/actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: https://gitea.com/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
|
uses: https://gitea.com/actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
rev: 0.9.5
|
rev: 0.8.22
|
||||||
hooks:
|
hooks:
|
||||||
- id: uv-lock
|
- id: uv-lock
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.14.3
|
rev: v0.13.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
|
|
||||||
|
|||||||
74
CHANGELOG.md
@@ -3,91 +3,20 @@
|
|||||||
Все заметные изменения в этом проекте фиксируются в этом файле.
|
Все заметные изменения в этом проекте фиксируются в этом файле.
|
||||||
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
## [0.1.9] - 2025-12-08
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
|
||||||
- Добавлены основные и расширенные настройки для `.exe`-файлов
|
|
||||||
- Добавлена кнопка обновления сетки без необходимости перезапуска PortProtonQt (F5 на клавиатуре, GUIDE + Select на геймпаде)
|
|
||||||
- Добавлена эмуляция мыши по GUIDE (Xbox или PS) + Start для установки приложений или взаимодействия с инструментами Wine не адаптированные под геймпад (работает только если PortProtonQt вне фокуса)
|
|
||||||
- При сворачивании приложения в трей оно теперь корректно восстанавливается, вместо запуска нового экземпляра
|
|
||||||
- Добавлена поддержка SteamGridDB в качестве дополнительного источника обложек
|
|
||||||
- При добавлении карточки в избранное она автоматически становится первой без необходимости перезапуска
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Изменено оформление виртуальной клавиатуры для лучшего соответствия общей теме
|
|
||||||
- Ускорено чтение конфигов за счёт уменьшения количества обращений к файловой системе.
|
|
||||||
- Из стандартной темы удалены неиспользуемые шрифты
|
|
||||||
- Улучшена совместимость с Qt 6.10
|
|
||||||
- Ускорен запуск программы
|
|
||||||
- В диалог редактирования ярылыка добавлен placeholder с уточнением того что в качевстве обложки можно использовать и ссылку, а не только файл
|
|
||||||
- Ссылку на обложку в диалоге редактирования ярлыка теперь можно указывать без протокола вроде http или https
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Добавлено больше проверок на None для избежания вылетов
|
|
||||||
- Улучшена работа с потоками для избежания вылетов
|
|
||||||
- Исправлен запуск PortProton из Flatpak: теперь используется `flatpak run`, а не `start.sh`
|
|
||||||
- Исправлено применение обложки по ссылке например со steamgriddb.com/
|
|
||||||
- Исправлено множественное открытие окон в X11
|
|
||||||
|
|
||||||
### Contributors
|
|
||||||
- @Vector_null
|
|
||||||
- @Dervart
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.1.8] - 2025-10-18
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
|
|
||||||
- В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет
|
|
||||||
- К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада
|
|
||||||
- Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы)
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- При завершении автоустановки приложение больше не перезапускается
|
|
||||||
- Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название
|
|
||||||
- Обновлены и дополнены скриншоты темы
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Исправлено наложение карточек при смене фильтра игр
|
|
||||||
- Исправлена невозможность запуска приложения без подключёного геймпада
|
|
||||||
- Исправлена невозможность установки компонентов Winetricks через геймпад
|
|
||||||
- Ресиверы и виртуальные устройства больше не считаются за геймпад
|
|
||||||
|
|
||||||
|
|
||||||
### Contributors
|
|
||||||
- @Vector_null
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [0.1.7] - 2025-10-12
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Возможность скроллинга библиотеки мышью или пальцем
|
- Возможность скроллинга библиотеки мышью или пальцем
|
||||||
- Импорт и экспорт бекапа префикса
|
|
||||||
- Диалог для управление Winetricks
|
|
||||||
- Кнопки для удаления префикса, wine или proton
|
|
||||||
- Все настройки Wine с оригинального PortProton
|
|
||||||
- Виртуальная клавиатура в диалог добавления игры и поиск по библиотеке и автоустановках
|
|
||||||
- Вкладка автоустановок
|
|
||||||
- В заголовке окна теперь отображается версия приложения и хеш коммита если запуск идёт с гита
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
|
- Проведён рефакторинг и оптимизация всего что связано с карточками и библиотекой игр
|
||||||
- В диалог выбора файлов в режиме directory_only (при выборе куда сохранить бекап префикса) добавлена кнопка ./ обозначающая нынешнюю папку
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений
|
- Исправлен вылет диалога выбора файлов при выборе обложки если в папке более сотни изображений
|
||||||
- Исправлено зависание при добавлении или удалении игры в Wayland
|
- Исправлено зависание при добавлении или удалении игры в Wayland
|
||||||
- Исправлено зависание при поиске игр
|
- Исправлено зависание при поиске игр
|
||||||
- Исправлено ошибочное присвоение ID игры с названием "GAME", возникавшее, если исполняемый файл находился в подпапке `game/` (часто встречается у игр на Unity)
|
|
||||||
- Исправлена ошибка из-за которой подсказки по управлению снизу и сверху могли не совпадать с друг другом, из-за чего возле вкладок были стрелки клавиатуры, а снизу кнопки геймпада
|
|
||||||
- Исправлен выход из полноэкранного режима при отключении геймпада подключённого по USB даже если настройка "Режим полноэкранного отображения приложения при подключении геймпада" выключена
|
|
||||||
- При сохранении настроек теперь не меняется размер окна
|
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
- @wmigor (Igor Akulov)
|
|
||||||
- @Vector_null
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -110,7 +39,6 @@
|
|||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
- @wmigor (Igor Akulov)
|
- @wmigor (Igor Akulov)
|
||||||
- @Vector_null
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
recursive-include portprotonqt/themes *
|
|
||||||
recursive-include portprotonqt/locales *
|
|
||||||
15
TODO.md
@@ -1,6 +1,6 @@
|
|||||||
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
- [X] Адаптировать структуру проекта для поддержки инструментов сборки
|
||||||
- [X] Добавить возможность управления с геймпада
|
- [X] Добавить возможность управления с геймпада
|
||||||
- [X] Добавить возможность управления с тачскрина (Формально и так есть)
|
- [ ] Добавить возможность управления с тачскрина
|
||||||
- [X] Добавить возможность управления с мыши и клавиатуры
|
- [X] Добавить возможность управления с мыши и клавиатуры
|
||||||
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
- [X] Добавить систему тем [Документация](documentation/theme_guide)
|
||||||
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
- [X] Вынести все константы, такие как уровень закругления карточек, в темы (частично выполнено)
|
||||||
@@ -11,18 +11,18 @@
|
|||||||
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
- [ ] Разработать адаптивный дизайн (за эталон берётся Steam Deck с разрешением 1280×800)
|
||||||
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
- [ ] Переделать скриншоты для соответствия [гайдлайнам Flathub](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines#screenshots)
|
||||||
- [X] Получать описания и названия игр из базы данных Steam
|
- [X] Получать описания и названия игр из базы данных Steam
|
||||||
- [X] Получать обложки для игр из CDN Steam
|
- [X] Получать обложки для игр из SteamGridDB или CDN Steam
|
||||||
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
- [X] Оптимизировать работу со Steam API для ускорения времени запуска
|
||||||
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
- [X] Улучшить функцию поиска в Steam API для исправления некорректного определения ID (например, Graven определялся как ENGRAVEN или GRAVENFALL, Spore — как SporeBound или Spore Valley)
|
||||||
- [X] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
- [ ] Убрать логи Steam API в релизной версии, так как они замедляют выполнение кода
|
||||||
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
- [X] Решить проблему с ограничением Steam API в 50 тысяч игр за один запрос (иногда нужные игры не попадают в выборку и остаются без обложки)
|
||||||
- [X] Избавиться от вызовов yad
|
- [X] Избавиться от вызовов yad
|
||||||
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
- [X] Реализовать собственный системный трей вместо использования трея PortProton
|
||||||
- [X] Добавить экранную клавиатуру в поиск
|
- [X] Добавить экранную клавиатуру в поиск (реализация собственной клавиатуры слишком затратна, поэтому используется встроенная в DE клавиатура: Maliit в KDE, gjs-osk в GNOME, Squeekboard в Phosh, клавиатура SteamOS и т.д.)
|
||||||
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
- [X] Добавить сортировку карточек по различным критериям (доступны: по недавности, количеству наигранного времени, избранному или алфавиту)
|
||||||
- [X] Добавить индикацию запуска приложения
|
- [X] Добавить индикацию запуска приложения
|
||||||
- [X] Достигнуть паритета функциональности с Ingame
|
- [X] Достигнуть паритета функциональности с Ingame
|
||||||
- [ ] Достигнуть паритета функциональности с PortProton (остались настройки игр и обновление скриптов)
|
- [ ] Достигнуть паритета функциональности с PortProton
|
||||||
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
- [X] Добавить возможность изменения названия, описания и обложки через файлы `.local/share/PortProtonQT/custom_data/exe_name/{desc,name,cover}`
|
||||||
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
- [X] Добавить встроенное переопределение названия, описания и обложки, например, по пути `portprotonqt/custom_data` [Документация](documentation/metadata_override/)
|
||||||
- [X] Добавить переводы в переопределения
|
- [X] Добавить переводы в переопределения
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
- [X] Добавить недокументированные параметры конфигурации в GUI (time_detail_level, games_sort_method, games_display_filter)
|
||||||
- [X] Добавить систему избранного для карточек
|
- [X] Добавить систему избранного для карточек
|
||||||
- [X] Заменить все `print` на `logging`
|
- [X] Заменить все `print` на `logging`
|
||||||
- [X] Привести все логи к единому языку
|
- [ ] Привести все логи к единому языку
|
||||||
- [X] Уменьшить количество подстановок в переводах
|
- [X] Уменьшить количество подстановок в переводах
|
||||||
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
- [X] Стилизовать все элементы без стилей (QMessageBox, QSlider, QDialog)
|
||||||
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
- [X] Убрать жёсткую привязку путей к стрелочкам QComboBox в `styles.py`
|
||||||
@@ -62,6 +62,7 @@
|
|||||||
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
- [X] Исправить некорректную работу слайдера увеличения размера карточек([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63)
|
||||||
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
- [X] Исправить баг с наложением карточек друг на друга при изменении фильтра отображения ([Последствия регрессии после этого коммита](https://github.com/Boria138/PortProtonQt/commit/aebdd60b5537280f06a922ff80469cd4ab27bc63))
|
||||||
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
- [X] Скопировать логику управления с D-pad на стрелки с клавиатуры
|
||||||
- [X] Добавить подсказки к управлению с геймпада
|
- [ ] Доделать светлую тему
|
||||||
|
- [ ] Добавить подсказки к управлению с геймпада
|
||||||
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
- [X] Добавить миниатюры к выбору файлов в диалоге добавления игры
|
||||||
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
- [X] Добавить быстрый доступ к смонтированным дискам к выбору файлов в диалоге добавления игры
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ script:
|
|||||||
- uv venv
|
- uv venv
|
||||||
- uv pip install --no-cache-dir ../
|
- uv pip install --no-cache-dir ../
|
||||||
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
- cp -r .venv/lib/python3.10/site-packages/* AppDir/usr/local/lib/python3.10/dist-packages
|
||||||
- cp -r usr AppDir/
|
- cp -r share AppDir/usr
|
||||||
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
|
||||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
|
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
|
||||||
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,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*,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*}
|
||||||
- shopt -s extglob
|
- shopt -s extglob
|
||||||
- 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*)
|
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!(libQt6Core*|libQt6DBus*|libQt6Egl*|libQt6Gui*|libQt6Svg*|libQt6Wayland*|libQt6Widgets*|libQt6XcbQpa*|libicudata*|libicui18n*|libicuuc*)
|
||||||
AppDir:
|
AppDir:
|
||||||
path: ./AppDir
|
path: ./AppDir
|
||||||
after_bundle:
|
after_bundle:
|
||||||
@@ -36,7 +36,7 @@ AppDir:
|
|||||||
id: ru.linux_gaming.PortProtonQt
|
id: ru.linux_gaming.PortProtonQt
|
||||||
name: PortProtonQt
|
name: PortProtonQt
|
||||||
icon: ru.linux_gaming.PortProtonQt
|
icon: ru.linux_gaming.PortProtonQt
|
||||||
version: 0.1.9
|
version: 0.1.6
|
||||||
exec: usr/bin/python3
|
exec: usr/bin/python3
|
||||||
exec_args: "-m portprotonqt.app $@"
|
exec_args: "-m portprotonqt.app $@"
|
||||||
apt:
|
apt:
|
||||||
@@ -54,11 +54,6 @@ AppDir:
|
|||||||
- libxcb-cursor0
|
- libxcb-cursor0
|
||||||
- libimage-exiftool-perl
|
- libimage-exiftool-perl
|
||||||
- xdg-utils
|
- xdg-utils
|
||||||
- cabextract
|
|
||||||
- curl
|
|
||||||
- 7zip
|
|
||||||
- unzip
|
|
||||||
- unrar
|
|
||||||
exclude:
|
exclude:
|
||||||
- "*-doc"
|
- "*-doc"
|
||||||
- "*-man"
|
- "*-man"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
pkgname=portprotonqt
|
pkgname=portprotonqt
|
||||||
pkgver=0.1.9
|
pkgver=0.1.6
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'python-rapidfuzz' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt#tag=v$pkgver")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
@@ -19,5 +19,5 @@ build() {
|
|||||||
package() {
|
package() {
|
||||||
cd "$srcdir/PortProtonQt"
|
cd "$srcdir/PortProtonQt"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
cp -r build-aux/usr "$pkgdir/"
|
cp -r build-aux/share "$pkgdir/usr/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ arch=('any')
|
|||||||
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
url="https://git.linux-gaming.ru/Boria138/PortProtonQt"
|
||||||
license=('GPL-3.0')
|
license=('GPL-3.0')
|
||||||
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
depends=('python-numpy' 'python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
|
||||||
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'python-rapidfuzz' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar')
|
'python-psutil' 'python-tqdm' 'python-vdf' 'pyside6' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client')
|
||||||
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
makedepends=('python-'{'build','installer','setuptools','wheel'})
|
||||||
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
|
||||||
sha256sums=('SKIP')
|
sha256sums=('SKIP')
|
||||||
@@ -24,5 +24,5 @@ build() {
|
|||||||
package() {
|
package() {
|
||||||
cd "$srcdir/PortProtonQt"
|
cd "$srcdir/PortProtonQt"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
cp -r build-aux/usr "$pkgdir/"
|
cp -r build-aux/share "$pkgdir/usr/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ BuildRequires: python3-build
|
|||||||
BuildRequires: pyproject-rpm-macros
|
BuildRequires: pyproject-rpm-macros
|
||||||
BuildRequires: python3dist(setuptools)
|
BuildRequires: python3dist(setuptools)
|
||||||
BuildRequires: git
|
BuildRequires: git
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
%description
|
%description
|
||||||
%{summary}
|
%{summary}
|
||||||
@@ -44,15 +43,9 @@ Requires: python3-tqdm
|
|||||||
Requires: python3-vdf
|
Requires: python3-vdf
|
||||||
Requires: python3-pefile
|
Requires: python3-pefile
|
||||||
Requires: python3-pillow
|
Requires: python3-pillow
|
||||||
Requires: python3-beautifulsoup4
|
|
||||||
Requires: python3-rapidfuzz
|
|
||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: cabextract
|
Requires: python3-beautifulsoup4
|
||||||
Requires: gzip
|
|
||||||
Requires: unzip
|
|
||||||
Requires: curl
|
|
||||||
Requires: unrar
|
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}-git
|
%description -n python3-%{pypi_name}-git
|
||||||
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||||
@@ -70,13 +63,12 @@ cd %{oname}
|
|||||||
cd %{oname}
|
cd %{oname}
|
||||||
%pyproject_install
|
%pyproject_install
|
||||||
%pyproject_save_files %{pypi_name}
|
%pyproject_save_files %{pypi_name}
|
||||||
cp -r build-aux/usr %{buildroot}/
|
cp -r build-aux/share %{buildroot}/usr/
|
||||||
|
|
||||||
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
%files -n python3-%{pypi_name}-git -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
%{_udevrulesdir}/60-portprotonqt.rules
|
|
||||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
%{bash_completions_dir}/portprotonqt
|
%{bash_completions_dir}/portprotonqt
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
%global pypi_name portprotonqt
|
%global pypi_name portprotonqt
|
||||||
%global pypi_version 0.1.9
|
%global pypi_version 0.1.6
|
||||||
%global oname PortProtonQt
|
%global oname PortProtonQt
|
||||||
%global _python_no_extras_requires 1
|
%global _python_no_extras_requires 1
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ BuildRequires: python3-build
|
|||||||
BuildRequires: pyproject-rpm-macros
|
BuildRequires: pyproject-rpm-macros
|
||||||
BuildRequires: python3dist(setuptools)
|
BuildRequires: python3dist(setuptools)
|
||||||
BuildRequires: git
|
BuildRequires: git
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
%description
|
%description
|
||||||
%{summary}
|
%{summary}
|
||||||
@@ -41,15 +40,9 @@ Requires: python3-tqdm
|
|||||||
Requires: python3-vdf
|
Requires: python3-vdf
|
||||||
Requires: python3-pefile
|
Requires: python3-pefile
|
||||||
Requires: python3-pillow
|
Requires: python3-pillow
|
||||||
Requires: python3-beautifulsoup4
|
|
||||||
Requires: python3-rapidfuzz
|
|
||||||
Requires: perl-Image-ExifTool
|
Requires: perl-Image-ExifTool
|
||||||
Requires: xdg-utils
|
Requires: xdg-utils
|
||||||
Requires: cabextract
|
Requires: python3-beautifulsoup4
|
||||||
Requires: gzip
|
|
||||||
Requires: unzip
|
|
||||||
Requires: curl
|
|
||||||
Requires: unrar
|
|
||||||
|
|
||||||
%description -n python3-%{pypi_name}
|
%description -n python3-%{pypi_name}
|
||||||
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
This application provides a sleek, intuitive graphical interface for managing and launching games from PortProton, Steam, and Epic Games Store. It consolidates your game libraries into a single, user-friendly hub for seamless navigation and organization. Its lightweight structure and cross-platform support deliver a cohesive gaming experience, eliminating the need for multiple launchers. Unique PortProton integration enhances Linux gaming, enabling effortless play of Windows-based titles with minimal setup.
|
||||||
@@ -69,13 +62,12 @@ cd %{oname}
|
|||||||
cd %{oname}
|
cd %{oname}
|
||||||
%pyproject_install
|
%pyproject_install
|
||||||
%pyproject_save_files %{pypi_name}
|
%pyproject_save_files %{pypi_name}
|
||||||
cp -r build-aux/usr %{buildroot}/
|
cp -r build-aux/share %{buildroot}/usr/
|
||||||
|
|
||||||
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
%files -n python3-%{pypi_name} -f %{pyproject_files}
|
||||||
%{_bindir}/%{pypi_name}
|
%{_bindir}/%{pypi_name}
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
%{_datadir}/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg
|
||||||
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
%{_metainfodir}/ru.linux_gaming.PortProtonQt.metainfo.xml
|
||||||
%{_udevrulesdir}/60-portprotonqt.rules
|
|
||||||
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
%{_datadir}/applications/ru.linux_gaming.PortProtonQt.desktop
|
||||||
%{bash_completions_dir}/portprotonqt
|
%{bash_completions_dir}/portprotonqt
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -1 +0,0 @@
|
|||||||
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
|
|
||||||
@@ -1021,7 +1021,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "farlight 84",
|
"normalized_name": "farlight 84",
|
||||||
"status": "Denied"
|
"status": "Supported"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "riders republic",
|
"normalized_name": "riders republic",
|
||||||
@@ -1373,7 +1373,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "arena breakout infinite",
|
"normalized_name": "arena breakout infinite",
|
||||||
"status": "Denied"
|
"status": "Broken"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "pixel gun 3d pc",
|
"normalized_name": "pixel gun 3d pc",
|
||||||
@@ -1436,8 +1436,8 @@
|
|||||||
"status": "Broken"
|
"status": "Broken"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "blue protocol star resonance",
|
"normalized_name": "blue protocol",
|
||||||
"status": "Running"
|
"status": "Broken"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "dark and darker",
|
"normalized_name": "dark and darker",
|
||||||
@@ -4316,7 +4316,7 @@
|
|||||||
"status": "Broken"
|
"status": "Broken"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "solo leveling arise overdrive",
|
"normalized_name": "solo leveling arise",
|
||||||
"status": "Running"
|
"status": "Running"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -4527,6 +4527,10 @@
|
|||||||
"normalized_name": "project wraith",
|
"normalized_name": "project wraith",
|
||||||
"status": "Broken"
|
"status": "Broken"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"normalized_name": "solo leveling arise",
|
||||||
|
"status": "Broken"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"normalized_name": "freedom wars",
|
"normalized_name": "freedom wars",
|
||||||
"status": "Running"
|
"status": "Running"
|
||||||
@@ -4538,9 +4542,5 @@
|
|||||||
{
|
{
|
||||||
"normalized_name": "no more room in hell 2",
|
"normalized_name": "no more room in hell 2",
|
||||||
"status": "Running"
|
"status": "Running"
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_name": "call of duty black ops 7",
|
|
||||||
"status": "Denied"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
25844
data/games_appid.json
@@ -1,228 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"normalized_title": "metal gear solid v the phantom pain",
|
|
||||||
"slug": "metal-gear-solid-v-the-phantom-pain"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "battlefield bad company 2",
|
|
||||||
"slug": "battlefield-bad-company-2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "call of duty black ops",
|
|
||||||
"slug": "call-of-duty-black-ops"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "call of duty modern warfare 2 (2009)",
|
|
||||||
"slug": "call-of-duty-modern-warfare-2-2009"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "call of duty black ops cold war",
|
|
||||||
"slug": "call-of-duty-black-ops-cold-war"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "call of duty infinite warfare",
|
|
||||||
"slug": "call-of-duty-infinite-warfare"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "lost planet 2",
|
|
||||||
"slug": "lost-planet-2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "lost planet extreme condition colonies",
|
|
||||||
"slug": "lost-planet-extreme-condition-colonies-edition"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "starcraft",
|
|
||||||
"slug": "starcraft-remastered"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "the entropy centre",
|
|
||||||
"slug": "the-entropy-centre"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "metal gear solid v ground zeroes",
|
|
||||||
"slug": "metal-gear-solid-v-ground-zeroes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "escape from tarkov",
|
|
||||||
"slug": "escape-from-tarkov"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "command & conquer generals",
|
|
||||||
"slug": "command-conquer-generals"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "command & conquer generals zero hour",
|
|
||||||
"slug": "command-conquer-generals-zero-hour"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "absolum",
|
|
||||||
"slug": "absolum"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "tom clancy's splinter cell chaos theory",
|
|
||||||
"slug": "tom-clancys-splinter-cell-chaos-theory"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "winter burrow",
|
|
||||||
"slug": "winter-burrow"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "forager",
|
|
||||||
"slug": "forager"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "wall world",
|
|
||||||
"slug": "wall-world"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "grand theft auto iv the",
|
|
||||||
"slug": "grand-theft-auto-iv-the-complete-edition"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "voidtrain",
|
|
||||||
"slug": "voidtrain"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "jdm japanese drift master",
|
|
||||||
"slug": "jdm-japanese-drift-master"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "lego harry potter collection",
|
|
||||||
"slug": "lego-harry-potter-collection"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "life is strange season",
|
|
||||||
"slug": "life-is-strange-complete-season"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "земский собор [демо]",
|
|
||||||
"slug": "zemskij-sobor-demo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "syberia",
|
|
||||||
"slug": "syberia-remastered"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "europa universalis v",
|
|
||||||
"slug": "europa-universalis-v"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "no i'm not a human",
|
|
||||||
"slug": "no-im-not-a-human"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "dispatch digital deluxe",
|
|
||||||
"slug": "dispatch-digital-deluxe-edition"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "cossacks 3 digital deluxe",
|
|
||||||
"slug": "cossacks-3-digital-deluxe"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "battlefield 2",
|
|
||||||
"slug": "battlefield-2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "split/second",
|
|
||||||
"slug": "split-second"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "warzone 2100",
|
|
||||||
"slug": "warzone-2100"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "foundation",
|
|
||||||
"slug": "foundation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "crusader kings 3",
|
|
||||||
"slug": "crusader-kings-3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "nadir a grimdark deck builder",
|
|
||||||
"slug": "nadir-a-grimdark-deck-builder"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "oriental empires",
|
|
||||||
"slug": "oriental-empires"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "vampire the masquerade bloodlines 2",
|
|
||||||
"slug": "vampire-the-masquerade-bloodlines-2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "escape from duckov",
|
|
||||||
"slug": "escape-from-duckov"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "xiii",
|
|
||||||
"slug": "xiii"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "saints row 2",
|
|
||||||
"slug": "saints-row-2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "frozenheim",
|
|
||||||
"slug": "frozenheim"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "saints row (2022)",
|
|
||||||
"slug": "saints-row-2022"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "iron harvest",
|
|
||||||
"slug": "iron-harvest"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "tom clancy's splinter cell blacklist",
|
|
||||||
"slug": "tom-clancys-splinter-cell-blacklist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "painkiller overdose",
|
|
||||||
"slug": "painkiller-overdose"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "ancestors legacy",
|
|
||||||
"slug": "ancestors-legacy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "bye sweet carole",
|
|
||||||
"slug": "bye-sweet-carole"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "painkiller black",
|
|
||||||
"slug": "painkiller-black-edition"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "hogwarts legacy",
|
|
||||||
"slug": "hogwarts-legacy"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "active matter",
|
|
||||||
"slug": "active-matter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "tom clancy's splinter cell",
|
|
||||||
"slug": "tom-clancys-splinter-cell"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "sniper ghost warrior",
|
|
||||||
"slug": "sniper-ghost-warrior"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "fate undiscovered realms",
|
|
||||||
"slug": "fate-undiscovered-realms"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "dying light the beast deluxe",
|
|
||||||
"slug": "dying-light-the-beast-deluxe-edition"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"normalized_title": "spellforce platinum",
|
|
||||||
"slug": "spellforce-platinum-edition"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"normalized_title": "dirt rally 2.0 game of the year",
|
"normalized_title": "dirt rally 2.0 game of the year",
|
||||||
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
"slug": "dirt-rally-2-0-game-of-the-year-edition"
|
||||||
@@ -495,6 +271,10 @@
|
|||||||
"normalized_title": "steins;gate the distant valhalla",
|
"normalized_title": "steins;gate the distant valhalla",
|
||||||
"slug": "steins-gate-the-distant-valhalla"
|
"slug": "steins-gate-the-distant-valhalla"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "hogwarts legacy",
|
||||||
|
"slug": "hogwarts-legacy"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"normalized_title": "osu!",
|
"normalized_title": "osu!",
|
||||||
"slug": "osu"
|
"slug": "osu"
|
||||||
@@ -1531,6 +1311,10 @@
|
|||||||
"normalized_title": "world of sea battle",
|
"normalized_title": "world of sea battle",
|
||||||
"slug": "world-of-sea-battle"
|
"slug": "world-of-sea-battle"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "escape from tarkov",
|
||||||
|
"slug": "escape-from-tarkov"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"normalized_title": "bayonetta",
|
"normalized_title": "bayonetta",
|
||||||
"slug": "bayonetta"
|
"slug": "bayonetta"
|
||||||
@@ -1655,6 +1439,10 @@
|
|||||||
"normalized_title": "call of duty 2",
|
"normalized_title": "call of duty 2",
|
||||||
"slug": "call-of-duty-2"
|
"slug": "call-of-duty-2"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "call of duty infinite warfare",
|
||||||
|
"slug": "call-of-duty-infinite-warfare"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"normalized_title": "call of duty world at war",
|
"normalized_title": "call of duty world at war",
|
||||||
"slug": "call-of-duty-world-at-war"
|
"slug": "call-of-duty-world-at-war"
|
||||||
@@ -1847,6 +1635,10 @@
|
|||||||
"normalized_title": "elden ring",
|
"normalized_title": "elden ring",
|
||||||
"slug": "elden-ring"
|
"slug": "elden-ring"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"normalized_title": "starcraft",
|
||||||
|
"slug": "starcraft-remastered"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"normalized_title": "cataclismo",
|
"normalized_title": "cataclismo",
|
||||||
"slug": "cataclismo"
|
"slug": "cataclismo"
|
||||||
|
|||||||
8
debian/README.Debian
vendored
@@ -1,8 +0,0 @@
|
|||||||
PortProtonQt for Debian
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
This package provides a modern GUI for managing and launching games from
|
|
||||||
PortProton, Steam, and Epic Games Store.
|
|
||||||
|
|
||||||
For more information about PortProtonQt, please see the project homepage:
|
|
||||||
https://git.linux-gaming.ru/Boria138/PortProtonQt
|
|
||||||
22
debian/changelog
vendored
@@ -1,22 +0,0 @@
|
|||||||
portprotonqt (0.1.9-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Добавлены основные и расширенные настройки для ".exe"-файлов
|
|
||||||
* Добавлена кнопка обновления сетки без необходимости перезапуска PortProtonQt (F5 на клавиатуре, GUIDE + Select на геймпаде)
|
|
||||||
* Добавлена эмуляция мыши по GUIDE (Xbox или PS) + Start для установки приложений или взаимодействия с инструментами Wine не адаптированные под геймпад (работает только если PortProtonQt вне фокуса)
|
|
||||||
* При сворачивании приложения в трей оно теперь корректно восстанавливается, вместо запуска нового экземпляра
|
|
||||||
* Добавлена поддержка SteamGridDB в качестве дополнительного источника обложек
|
|
||||||
* При добавлении карточки в избранное она автоматически становится первой без необходимости перезапуска
|
|
||||||
* Изменено оформление виртуальной клавиатуры для лучшего соответствия общей теме
|
|
||||||
* Ускорено чтение конфигов за счёт уменьшения количества обращений к файловой системе.
|
|
||||||
* Из стандартной темы удалены неиспользуемые шрифты
|
|
||||||
* Улучшена совместимость с Qt 6.10
|
|
||||||
* Ускорен запуск программы
|
|
||||||
* В диалог редактирования ярылыка добавлен placeholder с уточнением того что в качевстве обложки можно использовать и ссылку, а не только файл
|
|
||||||
* Ссылку на обложку в диалоге редактирования ярлыка теперь можно указывать без протокола вроде http или https
|
|
||||||
* Добавлено больше проверок на None для избежания вылетов
|
|
||||||
* Улучшена работа с потоками для избежания вылетов
|
|
||||||
* Исправлен запуск PortProton из Flatpak: теперь используется "flatpak run", а не "start.sh"
|
|
||||||
* Исправлено применение обложки по ссылке например со steamgriddb.com/
|
|
||||||
* Исправлено множественное открытие окон в X11
|
|
||||||
|
|
||||||
-- Boris Yumankulov <boria138@altlinux.org> Mon, 08 Dec 2025 00:00:00 +0000
|
|
||||||
5
debian/changelog.bak
vendored
@@ -1,5 +0,0 @@
|
|||||||
portprotonqt (0.1.9-1) unstable; urgency=medium
|
|
||||||
|
|
||||||
* Initial release of PortProtonQt for Debian
|
|
||||||
|
|
||||||
-- Boris Yumankulov <boria138@altlinux.org> Thu, 11 Dec 2025 00:00:00 +0000
|
|
||||||
1
debian/compat
vendored
@@ -1 +0,0 @@
|
|||||||
13
|
|
||||||
47
debian/control
vendored
@@ -1,47 +0,0 @@
|
|||||||
Source: portprotonqt
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Boris Yumankulov <boria138@altlinux.org>
|
|
||||||
Build-Depends: debhelper (>= 13),
|
|
||||||
dh-python,
|
|
||||||
python3-all,
|
|
||||||
python3-setuptools,
|
|
||||||
python3-build,
|
|
||||||
python3-installer,
|
|
||||||
pybuild-plugin-pyproject
|
|
||||||
Standards-Version: 4.6.0
|
|
||||||
Homepage: https://git.linux-gaming.ru/Boria138/PortProtonQt
|
|
||||||
|
|
||||||
Package: python3-portprotonqt
|
|
||||||
Architecture: all
|
|
||||||
Depends: ${python3:Depends},
|
|
||||||
${misc:Depends},
|
|
||||||
python3-babel,
|
|
||||||
python3-beautifulsoup4,
|
|
||||||
python3-evdev,
|
|
||||||
python3-icoextract,
|
|
||||||
python3-numpy,
|
|
||||||
python3-orjson,
|
|
||||||
python3-pillow,
|
|
||||||
python3-psutil,
|
|
||||||
python3-pyside6,
|
|
||||||
python3-pyudev,
|
|
||||||
python3-rapidfuzz,
|
|
||||||
python3-requests,
|
|
||||||
python3-tqdm,
|
|
||||||
python3-vdf,
|
|
||||||
python3-websocket-client,
|
|
||||||
perl-image-exiftool,
|
|
||||||
xdg-utils,
|
|
||||||
cabextract,
|
|
||||||
gzip,
|
|
||||||
unzip,
|
|
||||||
curl,
|
|
||||||
unrar
|
|
||||||
Description: Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store
|
|
||||||
This application provides a sleek, intuitive graphical interface for managing
|
|
||||||
and launching games from PortProton, Steam, and Epic Games Store. It
|
|
||||||
consolidates your game libraries into a single, user-friendly hub for seamless
|
|
||||||
navigation and organization. Its lightweight structure and cross-platform
|
|
||||||
support deliver a cohesive gaming experience, eliminating the need for
|
|
||||||
multiple launchers. Unique PortProton integration enhances Linux gaming,
|
|
||||||
enabling effortless play of Windows-based titles with minimal setup.
|
|
||||||
7
debian/copyright
vendored
@@ -1,7 +0,0 @@
|
|||||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: PortProtonQt
|
|
||||||
Source: https://git.linux-gaming.ru/Boria138/PortProtonQt
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2024-2025 Boris Yumankulov <boria138@altlinux.org>
|
|
||||||
License: GPL-3.0+
|
|
||||||
33
debian/rules
vendored
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/make -f
|
|
||||||
|
|
||||||
export PYBUILD_NAME=portprotonqt
|
|
||||||
export DEB_BUILD_OPTIONS=nocheck
|
|
||||||
export DH_VERBOSE=1
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@ --with python3 --buildsystem=pybuild
|
|
||||||
|
|
||||||
override_dh_install:
|
|
||||||
dh_install
|
|
||||||
# Create necessary directories
|
|
||||||
mkdir -p debian/python3-portprotonqt/usr/lib/udev/rules.d
|
|
||||||
mkdir -p debian/python3-portprotonqt/usr/share/applications
|
|
||||||
mkdir -p debian/python3-portprotonqt/usr/share/bash-completion/completions
|
|
||||||
mkdir -p debian/python3-portprotonqt/usr/share/icons/hicolor/scalable/apps
|
|
||||||
mkdir -p debian/python3-portprotonqt/usr/share/metainfo
|
|
||||||
# Copy additional files from build-aux/usr (if they exist)
|
|
||||||
if [ -d "$(CURDIR)/build-aux/usr/lib/udev/rules.d" ]; then \
|
|
||||||
cp -r $(CURDIR)/build-aux/usr/lib/udev/rules.d/* debian/python3-portprotonqt/usr/lib/udev/rules.d/ || true; \
|
|
||||||
fi
|
|
||||||
if [ -d "$(CURDIR)/build-aux/usr/share/applications" ]; then \
|
|
||||||
cp -r $(CURDIR)/build-aux/usr/share/applications/* debian/python3-portprotonqt/usr/share/applications/ || true; \
|
|
||||||
fi
|
|
||||||
if [ -d "$(CURDIR)/build-aux/usr/share/bash-completion/completions" ]; then \
|
|
||||||
cp -r $(CURDIR)/build-aux/usr/share/bash-completion/completions/* debian/python3-portprotonqt/usr/share/bash-completion/completions/ || true; \
|
|
||||||
fi
|
|
||||||
if [ -d "$(CURDIR)/build-aux/usr/share/icons/hicolor/scalable/apps" ]; then \
|
|
||||||
cp -r $(CURDIR)/build-aux/usr/share/icons/hicolor/scalable/apps/* debian/python3-portprotonqt/usr/share/icons/hicolor/scalable/apps/ || true; \
|
|
||||||
fi
|
|
||||||
if [ -d "$(CURDIR)/build-aux/usr/share/metainfo" ]; then \
|
|
||||||
cp -r $(CURDIR)/build-aux/usr/share/metainfo/* debian/python3-portprotonqt/usr/share/metainfo/ || true; \
|
|
||||||
fi
|
|
||||||
1
debian/source/format
vendored
@@ -1 +0,0 @@
|
|||||||
3.0 (native)
|
|
||||||
7
debian/source/options
vendored
@@ -1,7 +0,0 @@
|
|||||||
# Configuration for Debian source package
|
|
||||||
compression = "gzip"
|
|
||||||
|
|
||||||
# Files and directories to exclude from source package
|
|
||||||
tar-ignore = "dev-scripts"
|
|
||||||
tar-ignore = ".*"
|
|
||||||
tar-ignore = "__pycache__"
|
|
||||||
@@ -20,33 +20,3 @@ Stop Game
|
|||||||
Fullscreen
|
Fullscreen
|
||||||
Fulscreen
|
Fulscreen
|
||||||
\t
|
\t
|
||||||
Горячая
|
|
||||||
vkbasalt
|
|
||||||
dgVoodoo2
|
|
||||||
Zink
|
|
||||||
Vulkan
|
|
||||||
VKD3D
|
|
||||||
DirectX12
|
|
||||||
Prev Dir
|
|
||||||
Forced
|
|
||||||
GOverlay
|
|
||||||
Glide
|
|
||||||
all
|
|
||||||
futex
|
|
||||||
DLSS
|
|
||||||
fullscreen
|
|
||||||
ProtonGE
|
|
||||||
window
|
|
||||||
compositing
|
|
||||||
Zink
|
|
||||||
Use
|
|
||||||
bundled
|
|
||||||
dxvk
|
|
||||||
older games
|
|
||||||
versions
|
|
||||||
DLL Overrides
|
|
||||||
COMP
|
|
||||||
VKD3D
|
|
||||||
Select needed
|
|
||||||
CPUs
|
|
||||||
cores
|
|
||||||
|
|||||||
@@ -17,31 +17,17 @@ import json
|
|||||||
|
|
||||||
|
|
||||||
class PySide6DependencyAnalyzer:
|
class PySide6DependencyAnalyzer:
|
||||||
def __init__(self, project_root: Path = None):
|
def __init__(self):
|
||||||
# Системные библиотеки, которые нужно всегда оставлять
|
# Системные библиотеки, которые нужно всегда оставлять
|
||||||
self.system_libs = {
|
self.system_libs = {
|
||||||
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
'libQt6XcbQpa', 'libQt6Wayland', 'libQt6Egl',
|
||||||
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus',
|
'libicudata', 'libicuuc', 'libicui18n', 'libQt6DBus'
|
||||||
'libQt6Svg'
|
|
||||||
}
|
|
||||||
|
|
||||||
self.critical_modules = {
|
|
||||||
'QtSvg',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.real_dependencies = {}
|
self.real_dependencies = {}
|
||||||
self.used_modules_code = set()
|
self.used_modules_code = set()
|
||||||
self.used_modules_ldd = set()
|
self.used_modules_ldd = set()
|
||||||
self.all_required_modules = set()
|
self.all_required_modules = set()
|
||||||
# Определяем корень проекта
|
|
||||||
if project_root is None:
|
|
||||||
# Корень проекта - две директории выше от скрипта
|
|
||||||
self.project_root = Path(__file__).parent.parent
|
|
||||||
else:
|
|
||||||
self.project_root = project_root
|
|
||||||
|
|
||||||
self.venv_path = self.project_root / ".venv"
|
|
||||||
self.build_path = self.project_root / "build-aux"
|
|
||||||
|
|
||||||
def find_python_files(self, directory: Path) -> List[Path]:
|
def find_python_files(self, directory: Path) -> List[Path]:
|
||||||
"""Находит все Python файлы в директории"""
|
"""Находит все Python файлы в директории"""
|
||||||
@@ -58,61 +44,24 @@ class PySide6DependencyAnalyzer:
|
|||||||
"""Находит все PySide6 библиотеки (.so файлы)"""
|
"""Находит все PySide6 библиотеки (.so файлы)"""
|
||||||
libs = {}
|
libs = {}
|
||||||
|
|
||||||
# Ищем venv в корне проекта
|
# Поиск в единственной локации
|
||||||
venv_candidates = [
|
search_path = Path("../.venv/lib/python3.10/site-packages/PySide6")
|
||||||
self.venv_path, # .venv
|
print(f"Поиск PySide6 библиотек в: {search_path}")
|
||||||
self.project_root / "venv",
|
|
||||||
self.project_root / ".virtualenv",
|
|
||||||
]
|
|
||||||
|
|
||||||
pyside6_path = None
|
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 в venv
|
# Также ищем в подпапках
|
||||||
for venv in venv_candidates:
|
for subdir in search_path.iterdir():
|
||||||
if venv.exists():
|
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
||||||
# Ищем Python версию
|
for so_file in subdir.glob("*.so*"):
|
||||||
lib_path = venv / "lib"
|
if 'Qt' in so_file.name:
|
||||||
if lib_path.exists():
|
libs[subdir.name] = so_file
|
||||||
for python_dir in lib_path.iterdir():
|
break
|
||||||
if python_dir.name.startswith('python'):
|
|
||||||
candidate = python_dir / "site-packages" / "PySide6"
|
|
||||||
if candidate.exists():
|
|
||||||
pyside6_path = candidate
|
|
||||||
print(f"Найден PySide6 в: {candidate}")
|
|
||||||
break
|
|
||||||
if pyside6_path:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not pyside6_path:
|
|
||||||
print(f"Предупреждение: PySide6 не найден в venv, проверяем AppDir...")
|
|
||||||
# Если не нашли в venv, пробуем в AppDir
|
|
||||||
if base_path:
|
|
||||||
appdir_candidate = base_path / "AppDir/usr/local/lib"
|
|
||||||
if appdir_candidate.exists():
|
|
||||||
for python_dir in appdir_candidate.iterdir():
|
|
||||||
if python_dir.name.startswith('python'):
|
|
||||||
candidate = python_dir / "dist-packages" / "PySide6"
|
|
||||||
if candidate.exists():
|
|
||||||
pyside6_path = candidate
|
|
||||||
print(f"Найден PySide6 в AppDir: {candidate}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if not pyside6_path:
|
|
||||||
return libs
|
|
||||||
|
|
||||||
# Ищем .so файлы модулей
|
|
||||||
for so_file in pyside6_path.glob("Qt*.*.so"):
|
|
||||||
module_name = so_file.stem.split('.')[0] # QtCore.abi3.so -> QtCore
|
|
||||||
if module_name.startswith('Qt'):
|
|
||||||
libs[module_name] = so_file
|
|
||||||
|
|
||||||
# Также ищем в подпапках
|
|
||||||
for subdir in pyside6_path.iterdir():
|
|
||||||
if subdir.is_dir() and subdir.name.startswith('Qt'):
|
|
||||||
for so_file in subdir.glob("*.so*"):
|
|
||||||
if 'Qt' in so_file.name:
|
|
||||||
libs[subdir.name] = so_file
|
|
||||||
break
|
|
||||||
|
|
||||||
return libs
|
return libs
|
||||||
|
|
||||||
@@ -308,10 +257,8 @@ class PySide6DependencyAnalyzer:
|
|||||||
|
|
||||||
# Модули для удаления
|
# Модули для удаления
|
||||||
if removable_modules:
|
if removable_modules:
|
||||||
removable_filtered = [m for m in removable_modules if m not in self.critical_modules]
|
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_modules)])
|
||||||
if removable_filtered:
|
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
||||||
modules_list = ','.join([f"{mod}*" for mod in sorted(removable_filtered)])
|
|
||||||
cleanup_lines.append(f" - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{{{modules_list}}}")
|
|
||||||
|
|
||||||
# Генерируем команду для удаления нативных библиотек с сохранением нужных
|
# Генерируем команду для удаления нативных библиотек с сохранением нужных
|
||||||
required_libs = set()
|
required_libs = set()
|
||||||
@@ -329,82 +276,39 @@ class PySide6DependencyAnalyzer:
|
|||||||
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
f" - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/lib/!({keep_pattern})"
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Заменяем блок очистки в рецепте
|
||||||
import re
|
import re
|
||||||
|
|
||||||
# Ищем весь блок команд очистки PySide6 (от первой rm до AppDir:)
|
# Ищем блок "# 5) чистим от ненужных модулей и бинарников" до следующего комментария или до AppDir:
|
||||||
# Паттерн: после " - cp -r lib AppDir/usr\n" идут команды rm, а затем "AppDir:"
|
pattern = r'( # 5\) чистим от ненужных модулей и бинарников\n).*?(?=\nAppDir:|\n # [0-9]+\)|$)'
|
||||||
pattern = r'( - cp -r lib AppDir/usr\n)((?: - (?:rm|shopt).*\n)*?)(?=AppDir:)'
|
|
||||||
|
|
||||||
match = re.search(pattern, recipe_content)
|
new_cleanup_block = " # 5) чистим от ненужных модулей и бинарников\n" + '\n'.join(cleanup_lines)
|
||||||
|
|
||||||
if not match:
|
updated_recipe = re.sub(pattern, new_cleanup_block, recipe_content, flags=re.DOTALL)
|
||||||
print("ПРЕДУПРЕЖДЕНИЕ: Не удалось найти блок очистки в рецепте")
|
|
||||||
print("Добавляем команды очистки перед блоком AppDir:")
|
|
||||||
|
|
||||||
# Просто вставим команды перед AppDir:
|
|
||||||
appdir_pos = recipe_content.find('AppDir:')
|
|
||||||
if appdir_pos != -1:
|
|
||||||
new_content = (
|
|
||||||
recipe_content[:appdir_pos] +
|
|
||||||
'\n'.join(cleanup_lines) + '\n' +
|
|
||||||
recipe_content[appdir_pos:]
|
|
||||||
)
|
|
||||||
return new_content
|
|
||||||
else:
|
|
||||||
print("ОШИБКА: Не найден блок AppDir: в рецепте")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
# Создаем замену - группа 1 (cp -r lib) + новые команды очистки
|
|
||||||
replacement = r'\1' + '\n'.join(cleanup_lines) + '\n'
|
|
||||||
|
|
||||||
updated_recipe = re.sub(pattern, replacement, recipe_content, count=1)
|
|
||||||
|
|
||||||
return updated_recipe
|
return updated_recipe
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
parser = argparse.ArgumentParser(description='Анализ зависимостей PySide6 модулей с использованием ldd')
|
||||||
parser.add_argument('project_path', nargs='?', default='.',
|
parser.add_argument('project_path', help='Путь к проекту для анализа')
|
||||||
help='Путь к проекту для анализа (по умолчанию: текущая директория)')
|
|
||||||
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
parser.add_argument('--appdir', help='Путь к AppDir для поиска PySide6 библиотек')
|
||||||
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
parser.add_argument('--output', '-o', help='Путь для сохранения результатов (JSON)')
|
||||||
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
parser.add_argument('--verbose', '-v', action='store_true', help='Подробный вывод')
|
||||||
parser.add_argument('--venv', help='Путь к виртуальному окружению (по умолчанию: .venv в корне проекта)')
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
project_path = Path(args.project_path).resolve()
|
project_path = Path(args.project_path)
|
||||||
if not project_path.exists():
|
if not project_path.exists():
|
||||||
print(f"Ошибка: путь {project_path} не существует")
|
print(f"Ошибка: путь {project_path} не существует")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
appdir_path = Path(args.appdir).resolve() if args.appdir else None
|
appdir_path = Path(args.appdir) if args.appdir else None
|
||||||
if appdir_path and not appdir_path.exists():
|
if appdir_path and not appdir_path.exists():
|
||||||
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
print(f"Предупреждение: AppDir путь {appdir_path} не существует")
|
||||||
appdir_path = None
|
appdir_path = None
|
||||||
|
|
||||||
# Определяем корень проекта
|
analyzer = PySide6DependencyAnalyzer()
|
||||||
# Если запущен из подпапки проекта, ищем корень
|
|
||||||
project_root = project_path
|
|
||||||
if (project_path / ".git").exists() or (project_path / "pyproject.toml").exists():
|
|
||||||
project_root = project_path
|
|
||||||
else:
|
|
||||||
# Пытаемся найти корень проекта
|
|
||||||
current = project_path
|
|
||||||
while current != current.parent:
|
|
||||||
if (current / ".git").exists() or (current / "pyproject.toml").exists():
|
|
||||||
project_root = current
|
|
||||||
break
|
|
||||||
current = current.parent
|
|
||||||
|
|
||||||
print(f"Корень проекта: {project_root}")
|
|
||||||
|
|
||||||
analyzer = PySide6DependencyAnalyzer(project_root=project_root)
|
|
||||||
|
|
||||||
# Если указан custom venv путь
|
|
||||||
if args.venv:
|
|
||||||
analyzer.venv_path = Path(args.venv).resolve()
|
|
||||||
print(f"Использую указанный venv: {analyzer.venv_path}")
|
|
||||||
|
|
||||||
results = analyzer.analyze_project(project_path, appdir_path)
|
results = analyzer.analyze_project(project_path, appdir_path)
|
||||||
|
|
||||||
# Сохраняем в анализатор для генерации команд
|
# Сохраняем в анализатор для генерации команд
|
||||||
@@ -443,13 +347,13 @@ def main():
|
|||||||
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
print(f"\nПотенциальная экономия места: {results['analysis_summary']['space_saving_potential']}")
|
||||||
|
|
||||||
if args.verbose and results['real_dependencies']:
|
if args.verbose and results['real_dependencies']:
|
||||||
print(f"\nРеальные зависимости (ldd):")
|
Devlin(f"\nРеальные зависимости (ldd):")
|
||||||
for module, deps in results['real_dependencies'].items():
|
for module, deps in results['real_dependencies'].items():
|
||||||
if deps:
|
if deps:
|
||||||
print(f" {module} → {', '.join(deps)}")
|
print(f" {module} → {', '.join(deps)}")
|
||||||
|
|
||||||
# Обновляем AppImage рецепт
|
# Обновляем AppImage рецепт
|
||||||
recipe_path = analyzer.build_path / "AppImageBuilder.yml"
|
recipe_path = Path("../build-aux/AppImageBuilder.yml")
|
||||||
if recipe_path.exists():
|
if recipe_path.exists():
|
||||||
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
updated_recipe = analyzer.generate_appimage_recipe(results['removable'], recipe_path)
|
||||||
if updated_recipe:
|
if updated_recipe:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import argparse
|
|||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import date, datetime
|
from datetime import date
|
||||||
|
|
||||||
# Base directory of the project
|
# Base directory of the project
|
||||||
BASE_DIR = Path(__file__).parent.parent
|
BASE_DIR = Path(__file__).parent.parent
|
||||||
@@ -16,7 +16,6 @@ PYPROJECT = BASE_DIR / "pyproject.toml"
|
|||||||
APP_PY = BASE_DIR / "portprotonqt" / "app.py"
|
APP_PY = BASE_DIR / "portprotonqt" / "app.py"
|
||||||
GITEA_WORKFLOW = BASE_DIR / ".gitea" / "workflows" / "build.yml"
|
GITEA_WORKFLOW = BASE_DIR / ".gitea" / "workflows" / "build.yml"
|
||||||
CHANGELOG = BASE_DIR / "CHANGELOG.md"
|
CHANGELOG = BASE_DIR / "CHANGELOG.md"
|
||||||
DEBIAN_CHANGELOG = BASE_DIR / "debian" / "changelog"
|
|
||||||
|
|
||||||
def bump_appimage(path: Path, old: str, new: str) -> bool:
|
def bump_appimage(path: Path, old: str, new: str) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -110,138 +109,6 @@ def bump_changelog(path: Path, old: str, new: str) -> bool:
|
|||||||
path.write_text(new_text, encoding='utf-8')
|
path.write_text(new_text, encoding='utf-8')
|
||||||
return bool(count)
|
return bool(count)
|
||||||
|
|
||||||
def bump_debian_changelog(path: Path, old: str, new: str) -> bool:
|
|
||||||
"""
|
|
||||||
Update debian/changelog with new version
|
|
||||||
"""
|
|
||||||
if not path.exists():
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Extract changelog entries from CHANGELOG.md for this version
|
|
||||||
changelog_md_path = BASE_DIR / "CHANGELOG.md"
|
|
||||||
changelog_entries = []
|
|
||||||
changelog_date = None
|
|
||||||
|
|
||||||
if changelog_md_path.exists():
|
|
||||||
changelog_text = changelog_md_path.read_text(encoding='utf-8')
|
|
||||||
lines = changelog_text.splitlines()
|
|
||||||
|
|
||||||
# Find the section for the new version and extract the date
|
|
||||||
start_reading = False
|
|
||||||
end_reading = False
|
|
||||||
in_contributors_section = False
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if line.startswith(f"## [{new}]"):
|
|
||||||
# Extract date from line like "## [0.1.9] - 2025-12-08"
|
|
||||||
date_match = re.search(r'\[.+\] - (\d{4}-\d{2}-\d{2})', line)
|
|
||||||
if date_match:
|
|
||||||
changelog_date_str = date_match.group(1)
|
|
||||||
# Convert to the expected Debian format
|
|
||||||
date_obj = datetime.strptime(changelog_date_str, '%Y-%m-%d')
|
|
||||||
changelog_date = date_obj.strftime('%a, %d %b %Y') + " 00:00:00 +0000"
|
|
||||||
|
|
||||||
start_reading = True
|
|
||||||
in_contributors_section = False
|
|
||||||
continue
|
|
||||||
elif line.startswith("## [") and start_reading:
|
|
||||||
end_reading = True
|
|
||||||
break
|
|
||||||
elif line.strip().lower() == "### contributors":
|
|
||||||
# Start of contributors section - skip following lines until next section
|
|
||||||
in_contributors_section = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Skip section headers and contributor sections
|
|
||||||
if start_reading and not end_reading and not in_contributors_section:
|
|
||||||
stripped_line = line.strip()
|
|
||||||
if stripped_line and not line.startswith("#") and not line.startswith("[") and not line.lower().startswith("###"):
|
|
||||||
# Check if this line is a list item with changes
|
|
||||||
if re.match(r'^\s*[*-]\s+', line):
|
|
||||||
# Remove markdown list formatting and add proper Debian format
|
|
||||||
clean_line = re.sub(r'^\s*[*-]\s+', ' * ', line.rstrip())
|
|
||||||
# Remove common markdown formatting like backticks
|
|
||||||
clean_line = re.sub(r'`([^`]+)`', r'"\1"', clean_line) # Replace `code` with "code"
|
|
||||||
changelog_entries.append(clean_line)
|
|
||||||
# Also include lines that are sub-items (indented changes)
|
|
||||||
elif line.startswith(" ") and re.match(r'^\s*[*-]\s+', line[4:]):
|
|
||||||
clean_line = re.sub(r'^\s*[*-]\s+', ' * ', line[4:].rstrip())
|
|
||||||
clean_line = " " + clean_line # Add extra indentation
|
|
||||||
# Remove common markdown formatting
|
|
||||||
clean_line = re.sub(r'`([^`]+)`', r'"\1"', clean_line)
|
|
||||||
changelog_entries.append(clean_line)
|
|
||||||
|
|
||||||
# If no specific entries found for this version, use generic message
|
|
||||||
if not changelog_entries:
|
|
||||||
changelog_entries = [" * New upstream release"]
|
|
||||||
|
|
||||||
# Use changelog date if available, otherwise use current time
|
|
||||||
current_time = changelog_date if changelog_date else datetime.now().strftime('%a, %d %b %Y %H:%M:%S +0000')
|
|
||||||
|
|
||||||
# Read the existing changelog to get maintainer info and other fields
|
|
||||||
text = path.read_text(encoding='utf-8')
|
|
||||||
|
|
||||||
# If the file is empty or doesn't contain proper maintainer info, use a default
|
|
||||||
lines = text.splitlines()
|
|
||||||
|
|
||||||
if not lines or not any(line.startswith(" -- ") for line in lines):
|
|
||||||
# Create a default changelog entry with proper format
|
|
||||||
package_name = "portprotonqt"
|
|
||||||
new_version_line = f"{package_name} ({new}-1) unstable; urgency=medium"
|
|
||||||
|
|
||||||
# Default maintainer info from the original file
|
|
||||||
default_maintainer = "Boris Yumankulov <boria138@altlinux.org>"
|
|
||||||
maintainer_line = f" -- {default_maintainer} {current_time}"
|
|
||||||
|
|
||||||
new_content = new_version_line + "\n\n" + "\n".join(changelog_entries) + "\n\n" + maintainer_line + "\n"
|
|
||||||
else:
|
|
||||||
# Extract the header template from the current first entry
|
|
||||||
header_parts = []
|
|
||||||
entry_end_index = 0
|
|
||||||
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
header_parts.append(line)
|
|
||||||
if line.startswith(" -- "):
|
|
||||||
entry_end_index = i + 1
|
|
||||||
break
|
|
||||||
|
|
||||||
# Construct new changelog entry
|
|
||||||
new_entry_lines = []
|
|
||||||
|
|
||||||
if header_parts:
|
|
||||||
# Parse the first line to extract package name (before the version)
|
|
||||||
first_line = header_parts[0]
|
|
||||||
# Extract package name by getting everything before the opening parenthesis
|
|
||||||
if '(' in first_line:
|
|
||||||
package_name = first_line.split('(')[0].strip()
|
|
||||||
new_version_line = f"{package_name} ({new}-1) unstable; urgency=medium"
|
|
||||||
else:
|
|
||||||
# Fallback: if no parentheses found, use a default format
|
|
||||||
new_version_line = f"portprotonqt ({new}-1) unstable; urgency=medium"
|
|
||||||
|
|
||||||
new_entry_lines.append(new_version_line)
|
|
||||||
|
|
||||||
# Add the changelog entries
|
|
||||||
new_entry_lines.extend(changelog_entries)
|
|
||||||
|
|
||||||
# Add the maintainer info and timestamp
|
|
||||||
for j in range(1, len(header_parts)):
|
|
||||||
if header_parts[j].startswith(" -- "):
|
|
||||||
# Extract the maintainer information (everything after "-- ")
|
|
||||||
maintainer_part = header_parts[j][4:] # Remove leading " -- "
|
|
||||||
# Extract only the name and email, ignore timestamp
|
|
||||||
maintainer_info = maintainer_part.split(' ')[0].strip()
|
|
||||||
new_entry_lines.append(f" -- {maintainer_info} {current_time}")
|
|
||||||
elif not header_parts[j].startswith(" *"): # Skip existing changes since we added new ones
|
|
||||||
new_entry_lines.append(header_parts[j])
|
|
||||||
|
|
||||||
# Reconstruct the file with new entry at the top followed by the rest
|
|
||||||
new_content = '\n'.join(new_entry_lines) + '\n' + '\n'.join(lines[entry_end_index:])
|
|
||||||
|
|
||||||
path.write_text(new_content, encoding='utf-8')
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Bump project version in specific files')
|
parser = argparse.ArgumentParser(description='Bump project version in specific files')
|
||||||
parser.add_argument('old', help='Old version string')
|
parser.add_argument('old', help='Old version string')
|
||||||
@@ -256,8 +123,7 @@ def main():
|
|||||||
(PYPROJECT, bump_pyproject),
|
(PYPROJECT, bump_pyproject),
|
||||||
(APP_PY, bump_app_py),
|
(APP_PY, bump_app_py),
|
||||||
(GITEA_WORKFLOW, bump_workflow),
|
(GITEA_WORKFLOW, bump_workflow),
|
||||||
(CHANGELOG, bump_changelog),
|
(CHANGELOG, bump_changelog)
|
||||||
(DEBIAN_CHANGELOG, bump_debian_changelog)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
updated = []
|
updated = []
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ Current translation status:
|
|||||||
|
|
||||||
| Locale | Progress | Translated |
|
| Locale | Progress | Translated |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 341 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 341 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 193 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 341 of 341 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 of 193 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
|
|
||||||
| Локаль | Прогресс | Переведено |
|
| Локаль | Прогресс | Переведено |
|
||||||
| :----- | -------: | ---------: |
|
| :----- | -------: | ---------: |
|
||||||
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 341 |
|
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||||
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 341 |
|
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 193 |
|
||||||
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 341 из 341 |
|
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 193 из 193 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from typing import Any, cast
|
|
||||||
from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstractAnimation, QParallelAnimationGroup, QRect, Qt, QPoint
|
from PySide6.QtCore import QPropertyAnimation, QByteArray, QEasingCurve, QAbstractAnimation, QParallelAnimationGroup, QRect, Qt, QPoint
|
||||||
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
|
from PySide6.QtGui import QPainter, QPen, QColor, QConicalGradient, QBrush
|
||||||
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
from PySide6.QtWidgets import QWidget, QGraphicsOpacityEffect
|
||||||
@@ -237,31 +236,14 @@ class DetailPageAnimations:
|
|||||||
self.main_window = main_window
|
self.main_window = main_window
|
||||||
self.theme_manager = ThemeManager()
|
self.theme_manager = ThemeManager()
|
||||||
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
|
self.theme = theme if theme is not None else self.theme_manager.apply_theme(read_theme_from_config())
|
||||||
# Ensure the main window has an animations dict
|
self.animations = main_window._animations if hasattr(main_window, '_animations') else {}
|
||||||
if not hasattr(main_window, '_animations'):
|
|
||||||
main_window._animations = {}
|
|
||||||
self.animations = main_window._animations
|
|
||||||
|
|
||||||
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):
|
def animate_detail_page(self, detail_page: QWidget, load_image_and_restore_effect: Callable, cleanup_animation: Callable):
|
||||||
"""Animate the detail page based on theme settings."""
|
"""Animate the detail page based on theme settings."""
|
||||||
# Check if the detail page is still valid before proceeding
|
|
||||||
if not detail_page or detail_page.isHidden() or detail_page.parent() is None:
|
|
||||||
logger.warning("Detail page is not valid, skipping enter animation")
|
|
||||||
load_image_and_restore_effect()
|
|
||||||
cleanup_animation()
|
|
||||||
return
|
|
||||||
|
|
||||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration", 350)
|
||||||
|
|
||||||
if animation_type == "fade":
|
if animation_type == "fade":
|
||||||
# Check again if page is still valid before starting animation
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during fade setup, skipping animation")
|
|
||||||
load_image_and_restore_effect()
|
|
||||||
cleanup_animation()
|
|
||||||
return
|
|
||||||
|
|
||||||
original_effect = detail_page.graphicsEffect()
|
original_effect = detail_page.graphicsEffect()
|
||||||
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=True)
|
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=True)
|
||||||
opacity_effect.setOpacity(0.0)
|
opacity_effect.setOpacity(0.0)
|
||||||
@@ -270,36 +252,17 @@ class DetailPageAnimations:
|
|||||||
animation.setDuration(duration)
|
animation.setDuration(duration)
|
||||||
animation.setStartValue(0.0)
|
animation.setStartValue(0.0)
|
||||||
animation.setEndValue(0.999)
|
animation.setEndValue(0.999)
|
||||||
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
self.animations[detail_page] = animation
|
||||||
def restore_effect():
|
def restore_effect():
|
||||||
try:
|
try:
|
||||||
# Check if page is still valid before restoring effect
|
detail_page.setGraphicsEffect(original_effect) # type: ignore
|
||||||
if detail_page and not detail_page.isHidden():
|
|
||||||
detail_page.setGraphicsEffect(cast(Any, original_effect))
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
logger.warning("Original effect already deleted")
|
logger.warning("Original effect already deleted")
|
||||||
|
animation.finished.connect(restore_effect)
|
||||||
# Only start animation if page is still valid
|
animation.finished.connect(load_image_and_restore_effect)
|
||||||
if detail_page and not detail_page.isHidden():
|
animation.finished.connect(opacity_effect.deleteLater)
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
||||||
self.animations[detail_page] = animation
|
|
||||||
animation.finished.connect(restore_effect)
|
|
||||||
animation.finished.connect(load_image_and_restore_effect)
|
|
||||||
animation.finished.connect(opacity_effect.deleteLater)
|
|
||||||
else:
|
|
||||||
logger.warning("Detail page invalid when starting fade, cleaning up")
|
|
||||||
restore_effect()
|
|
||||||
load_image_and_restore_effect()
|
|
||||||
opacity_effect.deleteLater()
|
|
||||||
cleanup_animation()
|
|
||||||
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
||||||
# Check again if page is still valid before starting animation
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during slide setup, skipping animation")
|
|
||||||
load_image_and_restore_effect()
|
|
||||||
cleanup_animation()
|
|
||||||
return
|
|
||||||
|
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration", 500)
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
||||||
start_pos = {
|
start_pos = {
|
||||||
@@ -314,25 +277,11 @@ class DetailPageAnimations:
|
|||||||
animation.setStartValue(start_pos)
|
animation.setStartValue(start_pos)
|
||||||
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
|
animation.setEndValue(self.main_window.stackedWidget.rect().topLeft())
|
||||||
animation.setEasingCurve(easing_curve)
|
animation.setEasingCurve(easing_curve)
|
||||||
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
# Only start animation if page is still valid
|
self.animations[detail_page] = animation
|
||||||
if detail_page and not detail_page.isHidden():
|
animation.finished.connect(cleanup_animation)
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
animation.finished.connect(load_image_and_restore_effect)
|
||||||
self.animations[detail_page] = animation
|
|
||||||
animation.finished.connect(cleanup_animation)
|
|
||||||
animation.finished.connect(load_image_and_restore_effect)
|
|
||||||
else:
|
|
||||||
logger.warning("Detail page invalid when starting slide, cleaning up")
|
|
||||||
load_image_and_restore_effect()
|
|
||||||
cleanup_animation()
|
|
||||||
elif animation_type == "bounce":
|
elif animation_type == "bounce":
|
||||||
# Check again if page is still valid before starting animation
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during bounce setup, skipping animation")
|
|
||||||
load_image_and_restore_effect()
|
|
||||||
cleanup_animation()
|
|
||||||
return
|
|
||||||
|
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration", 400)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration", 400)
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve", "OutCubic")])
|
||||||
detail_page.setWindowOpacity(0.0)
|
detail_page.setWindowOpacity(0.0)
|
||||||
@@ -351,27 +300,14 @@ class DetailPageAnimations:
|
|||||||
group_anim = QParallelAnimationGroup()
|
group_anim = QParallelAnimationGroup()
|
||||||
group_anim.addAnimation(opacity_anim)
|
group_anim.addAnimation(opacity_anim)
|
||||||
group_anim.addAnimation(geometry_anim)
|
group_anim.addAnimation(geometry_anim)
|
||||||
|
group_anim.finished.connect(load_image_and_restore_effect)
|
||||||
# Only start animation if page is still valid
|
group_anim.finished.connect(cleanup_animation)
|
||||||
if detail_page and not detail_page.isHidden():
|
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
self.animations[detail_page] = group_anim
|
||||||
self.animations[detail_page] = group_anim
|
|
||||||
group_anim.finished.connect(load_image_and_restore_effect)
|
|
||||||
group_anim.finished.connect(cleanup_animation)
|
|
||||||
else:
|
|
||||||
logger.warning("Detail page invalid when starting bounce, cleaning up")
|
|
||||||
load_image_and_restore_effect()
|
|
||||||
cleanup_animation()
|
|
||||||
|
|
||||||
def animate_detail_page_exit(self, detail_page: QWidget, cleanup_callback: Callable):
|
def animate_detail_page_exit(self, detail_page: QWidget, cleanup_callback: Callable):
|
||||||
"""Animate the detail page exit based on theme settings."""
|
"""Animate the detail page exit based on theme settings."""
|
||||||
try:
|
try:
|
||||||
# Check if the detail page is still valid before proceeding
|
|
||||||
if not detail_page or detail_page.isHidden() or detail_page.parent() is None:
|
|
||||||
logger.warning("Detail page is not valid, skipping exit animation")
|
|
||||||
cleanup_callback()
|
|
||||||
return
|
|
||||||
|
|
||||||
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
animation_type = self.theme.GAME_CARD_ANIMATION.get("detail_page_animation_type", "fade")
|
||||||
|
|
||||||
# Safely stop and remove any existing animation
|
# Safely stop and remove any existing animation
|
||||||
@@ -390,13 +326,6 @@ class DetailPageAnimations:
|
|||||||
# Define animation based on type
|
# Define animation based on type
|
||||||
if animation_type == "fade":
|
if animation_type == "fade":
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration_exit", 350)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_fade_duration_exit", 350)
|
||||||
|
|
||||||
# Check if page is still valid before accessing properties
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during fade exit setup, skipping animation")
|
|
||||||
cleanup_callback()
|
|
||||||
return
|
|
||||||
|
|
||||||
original_effect = detail_page.graphicsEffect()
|
original_effect = detail_page.graphicsEffect()
|
||||||
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=False)
|
opacity_effect = SafeOpacityEffect(detail_page, disable_at_full=False)
|
||||||
opacity_effect.setOpacity(0.999)
|
opacity_effect.setOpacity(0.999)
|
||||||
@@ -405,36 +334,18 @@ class DetailPageAnimations:
|
|||||||
animation.setDuration(duration)
|
animation.setDuration(duration)
|
||||||
animation.setStartValue(0.999)
|
animation.setStartValue(0.999)
|
||||||
animation.setEndValue(0.0)
|
animation.setEndValue(0.0)
|
||||||
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
|
self.animations[detail_page] = animation
|
||||||
def restore_and_cleanup():
|
def restore_and_cleanup():
|
||||||
try:
|
try:
|
||||||
# Check if page is still valid before restoring effect
|
detail_page.setGraphicsEffect(original_effect) # type: ignore
|
||||||
if detail_page and not detail_page.isHidden():
|
|
||||||
detail_page.setGraphicsEffect(cast(Any, original_effect))
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
logger.debug("Original effect already deleted")
|
logger.debug("Original effect already deleted")
|
||||||
cleanup_callback()
|
cleanup_callback()
|
||||||
|
animation.finished.connect(restore_and_cleanup)
|
||||||
# Check if animation is still valid before starting
|
animation.finished.connect(opacity_effect.deleteLater)
|
||||||
if animation and not detail_page.isHidden():
|
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
||||||
self.animations[detail_page] = animation
|
|
||||||
animation.finished.connect(restore_and_cleanup)
|
|
||||||
animation.finished.connect(opacity_effect.deleteLater)
|
|
||||||
else:
|
|
||||||
logger.warning("Animation or detail page invalid when starting fade exit, cleaning up")
|
|
||||||
restore_and_cleanup()
|
|
||||||
opacity_effect.deleteLater()
|
|
||||||
|
|
||||||
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
elif animation_type in ["slide_left", "slide_right", "slide_up", "slide_down"]:
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_slide_duration_exit", 500)
|
||||||
|
|
||||||
# Check if page is still valid before accessing properties
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during slide exit setup, skipping animation")
|
|
||||||
cleanup_callback()
|
|
||||||
return
|
|
||||||
|
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
||||||
end_pos = {
|
end_pos = {
|
||||||
"slide_left": QPoint(-self.main_window.width(), 0),
|
"slide_left": QPoint(-self.main_window.width(), 0),
|
||||||
@@ -442,37 +353,16 @@ class DetailPageAnimations:
|
|||||||
"slide_up": QPoint(0, self.main_window.height()),
|
"slide_up": QPoint(0, self.main_window.height()),
|
||||||
"slide_down": QPoint(0, -self.main_window.height())
|
"slide_down": QPoint(0, -self.main_window.height())
|
||||||
}[animation_type]
|
}[animation_type]
|
||||||
|
|
||||||
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
animation = QPropertyAnimation(detail_page, QByteArray(b"pos"))
|
||||||
animation.setDuration(duration)
|
animation.setDuration(duration)
|
||||||
animation.setStartValue(detail_page.pos())
|
animation.setStartValue(detail_page.pos())
|
||||||
animation.setEndValue(end_pos)
|
animation.setEndValue(end_pos)
|
||||||
animation.setEasingCurve(easing_curve)
|
animation.setEasingCurve(easing_curve)
|
||||||
|
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
def slide_cleanup():
|
self.animations[detail_page] = animation
|
||||||
# Check if page is still valid before cleanup
|
animation.finished.connect(cleanup_callback)
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.debug("Detail page already cleaned up")
|
|
||||||
cleanup_callback()
|
|
||||||
|
|
||||||
# Check if animation is still valid before starting
|
|
||||||
if animation and not detail_page.isHidden():
|
|
||||||
animation.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
|
||||||
self.animations[detail_page] = animation
|
|
||||||
animation.finished.connect(slide_cleanup)
|
|
||||||
else:
|
|
||||||
logger.warning("Animation or detail page invalid when starting slide exit, cleaning up")
|
|
||||||
slide_cleanup()
|
|
||||||
|
|
||||||
elif animation_type == "bounce":
|
elif animation_type == "bounce":
|
||||||
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration_exit", 400)
|
duration = self.theme.GAME_CARD_ANIMATION.get("detail_page_bounce_duration_exit", 400)
|
||||||
|
|
||||||
# Check if page is still valid before accessing properties
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during bounce exit setup, skipping animation")
|
|
||||||
cleanup_callback()
|
|
||||||
return
|
|
||||||
|
|
||||||
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
easing_curve = QEasingCurve(QEasingCurve.Type[self.theme.GAME_CARD_ANIMATION.get("detail_page_easing_curve_exit", "InCubic")])
|
||||||
opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity"))
|
opacity_anim = QPropertyAnimation(detail_page, QByteArray(b"windowOpacity"))
|
||||||
opacity_anim.setDuration(duration)
|
opacity_anim.setDuration(duration)
|
||||||
@@ -485,38 +375,13 @@ class DetailPageAnimations:
|
|||||||
geometry_anim.setStartValue(detail_page.geometry())
|
geometry_anim.setStartValue(detail_page.geometry())
|
||||||
geometry_anim.setEndValue(final_rect)
|
geometry_anim.setEndValue(final_rect)
|
||||||
geometry_anim.setEasingCurve(easing_curve)
|
geometry_anim.setEasingCurve(easing_curve)
|
||||||
|
|
||||||
# Check if animations are still valid before creating group
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during bounce exit setup, cleaning up")
|
|
||||||
cleanup_callback()
|
|
||||||
return
|
|
||||||
|
|
||||||
group_anim = QParallelAnimationGroup()
|
group_anim = QParallelAnimationGroup()
|
||||||
group_anim.addAnimation(opacity_anim)
|
group_anim.addAnimation(opacity_anim)
|
||||||
group_anim.addAnimation(geometry_anim)
|
group_anim.addAnimation(geometry_anim)
|
||||||
|
group_anim.finished.connect(cleanup_callback)
|
||||||
# Check if group animation is still valid before connecting
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.warning("Detail page became invalid during group animation setup, cleaning up")
|
|
||||||
cleanup_callback()
|
|
||||||
return
|
|
||||||
|
|
||||||
def bounce_cleanup():
|
|
||||||
# Check if page is still valid before cleanup
|
|
||||||
if not detail_page or detail_page.isHidden():
|
|
||||||
logger.debug("Detail page already cleaned up")
|
|
||||||
cleanup_callback()
|
|
||||||
|
|
||||||
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
group_anim.start(QAbstractAnimation.DeletionPolicy.DeleteWhenStopped)
|
||||||
self.animations[detail_page] = group_anim
|
self.animations[detail_page] = group_anim
|
||||||
group_anim.finished.connect(bounce_cleanup)
|
|
||||||
except RuntimeError:
|
|
||||||
# Widget was already deleted, which is expected after deleteLater()
|
|
||||||
logger.debug("Detail page already deleted during animation setup")
|
|
||||||
cleanup_callback()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
|
logger.error(f"Error in animate_detail_page_exit: {e}", exc_info=True)
|
||||||
if detail_page in self.animations:
|
self.animations.pop(detail_page, None)
|
||||||
self.animations.pop(detail_page, None)
|
|
||||||
cleanup_callback()
|
cleanup_callback()
|
||||||
|
|||||||
@@ -1,45 +1,17 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo
|
||||||
import subprocess
|
|
||||||
from PySide6.QtCore import QLocale, QTranslator, QLibraryInfo, QTimer, Qt
|
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
from PySide6.QtGui import QIcon
|
from PySide6.QtGui import QIcon
|
||||||
from PySide6.QtNetwork import QLocalServer, QLocalSocket
|
|
||||||
|
|
||||||
from portprotonqt.main_window import MainWindow
|
from portprotonqt.main_window import MainWindow
|
||||||
from portprotonqt.config_utils import (
|
from portprotonqt.config_utils import save_fullscreen_config
|
||||||
save_fullscreen_config,
|
|
||||||
read_fullscreen_config,
|
|
||||||
get_portproton_start_command
|
|
||||||
)
|
|
||||||
from portprotonqt.logger import get_logger, setup_logger
|
from portprotonqt.logger import get_logger, setup_logger
|
||||||
from portprotonqt.cli import parse_args
|
from portprotonqt.cli import parse_args
|
||||||
|
|
||||||
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
__app_id__ = "ru.linux_gaming.PortProtonQt"
|
||||||
__app_name__ = "PortProtonQt"
|
__app_name__ = "PortProtonQt"
|
||||||
__app_version__ = "0.1.9"
|
__app_version__ = "0.1.6"
|
||||||
|
|
||||||
def get_version():
|
|
||||||
try:
|
|
||||||
commit = subprocess.check_output(
|
|
||||||
["git", "rev-parse", "--short", "HEAD"],
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
).decode("utf-8").strip()
|
|
||||||
return f"{__app_version__} ({commit})"
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
||||||
return __app_version__
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ["PW_CLI"] = "1"
|
|
||||||
os.environ["PROCESS_LOG"] = "1"
|
|
||||||
os.environ["START_FROM_STEAM"] = "1"
|
|
||||||
|
|
||||||
# Get the PortProton start command
|
|
||||||
start_sh = get_portproton_start_command()
|
|
||||||
|
|
||||||
if start_sh is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
app.setWindowIcon(QIcon.fromTheme(__app_id__))
|
||||||
app.setDesktopFileName(__app_id__)
|
app.setDesktopFileName(__app_id__)
|
||||||
@@ -47,131 +19,40 @@ def main():
|
|||||||
app.setApplicationVersion(__app_version__)
|
app.setApplicationVersion(__app_version__)
|
||||||
|
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
|
||||||
|
# Setup logger with specified debug level
|
||||||
setup_logger(args.debug_level)
|
setup_logger(args.debug_level)
|
||||||
|
|
||||||
|
# Reinitialize logger after setup to ensure it uses the new configuration
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# --- Single-instance logic ---
|
|
||||||
server_name = __app_id__
|
|
||||||
socket = QLocalSocket()
|
|
||||||
socket.connectToServer(server_name)
|
|
||||||
|
|
||||||
if socket.waitForConnected(200):
|
|
||||||
# Второй экземпляр — передаём команду первому
|
|
||||||
fullscreen = args.fullscreen or read_fullscreen_config()
|
|
||||||
msg = b"show:fullscreen" if fullscreen else b"show"
|
|
||||||
socket.write(msg)
|
|
||||||
socket.flush()
|
|
||||||
socket.waitForBytesWritten(500)
|
|
||||||
socket.disconnectFromServer()
|
|
||||||
logger.info("Restored existing instance from tray")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Если старый сокет остался — удалить
|
|
||||||
QLocalServer.removeServer(server_name)
|
|
||||||
|
|
||||||
local_server = QLocalServer()
|
|
||||||
if not local_server.listen(server_name):
|
|
||||||
logger.warning(f"Failed to start local server: {local_server.errorString()}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# --- Qt translations ---
|
|
||||||
system_locale = QLocale.system()
|
system_locale = QLocale.system()
|
||||||
qt_translator = QTranslator()
|
qt_translator = QTranslator()
|
||||||
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
translations_path = QLibraryInfo.path(QLibraryInfo.LibraryPath.TranslationsPath)
|
||||||
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
if qt_translator.load(system_locale, "qtbase", "_", translations_path):
|
||||||
app.installTranslator(qt_translator)
|
app.installTranslator(qt_translator)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(f"Qt translations for {system_locale.name()} not found in {translations_path}, using english language")
|
||||||
f"Qt translations for {system_locale.name()} not found in {translations_path}, using English"
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- Main Window ---
|
window = MainWindow(app_name=__app_name__)
|
||||||
version = get_version()
|
|
||||||
window = MainWindow(app_name=__app_name__, version=version)
|
|
||||||
|
|
||||||
# --- Handle incoming connections ---
|
if args.fullscreen:
|
||||||
def handle_new_connection():
|
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
||||||
conn = local_server.nextPendingConnection()
|
|
||||||
if not conn:
|
|
||||||
return
|
|
||||||
|
|
||||||
if conn.waitForReadyRead(1000):
|
|
||||||
data = conn.readAll().data()
|
|
||||||
msg = bytes(data).decode("utf-8", errors="ignore")
|
|
||||||
logger.info(f"IPC message received: {msg}")
|
|
||||||
|
|
||||||
def restore_window():
|
|
||||||
try:
|
|
||||||
if msg.startswith("show"):
|
|
||||||
# Ensure the window is visible and not minimized
|
|
||||||
window.setWindowState(window.windowState() & ~Qt.WindowState.WindowMinimized)
|
|
||||||
window.show()
|
|
||||||
window.raise_()
|
|
||||||
window.activateWindow()
|
|
||||||
|
|
||||||
# Ensure window is in active state for systems with strict focus policies
|
|
||||||
window.setWindowState(window.windowState() | Qt.WindowState.WindowActive)
|
|
||||||
|
|
||||||
if ":fullscreen" in msg:
|
|
||||||
logger.info("Switching to fullscreen via IPC")
|
|
||||||
save_fullscreen_config(True)
|
|
||||||
window.showFullScreen()
|
|
||||||
else:
|
|
||||||
logger.info("Switching to normal window via IPC")
|
|
||||||
save_fullscreen_config(False)
|
|
||||||
window.showNormal()
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Failed to restore window: {e}")
|
|
||||||
|
|
||||||
# Выполняем в основном потоке
|
|
||||||
QTimer.singleShot(0, restore_window)
|
|
||||||
|
|
||||||
conn.disconnectFromServer()
|
|
||||||
|
|
||||||
local_server.newConnection.connect(handle_new_connection)
|
|
||||||
|
|
||||||
# --- Initial fullscreen state ---
|
|
||||||
launch_fullscreen = args.fullscreen or read_fullscreen_config()
|
|
||||||
if launch_fullscreen:
|
|
||||||
logger.info(
|
|
||||||
f"Launching in fullscreen mode ({'--fullscreen' if args.fullscreen else 'config'})"
|
|
||||||
)
|
|
||||||
save_fullscreen_config(True)
|
save_fullscreen_config(True)
|
||||||
window.showFullScreen()
|
window.showFullScreen()
|
||||||
else:
|
|
||||||
logger.info("Launching in normal mode")
|
|
||||||
save_fullscreen_config(False)
|
|
||||||
window.showNormal()
|
|
||||||
|
|
||||||
# Execute the initial PortProton command after the UI is set up
|
|
||||||
def run_initial_command():
|
|
||||||
nonlocal start_sh
|
|
||||||
if start_sh:
|
|
||||||
try:
|
|
||||||
subprocess.run(start_sh + ["cli", "--initial"], timeout=10)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logger.warning("Initial PortProton command timed out")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error running initial PortProton command: {e}")
|
|
||||||
else:
|
|
||||||
logger.warning("PortProton start command not available, skipping initial command")
|
|
||||||
|
|
||||||
# Run the initial command after the UI is displayed
|
|
||||||
QTimer.singleShot(100, run_initial_command)
|
|
||||||
|
|
||||||
# --- Cleanup ---
|
|
||||||
def cleanup_on_exit():
|
def cleanup_on_exit():
|
||||||
try:
|
nonlocal window
|
||||||
local_server.close()
|
app.aboutToQuit.disconnect()
|
||||||
QLocalServer.removeServer(server_name)
|
if window:
|
||||||
if window:
|
window.close()
|
||||||
window.close()
|
app.quit()
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Cleanup error: {e}")
|
|
||||||
|
|
||||||
app.aboutToQuit.connect(cleanup_on_exit)
|
app.aboutToQuit.connect(cleanup_on_exit)
|
||||||
|
|
||||||
|
window.show()
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
import configparser
|
import configparser
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
_portproton_location = None
|
_portproton_location = None
|
||||||
_portproton_start_sh = None
|
|
||||||
|
|
||||||
# Configuration cache for performance optimization
|
|
||||||
_config_cache = {}
|
|
||||||
_config_last_modified = {}
|
|
||||||
|
|
||||||
# Paths to configuration files
|
# Paths to configuration files
|
||||||
CONFIG_FILE = os.path.join(
|
CONFIG_FILE = os.path.join(
|
||||||
@@ -32,35 +26,13 @@ THEMES_DIRS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
||||||
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails.
|
"""Safely reads a configuration file and returns a ConfigParser object or None if reading fails."""
|
||||||
Uses caching to avoid repeated file reads for better performance.
|
cp = configparser.ConfigParser()
|
||||||
"""
|
|
||||||
# Check if file exists
|
|
||||||
if not os.path.exists(config_file):
|
if not os.path.exists(config_file):
|
||||||
logger.debug(f"Configuration file {config_file} not found")
|
logger.debug(f"Configuration file {config_file} not found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get file modification time
|
|
||||||
try:
|
|
||||||
current_mtime = os.path.getmtime(config_file)
|
|
||||||
except OSError:
|
|
||||||
logger.warning(f"Failed to get modification time for {config_file}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Check if we have a cached version that's still valid
|
|
||||||
if config_file in _config_cache and config_file in _config_last_modified:
|
|
||||||
if _config_last_modified[config_file] == current_mtime:
|
|
||||||
logger.debug(f"Using cached config for {config_file}")
|
|
||||||
return _config_cache[config_file]
|
|
||||||
|
|
||||||
# Read and parse the config file
|
|
||||||
cp = configparser.ConfigParser()
|
|
||||||
try:
|
try:
|
||||||
cp.read(config_file, encoding="utf-8")
|
cp.read(config_file, encoding="utf-8")
|
||||||
# Update cache
|
|
||||||
_config_cache[config_file] = cp
|
|
||||||
_config_last_modified[config_file] = current_mtime
|
|
||||||
logger.debug(f"Config file {config_file} loaded and cached")
|
|
||||||
return cp
|
return cp
|
||||||
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
|
except (configparser.DuplicateSectionError, configparser.DuplicateOptionError) as e:
|
||||||
logger.warning(f"Invalid configuration file format: {e}")
|
logger.warning(f"Invalid configuration file format: {e}")
|
||||||
@@ -69,14 +41,6 @@ def read_config_safely(config_file: str) -> configparser.ConfigParser | None:
|
|||||||
logger.warning(f"Failed to read configuration file: {e}")
|
logger.warning(f"Failed to read configuration file: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def invalidate_config_cache(config_file: str = CONFIG_FILE):
|
|
||||||
"""Invalidates the cached configuration for the specified file."""
|
|
||||||
if config_file in _config_cache:
|
|
||||||
del _config_cache[config_file]
|
|
||||||
if config_file in _config_last_modified:
|
|
||||||
del _config_last_modified[config_file]
|
|
||||||
logger.debug(f"Config cache invalidated for {config_file}")
|
|
||||||
|
|
||||||
def read_config():
|
def read_config():
|
||||||
"""Reads the configuration file and returns a dictionary of parameters.
|
"""Reads the configuration file and returns a dictionary of parameters.
|
||||||
Example line in config (no sections):
|
Example line in config (no sections):
|
||||||
@@ -111,8 +75,6 @@ def save_theme_to_config(theme_name):
|
|||||||
cp["Appearance"]["theme"] = theme_name
|
cp["Appearance"]["theme"] = theme_name
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_time_config():
|
def read_time_config():
|
||||||
"""Reads time settings from the [Time] section of the configuration file.
|
"""Reads time settings from the [Time] section of the configuration file.
|
||||||
@@ -132,29 +94,21 @@ def save_time_config(detail_level):
|
|||||||
cp["Time"]["detail_level"] = detail_level
|
cp["Time"]["detail_level"] = detail_level
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_file_content(file_path):
|
def read_file_content(file_path):
|
||||||
"""Reads the content of a file and returns it as a string."""
|
"""Reads the content of a file and returns it as a string."""
|
||||||
try:
|
with open(file_path, encoding="utf-8") as f:
|
||||||
# Add timeout protection for file operations using a simple approach
|
return f.read().strip()
|
||||||
with open(file_path, encoding="utf-8") as f:
|
|
||||||
content = f.read().strip()
|
|
||||||
return content
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error reading file {file_path}: {e}")
|
|
||||||
raise # Re-raise the exception to be handled by the caller
|
|
||||||
|
|
||||||
def get_portproton_location():
|
def get_portproton_location():
|
||||||
"""Возвращает путь к PortProton каталогу (строку) или None."""
|
"""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.
|
||||||
|
"""
|
||||||
global _portproton_location
|
global _portproton_location
|
||||||
|
|
||||||
if _portproton_location is not None:
|
if _portproton_location is not None:
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
|
|
||||||
location = None
|
|
||||||
|
|
||||||
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
if os.path.isfile(PORTPROTON_CONFIG_FILE):
|
||||||
try:
|
try:
|
||||||
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
location = read_file_content(PORTPROTON_CONFIG_FILE).strip()
|
||||||
@@ -162,69 +116,19 @@ def get_portproton_location():
|
|||||||
_portproton_location = location
|
_portproton_location = location
|
||||||
logger.info(f"PortProton path from configuration: {location}")
|
logger.info(f"PortProton path from configuration: {location}")
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
logger.warning(f"Invalid PortProton path in configuration: {location}, using defaults")
|
logger.warning(f"Invalid PortProton path in configuration: {location}, using default path")
|
||||||
except (OSError, PermissionError) as e:
|
except (OSError, PermissionError) as e:
|
||||||
logger.warning(f"Failed to read PortProton configuration file: {e}")
|
logger.warning(f"Failed to read PortProton configuration file: {e}, using default path")
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Unexpected error reading PortProton configuration file: {e}")
|
|
||||||
|
|
||||||
default_flatpak_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
default_dir = os.path.join(os.path.expanduser("~"), ".var", "app", "ru.linux_gaming.PortProton")
|
||||||
if os.path.isdir(default_flatpak_dir):
|
if os.path.isdir(default_dir):
|
||||||
_portproton_location = default_flatpak_dir
|
_portproton_location = default_dir
|
||||||
logger.info(f"Using Flatpak PortProton directory: {default_flatpak_dir}")
|
logger.info(f"Using flatpak PortProton directory: {default_dir}")
|
||||||
return _portproton_location
|
return _portproton_location
|
||||||
|
|
||||||
logger.warning("PortProton configuration and Flatpak directory not found")
|
logger.warning("PortProton configuration and flatpak directory not found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_portproton_start_command():
|
|
||||||
"""Возвращает список команд для запуска PortProton (start.sh или flatpak run)."""
|
|
||||||
portproton_path = get_portproton_location()
|
|
||||||
if not portproton_path:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Check if flatpak command exists before trying to run it
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
["flatpak", "--version"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
timeout=5
|
|
||||||
)
|
|
||||||
flatpak_available = True
|
|
||||||
except FileNotFoundError:
|
|
||||||
flatpak_available = False
|
|
||||||
except Exception:
|
|
||||||
flatpak_available = False
|
|
||||||
|
|
||||||
if flatpak_available:
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["flatpak", "list"],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
if "ru.linux_gaming.PortProton" in result.stdout:
|
|
||||||
logger.info("Detected Flatpak installation")
|
|
||||||
return ["flatpak", "run", "ru.linux_gaming.PortProton"]
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logger.warning("Flatpak list command timed out")
|
|
||||||
return None
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error checking flatpak list: {e}")
|
|
||||||
pass
|
|
||||||
|
|
||||||
start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
|
|
||||||
if os.path.exists(start_sh_path):
|
|
||||||
return [start_sh_path]
|
|
||||||
|
|
||||||
logger.warning("Neither flatpak nor start.sh found for PortProton")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_desktop_entry(file_path):
|
def parse_desktop_entry(file_path):
|
||||||
"""Reads and parses a .desktop file using configparser.
|
"""Reads and parses a .desktop file using configparser.
|
||||||
Returns None if the [Desktop Entry] section is missing.
|
Returns None if the [Desktop Entry] section is missing.
|
||||||
@@ -272,30 +176,6 @@ def save_card_size(card_width):
|
|||||||
cp["Cards"]["card_width"] = str(card_width)
|
cp["Cards"]["card_width"] = str(card_width)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_auto_card_size():
|
|
||||||
"""Reads the card size (width) for Auto Install from the [Cards] section.
|
|
||||||
Returns 250 if the parameter is not set.
|
|
||||||
"""
|
|
||||||
cp = read_config_safely(CONFIG_FILE)
|
|
||||||
if cp is None or not cp.has_section("Cards") or not cp.has_option("Cards", "auto_card_width"):
|
|
||||||
save_auto_card_size(250)
|
|
||||||
return 250
|
|
||||||
return cp.getint("Cards", "auto_card_width", fallback=250)
|
|
||||||
|
|
||||||
def save_auto_card_size(card_width):
|
|
||||||
"""Saves the card size (width) for Auto Install to the [Cards] section."""
|
|
||||||
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
|
||||||
if "Cards" not in cp:
|
|
||||||
cp["Cards"] = {}
|
|
||||||
cp["Cards"]["auto_card_width"] = str(card_width)
|
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
|
||||||
cp.write(configfile)
|
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
|
|
||||||
def read_sort_method():
|
def read_sort_method():
|
||||||
"""Reads the sort method from the [Games] section.
|
"""Reads the sort method from the [Games] section.
|
||||||
@@ -315,8 +195,6 @@ def save_sort_method(sort_method):
|
|||||||
cp["Games"]["sort_method"] = sort_method
|
cp["Games"]["sort_method"] = sort_method
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_display_filter():
|
def read_display_filter():
|
||||||
"""Reads the display_filter parameter from the [Games] section.
|
"""Reads the display_filter parameter from the [Games] section.
|
||||||
@@ -336,8 +214,6 @@ def save_display_filter(filter_value):
|
|||||||
cp["Games"]["display_filter"] = filter_value
|
cp["Games"]["display_filter"] = filter_value
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_favorites():
|
def read_favorites():
|
||||||
"""Reads the list of favorite games from the [Favorites] section.
|
"""Reads the list of favorite games from the [Favorites] section.
|
||||||
@@ -363,8 +239,6 @@ def save_favorites(favorites):
|
|||||||
cp["Favorites"]["games"] = f'"{fav_str}"'
|
cp["Favorites"]["games"] = f'"{fav_str}"'
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_rumble_config():
|
def read_rumble_config():
|
||||||
"""Reads the gamepad rumble setting from the [Gamepad] section.
|
"""Reads the gamepad rumble setting from the [Gamepad] section.
|
||||||
@@ -384,29 +258,6 @@ def save_rumble_config(rumble_enabled):
|
|||||||
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
|
cp["Gamepad"]["rumble_enabled"] = str(rumble_enabled)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_gamepad_type():
|
|
||||||
"""Reads the gamepad type from the [Gamepad] section.
|
|
||||||
Returns 'xbox' if the parameter is missing.
|
|
||||||
"""
|
|
||||||
cp = read_config_safely(CONFIG_FILE)
|
|
||||||
if cp is None or not cp.has_section("Gamepad") or not cp.has_option("Gamepad", "type"):
|
|
||||||
save_gamepad_type("xbox")
|
|
||||||
return "xbox"
|
|
||||||
return cp.get("Gamepad", "type", fallback="xbox").lower()
|
|
||||||
|
|
||||||
def save_gamepad_type(gpad_type):
|
|
||||||
"""Saves the gamepad type to the [Gamepad] section."""
|
|
||||||
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
|
||||||
if "Gamepad" not in cp:
|
|
||||||
cp["Gamepad"] = {}
|
|
||||||
cp["Gamepad"]["type"] = gpad_type
|
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
|
||||||
cp.write(configfile)
|
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def ensure_default_proxy_config():
|
def ensure_default_proxy_config():
|
||||||
"""Ensures the [Proxy] section exists in the configuration file.
|
"""Ensures the [Proxy] section exists in the configuration file.
|
||||||
@@ -451,8 +302,6 @@ def save_proxy_config(proxy_url="", proxy_user="", proxy_password=""):
|
|||||||
cp["Proxy"]["proxy_password"] = proxy_password
|
cp["Proxy"]["proxy_password"] = proxy_password
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_fullscreen_config():
|
def read_fullscreen_config():
|
||||||
"""Reads the fullscreen mode setting from the [Display] section.
|
"""Reads the fullscreen mode setting from the [Display] section.
|
||||||
@@ -472,8 +321,6 @@ def save_fullscreen_config(fullscreen):
|
|||||||
cp["Display"]["fullscreen"] = str(fullscreen)
|
cp["Display"]["fullscreen"] = str(fullscreen)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_window_geometry() -> tuple[int, int]:
|
def read_window_geometry() -> tuple[int, int]:
|
||||||
"""Reads the window width and height from the [MainWindow] section.
|
"""Reads the window width and height from the [MainWindow] section.
|
||||||
@@ -495,8 +342,6 @@ def save_window_geometry(width: int, height: int):
|
|||||||
cp["MainWindow"]["height"] = str(height)
|
cp["MainWindow"]["height"] = str(height)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def reset_config():
|
def reset_config():
|
||||||
"""Resets the configuration file by deleting it.
|
"""Resets the configuration file by deleting it.
|
||||||
@@ -506,8 +351,6 @@ def reset_config():
|
|||||||
try:
|
try:
|
||||||
os.remove(CONFIG_FILE)
|
os.remove(CONFIG_FILE)
|
||||||
logger.info("Configuration file %s deleted", CONFIG_FILE)
|
logger.info("Configuration file %s deleted", CONFIG_FILE)
|
||||||
# Invalidate cache after deletion
|
|
||||||
invalidate_config_cache()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete configuration file: {e}")
|
logger.warning(f"Failed to delete configuration file: {e}")
|
||||||
|
|
||||||
@@ -522,9 +365,6 @@ def clear_cache():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete cache: {e}")
|
logger.warning(f"Failed to delete cache: {e}")
|
||||||
|
|
||||||
# Also clear our internal config cache
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_auto_fullscreen_gamepad():
|
def read_auto_fullscreen_gamepad():
|
||||||
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
|
"""Reads the auto-fullscreen setting for gamepad from the [Display] section.
|
||||||
Returns False if the parameter is missing.
|
Returns False if the parameter is missing.
|
||||||
@@ -543,8 +383,6 @@ def save_auto_fullscreen_gamepad(auto_fullscreen):
|
|||||||
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
cp["Display"]["auto_fullscreen_gamepad"] = str(auto_fullscreen)
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_favorite_folders():
|
def read_favorite_folders():
|
||||||
"""Reads the list of favorite folders from the [FavoritesFolders] section.
|
"""Reads the list of favorite folders from the [FavoritesFolders] section.
|
||||||
@@ -570,26 +408,3 @@ def save_favorite_folders(folders):
|
|||||||
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
cp["FavoritesFolders"]["folders"] = f'"{fav_str}"'
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
||||||
cp.write(configfile)
|
cp.write(configfile)
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|
||||||
def read_minimize_to_tray():
|
|
||||||
"""Reads the minimize-to-tray setting from the [Display] section.
|
|
||||||
Returns True if the parameter is missing (default: minimize to tray).
|
|
||||||
"""
|
|
||||||
cp = read_config_safely(CONFIG_FILE)
|
|
||||||
if cp is None or not cp.has_section("Display") or not cp.has_option("Display", "minimize_to_tray"):
|
|
||||||
save_minimize_to_tray(True)
|
|
||||||
return True
|
|
||||||
return cp.getboolean("Display", "minimize_to_tray", fallback=True)
|
|
||||||
|
|
||||||
def save_minimize_to_tray(minimize_to_tray):
|
|
||||||
"""Saves the minimize-to-tray setting to the [Display] section."""
|
|
||||||
cp = read_config_safely(CONFIG_FILE) or configparser.ConfigParser()
|
|
||||||
if "Display" not in cp:
|
|
||||||
cp["Display"] = {}
|
|
||||||
cp["Display"]["minimize_to_tray"] = str(minimize_to_tray)
|
|
||||||
with open(CONFIG_FILE, "w", encoding="utf-8") as configfile:
|
|
||||||
cp.write(configfile)
|
|
||||||
# Invalidate cache after saving
|
|
||||||
invalidate_config_cache()
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QLineEdit, QApplicati
|
|||||||
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
||||||
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
from PySide6.QtGui import QDesktopServices, QIcon, QKeySequence
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders, get_portproton_start_command
|
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites, read_favorite_folders, save_favorite_folders
|
||||||
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
||||||
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
|
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable, remove_egs_from_steam
|
||||||
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
from portprotonqt.dialogs import AddGameDialog, FileExplorer, generate_thumbnail
|
||||||
@@ -406,7 +406,16 @@ class ContextMenuManager:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
# Construct EGS launch command
|
# Construct EGS launch command
|
||||||
wrapper = get_portproton_start_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
|
||||||
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
|
exec_line = f'"{self.legendary_path}" launch {game_card.appid} --no-wine --wrapper "env START_FROM_STEAM=1 {wrapper}"'
|
||||||
else:
|
else:
|
||||||
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
|
exec_line = self._get_exec_line(game_card.name, game_card.exec_line)
|
||||||
@@ -1035,15 +1044,7 @@ Icon={icon_path}
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if new_cover_path is a URL by checking for common image extensions
|
if os.path.isfile(new_cover_path):
|
||||||
image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp')
|
|
||||||
has_image_extension = any(new_cover_path.lower().endswith(ext) for ext in image_extensions)
|
|
||||||
|
|
||||||
# Consider it a URL if it has image extension and is not a local file
|
|
||||||
is_url = has_image_extension and not os.path.isfile(new_cover_path)
|
|
||||||
|
|
||||||
# Use the downloaded file path if we have a URL and the file was downloaded, otherwise use the local file
|
|
||||||
if os.path.isfile(new_cover_path) or (is_url and dialog.last_cover_path and os.path.isfile(dialog.last_cover_path)):
|
|
||||||
exe_name = os.path.splitext(os.path.basename(new_exe_path))[0]
|
exe_name = os.path.splitext(os.path.basename(new_exe_path))[0]
|
||||||
xdg_data_home = os.getenv(
|
xdg_data_home = os.getenv(
|
||||||
"XDG_DATA_HOME",
|
"XDG_DATA_HOME",
|
||||||
@@ -1051,25 +1052,16 @@ Icon={icon_path}
|
|||||||
)
|
)
|
||||||
custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name)
|
custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name)
|
||||||
os.makedirs(custom_folder, exist_ok=True)
|
os.makedirs(custom_folder, exist_ok=True)
|
||||||
|
ext = os.path.splitext(new_cover_path)[1].lower()
|
||||||
# Use the actual cover file path (either from URL download or local file)
|
|
||||||
cover_to_copy = dialog.last_cover_path if is_url and dialog.last_cover_path and os.path.isfile(dialog.last_cover_path) else new_cover_path
|
|
||||||
ext = os.path.splitext(cover_to_copy)[1].lower()
|
|
||||||
if ext in [".png", ".jpg", ".jpeg", ".bmp"]:
|
if ext in [".png", ".jpg", ".jpeg", ".bmp"]:
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(cover_to_copy, os.path.join(custom_folder, f"cover{ext}"))
|
shutil.copyfile(new_cover_path, os.path.join(custom_folder, f"cover{ext}"))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.signals.show_warning_dialog.emit(
|
self.signals.show_warning_dialog.emit(
|
||||||
_("Error"),
|
_("Error"),
|
||||||
_("Failed to copy cover image: {error}").format(error=str(e))
|
_("Failed to copy cover image: {error}").format(error=str(e))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
|
||||||
self.signals.show_warning_dialog.emit(
|
|
||||||
_("Error"),
|
|
||||||
_("Unsupported image format: {extension}").format(extension=ext)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
def add_to_steam(self, game_name, exec_line, cover_path):
|
def add_to_steam(self, game_name, exec_line, cover_path):
|
||||||
"""
|
"""
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 978 KiB After Width: | Height: | Size: 978 KiB |
|
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 634 KiB |
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 391 KiB |
|
Before Width: | Height: | Size: 710 KiB After Width: | Height: | Size: 710 KiB |
|
Before Width: | Height: | Size: 627 KiB After Width: | Height: | Size: 627 KiB |
@@ -126,21 +126,7 @@ class FlowLayout(QLayout):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def heightForWidth(self, width):
|
def heightForWidth(self, width):
|
||||||
# Аналогично фильтруем видимые для тестового расчёта высоты
|
return self.doLayout(QRect(0, 0, width, 0), True)
|
||||||
visible_items = []
|
|
||||||
nat_sizes = np.empty((0, 2), dtype=np.int32)
|
|
||||||
for item in self.itemList:
|
|
||||||
if item.widget() and item.widget().isVisible():
|
|
||||||
visible_items.append(item)
|
|
||||||
s = item.sizeHint()
|
|
||||||
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
|
|
||||||
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
|
|
||||||
|
|
||||||
if len(visible_items) == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
_, total_height = compute_layout(nat_sizes, width, self._spacing, self._max_scale)
|
|
||||||
return total_height
|
|
||||||
|
|
||||||
def setGeometry(self, rect):
|
def setGeometry(self, rect):
|
||||||
super().setGeometry(rect)
|
super().setGeometry(rect)
|
||||||
@@ -159,46 +145,26 @@ class FlowLayout(QLayout):
|
|||||||
return size
|
return size
|
||||||
|
|
||||||
def doLayout(self, rect, testOnly):
|
def doLayout(self, rect, testOnly):
|
||||||
N_total = len(self.itemList)
|
N = len(self.itemList)
|
||||||
if N_total == 0:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Фильтруем только видимые элементы
|
|
||||||
visible_items = []
|
|
||||||
visible_indices = [] # Индексы в оригинальном itemList для установки геометрии
|
|
||||||
nat_sizes = np.empty((0, 2), dtype=np.int32)
|
|
||||||
for i, item in enumerate(self.itemList):
|
|
||||||
if item.widget() and item.widget().isVisible():
|
|
||||||
visible_items.append(item)
|
|
||||||
visible_indices.append(i)
|
|
||||||
s = item.sizeHint()
|
|
||||||
new_row = np.array([[s.width(), s.height()]], dtype=np.int32)
|
|
||||||
nat_sizes = np.vstack([nat_sizes, new_row]) if len(nat_sizes) > 0 else new_row
|
|
||||||
|
|
||||||
N = len(visible_items)
|
|
||||||
if N == 0:
|
if N == 0:
|
||||||
# Если все скрыты, устанавливаем нулевые геометрии для всех
|
|
||||||
if not testOnly:
|
|
||||||
for item in self.itemList:
|
|
||||||
item.setGeometry(QRect())
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
nat_sizes = np.empty((N, 2), dtype=np.int32)
|
||||||
|
for i, item in enumerate(self.itemList):
|
||||||
|
s = item.sizeHint()
|
||||||
|
nat_sizes[i, 0] = s.width()
|
||||||
|
nat_sizes[i, 1] = s.height()
|
||||||
|
|
||||||
geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale)
|
geom_array, total_height = compute_layout(nat_sizes, rect.width(), self._spacing, self._max_scale)
|
||||||
|
|
||||||
if not testOnly:
|
if not testOnly:
|
||||||
# Устанавливаем геометрии только для видимых
|
for i, item in enumerate(self.itemList):
|
||||||
for idx, (_vis_idx, item) in enumerate(zip(visible_indices, visible_items, strict=True)):
|
x = geom_array[i, 0] + rect.x()
|
||||||
x = geom_array[idx, 0] + rect.x()
|
y = geom_array[i, 1] + rect.y()
|
||||||
y = geom_array[idx, 1] + rect.y()
|
w = geom_array[i, 2]
|
||||||
w = geom_array[idx, 2]
|
h = geom_array[i, 3]
|
||||||
h = geom_array[idx, 3]
|
|
||||||
item.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
|
item.setGeometry(QRect(QPoint(x, y), QSize(w, h)))
|
||||||
|
|
||||||
# Для невидимых — нулевая геометрия
|
|
||||||
for i in range(N_total):
|
|
||||||
if i not in visible_indices:
|
|
||||||
self.itemList[i].setGeometry(QRect())
|
|
||||||
|
|
||||||
return total_height
|
return total_height
|
||||||
|
|
||||||
class ClickableLabel(QLabel):
|
class ClickableLabel(QLabel):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from portprotonqt.localization import get_egs_language, _
|
|||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.image_utils import load_pixmap_async
|
from portprotonqt.image_utils import load_pixmap_async
|
||||||
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
|
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
|
||||||
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
|
from portprotonqt.config_utils import get_portproton_location
|
||||||
from portprotonqt.steam_api import (
|
from portprotonqt.steam_api import (
|
||||||
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
|
get_weanticheatyet_status_async, get_steam_apps_and_index_async, get_protondb_tier_async,
|
||||||
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
|
search_app, get_steam_home, get_last_steam_user, convert_steam_id, generate_thumbnail, call_steam_api
|
||||||
@@ -254,7 +254,14 @@ def add_egs_to_steam(app_name: str, game_title: str, legendary_path: str, callba
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Determine wrapper
|
# Determine wrapper
|
||||||
wrapper = get_portproton_start_command()
|
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
|
||||||
|
|
||||||
# Create launch script
|
# Create launch script
|
||||||
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
|
steam_scripts_dir = os.path.join(portproton_dir, "steam_scripts")
|
||||||
@@ -577,7 +584,7 @@ def get_egs_game_description_async(
|
|||||||
"https://launcher.store.epicgames.com/graphql",
|
"https://launcher.store.epicgames.com/graphql",
|
||||||
json=search_query,
|
json=search_query,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=10
|
timeout=5
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
@@ -597,7 +604,7 @@ def get_egs_game_description_async(
|
|||||||
def fetch_legacy_description(url: str) -> str:
|
def fetch_legacy_description(url: str) -> str:
|
||||||
"""Fetches description from the legacy API, handling DNS failures."""
|
"""Fetches description from the legacy API, handling DNS failures."""
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=headers, timeout=10)
|
response = requests.get(url, headers=headers, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
@@ -619,9 +626,6 @@ def get_egs_game_description_async(
|
|||||||
except requests.exceptions.ConnectionError as e:
|
except requests.exceptions.ConnectionError as e:
|
||||||
logger.error("DNS resolution failed for legacy API %s: %s", url, str(e))
|
logger.error("DNS resolution failed for legacy API %s: %s", url, str(e))
|
||||||
return ""
|
return ""
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
logger.warning("Request timeout for legacy API %s", url)
|
|
||||||
return ""
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.warning("Failed to fetch legacy API for %s: %s", app_name, str(e))
|
logger.warning("Failed to fetch legacy API for %s: %s", app_name, str(e))
|
||||||
return ""
|
return ""
|
||||||
@@ -673,7 +677,7 @@ def get_egs_game_description_async(
|
|||||||
url = "https://graphql.epicgames.com/graphql"
|
url = "https://graphql.epicgames.com/graphql"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=search_query, headers=headers, timeout=10)
|
response = requests.post(url, json=search_query, headers=headers, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = orjson.loads(response.content)
|
data = orjson.loads(response.content)
|
||||||
if namespace:
|
if namespace:
|
||||||
@@ -692,9 +696,6 @@ def get_egs_game_description_async(
|
|||||||
for substring in ["bundle", "pack", "edition", "dlc", "upgrade", "chapter", "набор", "пак", "дополнение"])):
|
for substring in ["bundle", "pack", "edition", "dlc", "upgrade", "chapter", "набор", "пак", "дополнение"])):
|
||||||
return element.get("description", ""), element.get("productSlug", "")
|
return element.get("description", ""), element.get("productSlug", "")
|
||||||
return "", ""
|
return "", ""
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
logger.warning("GraphQL request timeout for %s with locale %s", app_name, locale)
|
|
||||||
return "", ""
|
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
logger.warning("Failed to fetch GraphQL data for %s with locale %s: %s", app_name, locale, str(e))
|
logger.warning("Failed to fetch GraphQL data for %s with locale %s: %s", app_name, locale, str(e))
|
||||||
return "", ""
|
return "", ""
|
||||||
@@ -723,10 +724,6 @@ def get_egs_game_description_async(
|
|||||||
logger.debug("Fetched description from legacy API for %s: %s", app_name, (description[:100] + "...") if len(description) > 100 else description)
|
logger.debug("Fetched description from legacy API for %s: %s", app_name, (description[:100] + "...") if len(description) > 100 else description)
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
logger.error("Skipping legacy API due to DNS resolution failure for %s", app_name)
|
logger.error("Skipping legacy API due to DNS resolution failure for %s", app_name)
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
logger.warning("Legacy API request timed out for %s", app_name)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Unexpected error fetching legacy API for %s: %s", app_name, str(e))
|
|
||||||
|
|
||||||
# Step 3: If still no description and no namespace, try GraphQL with title
|
# Step 3: If still no description and no namespace, try GraphQL with title
|
||||||
if not description and not namespace:
|
if not description and not namespace:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
from PySide6.QtGui import QPainter, QColor, QDesktopServices
|
||||||
from PySide6.QtCore import Signal, Property, Qt, QUrl, QTimer
|
from PySide6.QtCore import Signal, Property, Qt, QUrl
|
||||||
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
from PySide6.QtWidgets import QFrame, QGraphicsDropShadowEffect, QVBoxLayout, QWidget, QStackedLayout, QLabel
|
||||||
|
from collections.abc import Callable
|
||||||
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
from portprotonqt.image_utils import load_pixmap_async, round_corners
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter, read_theme_from_config
|
from portprotonqt.config_utils import read_favorites, save_favorites, read_display_filter, read_theme_from_config
|
||||||
@@ -9,6 +10,7 @@ from portprotonqt.custom_widgets import ClickableLabel
|
|||||||
from portprotonqt.portproton_api import PortProtonAPI
|
from portprotonqt.portproton_api import PortProtonAPI
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.animations import GameCardAnimations
|
from portprotonqt.animations import GameCardAnimations
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
class GameCard(QFrame):
|
class GameCard(QFrame):
|
||||||
borderWidthChanged = Signal()
|
borderWidthChanged = Signal()
|
||||||
@@ -99,7 +101,7 @@ class GameCard(QFrame):
|
|||||||
|
|
||||||
self.favoriteLabel = ClickableLabel(self.coverWidget)
|
self.favoriteLabel = ClickableLabel(self.coverWidget)
|
||||||
self.favoriteLabel.clicked.connect(self.toggle_favorite)
|
self.favoriteLabel.clicked.connect(self.toggle_favorite)
|
||||||
self.is_favorite = self.name in set(read_favorites())
|
self.is_favorite = self.name in read_favorites()
|
||||||
self.update_favorite_icon()
|
self.update_favorite_icon()
|
||||||
self.favoriteLabel.raise_()
|
self.favoriteLabel.raise_()
|
||||||
|
|
||||||
@@ -200,27 +202,13 @@ class GameCard(QFrame):
|
|||||||
self.update_cover_pixmap()
|
self.update_cover_pixmap()
|
||||||
|
|
||||||
def update_cover_pixmap(self):
|
def update_cover_pixmap(self):
|
||||||
# Check if the coverLabel still exists before trying to update it
|
if self.base_pixmap:
|
||||||
# This prevents the "Internal C++ object already deleted" error when
|
|
||||||
# the widget has been destroyed but the async callback still executes
|
|
||||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.base_pixmap and not self.base_pixmap.isNull():
|
|
||||||
scaled_width = int(self.base_card_width * self._scale)
|
scaled_width = int(self.base_card_width * self._scale)
|
||||||
scaled_pixmap = self.base_pixmap.scaled(scaled_width, int(scaled_width * 1.5), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
scaled_pixmap = self.base_pixmap.scaled(scaled_width, int(scaled_width * 1.5), Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||||
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
|
rounded_pixmap = round_corners(scaled_pixmap, int(15 * self._scale))
|
||||||
try:
|
self.coverLabel.setPixmap(rounded_pixmap)
|
||||||
self.coverLabel.setPixmap(rounded_pixmap)
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted between the check and the call
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _position_badges(self, current_width):
|
def _position_badges(self, current_width):
|
||||||
# Check if the card has been destroyed before updating
|
|
||||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
right_margin = int(8 * self._scale)
|
right_margin = int(8 * self._scale)
|
||||||
badge_spacing = int(current_width * 0.02)
|
badge_spacing = int(current_width * 0.02)
|
||||||
top_y = int(10 * self._scale)
|
top_y = int(10 * self._scale)
|
||||||
@@ -239,28 +227,16 @@ class GameCard(QFrame):
|
|||||||
if is_visible:
|
if is_visible:
|
||||||
badge_x = current_width - badge_width - right_margin
|
badge_x = current_width - badge_width - right_margin
|
||||||
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
badge_y = badge_y_positions[-1] + badge_spacing if badge_y_positions else top_y
|
||||||
try:
|
badge.move(int(badge_x), int(badge_y))
|
||||||
badge.move(int(badge_x), int(badge_y))
|
badge_y_positions.append(badge_y + badge.height())
|
||||||
badge_y_positions.append(badge_y + badge.height())
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
self.anticheatLabel.raise_()
|
||||||
self.anticheatLabel.raise_()
|
self.protondbLabel.raise_()
|
||||||
self.protondbLabel.raise_()
|
self.portprotonLabel.raise_()
|
||||||
self.portprotonLabel.raise_()
|
self.egsLabel.raise_()
|
||||||
self.egsLabel.raise_()
|
self.steamLabel.raise_()
|
||||||
self.steamLabel.raise_()
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update_scale(self):
|
def update_scale(self):
|
||||||
# Check if the card has been destroyed before updating
|
|
||||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
scaled_width = int(self.base_card_width * self._scale)
|
scaled_width = int(self.base_card_width * self._scale)
|
||||||
scaled_height = int(self.base_card_width * 1.8 * self._scale)
|
scaled_height = int(self.base_card_width * 1.8 * self._scale)
|
||||||
scaled_extra = int(self.base_extra_margin * self._scale)
|
scaled_extra = int(self.base_extra_margin * self._scale)
|
||||||
@@ -281,53 +257,33 @@ class GameCard(QFrame):
|
|||||||
icon_space = int(scaled_width * 0.012)
|
icon_space = int(scaled_width * 0.012)
|
||||||
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
|
for label in [self.steamLabel, self.egsLabel, self.portprotonLabel, self.protondbLabel, self.anticheatLabel]:
|
||||||
if label is not None:
|
if label is not None:
|
||||||
try:
|
label.setFixedWidth(badge_width)
|
||||||
label.setFixedWidth(badge_width)
|
label.setIconSize(icon_size, icon_space)
|
||||||
label.setIconSize(icon_size, icon_space)
|
label.setCardWidth(scaled_width)
|
||||||
label.setCardWidth(scaled_width)
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._position_badges(scaled_width)
|
self._position_badges(scaled_width)
|
||||||
|
|
||||||
if self.base_font_size is not None:
|
if self.base_font_size is not None:
|
||||||
try:
|
font = self.nameLabel.font()
|
||||||
font = self.nameLabel.font()
|
new_font_size = self.base_font_size * self._scale
|
||||||
new_font_size = self.base_font_size * self._scale
|
if new_font_size > 0:
|
||||||
if new_font_size > 0:
|
font.setPointSizeF(new_font_size)
|
||||||
font.setPointSizeF(new_font_size)
|
self.nameLabel.setFont(font)
|
||||||
self.nameLabel.setFont(font)
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
self.shadow.setBlurRadius(int(20 * self._scale))
|
||||||
self.shadow.setBlurRadius(int(20 * self._scale))
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
self.updateGeometry()
|
||||||
self.updateGeometry()
|
self.update()
|
||||||
self.update()
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Ensure parent layout is updated safely
|
# Ensure parent layout is updated safely
|
||||||
try:
|
parent = self.parentWidget()
|
||||||
parent = self.parentWidget()
|
if parent:
|
||||||
if parent:
|
layout = parent.layout()
|
||||||
layout = parent.layout()
|
if layout:
|
||||||
if layout:
|
layout.invalidate()
|
||||||
layout.invalidate()
|
layout.activate()
|
||||||
layout.activate()
|
layout.update()
|
||||||
layout.update()
|
parent.updateGeometry()
|
||||||
parent.updateGeometry()
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update_card_size(self, new_width: int):
|
def update_card_size(self, new_width: int):
|
||||||
self.base_card_width = new_width
|
self.base_card_width = new_width
|
||||||
@@ -335,10 +291,6 @@ class GameCard(QFrame):
|
|||||||
self.update_scale()
|
self.update_scale()
|
||||||
|
|
||||||
def update_badge_visibility(self, display_filter: str):
|
def update_badge_visibility(self, display_filter: str):
|
||||||
# Check if the card has been destroyed before updating
|
|
||||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.display_filter = display_filter
|
self.display_filter = display_filter
|
||||||
self.steam_visible = (str(self.game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
self.steam_visible = (str(self.game_source).lower() == "steam" and self.display_filter in ("all", "favorites"))
|
||||||
self.egs_visible = (str(self.game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
self.egs_visible = (str(self.game_source).lower() == "epic" and self.display_filter in ("all", "favorites"))
|
||||||
@@ -346,15 +298,11 @@ class GameCard(QFrame):
|
|||||||
protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
|
protondb_visible = bool(self.getProtonDBText(self.protondb_tier))
|
||||||
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
|
anticheat_visible = bool(self.getAntiCheatText(self.anticheat_status))
|
||||||
|
|
||||||
try:
|
self.steamLabel.setVisible(self.steam_visible)
|
||||||
self.steamLabel.setVisible(self.steam_visible)
|
self.egsLabel.setVisible(self.egs_visible)
|
||||||
self.egsLabel.setVisible(self.egs_visible)
|
self.portprotonLabel.setVisible(self.portproton_visible)
|
||||||
self.portprotonLabel.setVisible(self.portproton_visible)
|
self.protondbLabel.setVisible(protondb_visible)
|
||||||
self.protondbLabel.setVisible(protondb_visible)
|
self.anticheatLabel.setVisible(anticheat_visible)
|
||||||
self.anticheatLabel.setVisible(anticheat_visible)
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
return
|
|
||||||
|
|
||||||
scaled_width = int(self.base_card_width * self._scale)
|
scaled_width = int(self.base_card_width * self._scale)
|
||||||
self._position_badges(scaled_width)
|
self._position_badges(scaled_width)
|
||||||
@@ -449,43 +397,20 @@ class GameCard(QFrame):
|
|||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
def update_favorite_icon(self):
|
def update_favorite_icon(self):
|
||||||
# Check if the card has been destroyed before updating
|
if self.is_favorite:
|
||||||
if not hasattr(self, 'coverLabel') or self.coverLabel is None:
|
self.favoriteLabel.setText("★")
|
||||||
return
|
else:
|
||||||
|
self.favoriteLabel.setText("☆")
|
||||||
try:
|
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
||||||
if self.is_favorite:
|
|
||||||
self.favoriteLabel.setText("★")
|
|
||||||
else:
|
|
||||||
self.favoriteLabel.setText("☆")
|
|
||||||
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
parent = self.parent()
|
|
||||||
while parent:
|
|
||||||
if hasattr(parent, 'game_library_manager'):
|
|
||||||
# Access using getattr with default to avoid Ruff B009 warning
|
|
||||||
manager = getattr(parent, 'game_library_manager', None)
|
|
||||||
if manager is not None:
|
|
||||||
QTimer.singleShot(0, manager.update_game_grid)
|
|
||||||
break
|
|
||||||
parent = parent.parent()
|
|
||||||
except RuntimeError:
|
|
||||||
# Handle the case where the Qt object was deleted
|
|
||||||
pass
|
|
||||||
|
|
||||||
def toggle_favorite(self):
|
def toggle_favorite(self):
|
||||||
favorites = read_favorites()
|
favorites = read_favorites()
|
||||||
favorites_set = set(favorites)
|
|
||||||
if self.is_favorite:
|
if self.is_favorite:
|
||||||
if self.name in favorites_set:
|
if self.name in favorites:
|
||||||
favorites.remove(self.name)
|
favorites.remove(self.name)
|
||||||
self.is_favorite = False
|
self.is_favorite = False
|
||||||
else:
|
else:
|
||||||
if self.name not in favorites_set:
|
if self.name not in favorites:
|
||||||
favorites.append(self.name)
|
favorites.append(self.name)
|
||||||
self.is_favorite = True
|
self.is_favorite = True
|
||||||
save_favorites(favorites)
|
save_favorites(favorites)
|
||||||
@@ -518,10 +443,9 @@ class GameCard(QFrame):
|
|||||||
self.update_scale()
|
self.update_scale()
|
||||||
self.scaleChanged.emit()
|
self.scaleChanged.emit()
|
||||||
|
|
||||||
borderWidth = Property(int, getBorderWidth, setBorderWidth, None, "", notify=borderWidthChanged)
|
borderWidth = Property(int, getBorderWidth, setBorderWidth, None, "", notify=cast(Callable[[], None], borderWidthChanged))
|
||||||
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=gradientAngleChanged)
|
gradientAngle = Property(float, getGradientAngle, setGradientAngle, None, "", notify=cast(Callable[[], None], gradientAngleChanged))
|
||||||
scale = Property(float, getScale, setScale, None, "", notify=scaleChanged)
|
scale = Property(float, getScale, setScale, None, "", notify=cast(Callable[[], None], scaleChanged))
|
||||||
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
def paintEvent(self, event):
|
||||||
super().paintEvent(event)
|
super().paintEvent(event)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
from portprotonqt.game_card import GameCard
|
from portprotonqt.game_card import GameCard
|
||||||
from portprotonqt.search_utils import SearchOptimizer, ThreadedSearch
|
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea, QSlider, QScroller
|
||||||
from PySide6.QtCore import Qt, QTimer
|
from PySide6.QtCore import Qt, QTimer
|
||||||
from portprotonqt.custom_widgets import FlowLayout
|
from portprotonqt.custom_widgets import FlowLayout
|
||||||
@@ -34,10 +33,8 @@ class MainWindowProtocol(Protocol):
|
|||||||
# Required attributes
|
# Required attributes
|
||||||
searchEdit: CustomLineEdit
|
searchEdit: CustomLineEdit
|
||||||
_last_card_width: int
|
_last_card_width: int
|
||||||
card_width: int
|
|
||||||
current_hovered_card: GameCard | None
|
current_hovered_card: GameCard | None
|
||||||
current_focused_card: GameCard | None
|
current_focused_card: GameCard | None
|
||||||
gamesListWidget: QWidget | None
|
|
||||||
|
|
||||||
class GameLibraryManager:
|
class GameLibraryManager:
|
||||||
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
|
def __init__(self, main_window: MainWindowProtocol, theme, context_menu_manager: ContextMenuManager | None):
|
||||||
@@ -57,9 +54,6 @@ class GameLibraryManager:
|
|||||||
self.pending_deletions = deque()
|
self.pending_deletions = deque()
|
||||||
self.is_filtering = False
|
self.is_filtering = False
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
# Initialize search optimizer
|
|
||||||
self.search_optimizer = SearchOptimizer()
|
|
||||||
self.search_thread: ThreadedSearch | None = None
|
|
||||||
|
|
||||||
def create_games_library_widget(self):
|
def create_games_library_widget(self):
|
||||||
"""Creates the games library widget with search, grid, and slider."""
|
"""Creates the games library widget with search, grid, and slider."""
|
||||||
@@ -133,8 +127,6 @@ class GameLibraryManager:
|
|||||||
self.card_width = self.sizeSlider.value()
|
self.card_width = self.sizeSlider.value()
|
||||||
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
self.sizeSlider.setToolTip(f"{self.card_width} px")
|
||||||
save_card_size(self.card_width)
|
save_card_size(self.card_width)
|
||||||
self.main_window.card_width = self.card_width
|
|
||||||
self.main_window._last_card_width = self.card_width
|
|
||||||
for card in self.game_card_cache.values():
|
for card in self.game_card_cache.values():
|
||||||
card.update_card_size(self.card_width)
|
card.update_card_size(self.card_width)
|
||||||
self.update_game_grid()
|
self.update_game_grid()
|
||||||
@@ -167,18 +159,12 @@ class GameLibraryManager:
|
|||||||
|
|
||||||
if is_focused:
|
if is_focused:
|
||||||
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
|
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
|
||||||
try:
|
self.main_window.current_hovered_card._hovered = False
|
||||||
self.main_window.current_hovered_card._hovered = False
|
self.main_window.current_hovered_card.leaveEvent(None)
|
||||||
self.main_window.current_hovered_card.leaveEvent(None)
|
|
||||||
except RuntimeError:
|
|
||||||
pass # Card already deleted
|
|
||||||
self.main_window.current_hovered_card = None
|
self.main_window.current_hovered_card = None
|
||||||
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
|
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
|
||||||
try:
|
self.main_window.current_focused_card._focused = False
|
||||||
self.main_window.current_focused_card._focused = False
|
self.main_window.current_focused_card.clearFocus()
|
||||||
self.main_window.current_focused_card.clearFocus()
|
|
||||||
except RuntimeError:
|
|
||||||
pass # Card already deleted
|
|
||||||
self.main_window.current_focused_card = card
|
self.main_window.current_focused_card = card
|
||||||
else:
|
else:
|
||||||
if self.main_window.current_focused_card == card:
|
if self.main_window.current_focused_card == card:
|
||||||
@@ -199,19 +185,11 @@ class GameLibraryManager:
|
|||||||
|
|
||||||
if is_hovered:
|
if is_hovered:
|
||||||
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
|
if self.main_window.current_focused_card and self.main_window.current_focused_card != card:
|
||||||
try:
|
self.main_window.current_focused_card._focused = False
|
||||||
if self.main_window.current_focused_card:
|
self.main_window.current_focused_card.clearFocus()
|
||||||
self.main_window.current_focused_card._focused = False
|
|
||||||
self.main_window.current_focused_card.clearFocus()
|
|
||||||
except RuntimeError:
|
|
||||||
pass # Card already deleted
|
|
||||||
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
|
if self.main_window.current_hovered_card and self.main_window.current_hovered_card != card:
|
||||||
try:
|
self.main_window.current_hovered_card._hovered = False
|
||||||
if self.main_window.current_hovered_card:
|
self.main_window.current_hovered_card.leaveEvent(None)
|
||||||
self.main_window.current_hovered_card._hovered = False
|
|
||||||
self.main_window.current_hovered_card.leaveEvent(None)
|
|
||||||
except RuntimeError:
|
|
||||||
pass # Card already deleted
|
|
||||||
self.main_window.current_hovered_card = card
|
self.main_window.current_hovered_card = card
|
||||||
else:
|
else:
|
||||||
if self.main_window.current_hovered_card == card:
|
if self.main_window.current_hovered_card == card:
|
||||||
@@ -230,10 +208,6 @@ class GameLibraryManager:
|
|||||||
if games_list is not None:
|
if games_list is not None:
|
||||||
self.filtered_games = games_list
|
self.filtered_games = games_list
|
||||||
self.dirty = True # Full rebuild only for non-filter
|
self.dirty = True # Full rebuild only for non-filter
|
||||||
else:
|
|
||||||
# When filtering, we want to update with the current filtered_games
|
|
||||||
# which has already been set by _perform_search
|
|
||||||
pass
|
|
||||||
self.is_filtering = is_filter
|
self.is_filtering = is_filter
|
||||||
self._pending_update = True
|
self._pending_update = True
|
||||||
|
|
||||||
@@ -242,20 +216,6 @@ class GameLibraryManager:
|
|||||||
else:
|
else:
|
||||||
self._update_game_grid_immediate()
|
self._update_game_grid_immediate()
|
||||||
|
|
||||||
def force_update_cards_library(self):
|
|
||||||
if self.gamesListWidget and self.gamesListLayout:
|
|
||||||
# Use singleShot to ensure UI updates happen after all other operations complete
|
|
||||||
# This prevents potential freezing in PySide 6.10.1
|
|
||||||
QTimer.singleShot(0, self._perform_force_update)
|
|
||||||
|
|
||||||
def _perform_force_update(self):
|
|
||||||
"""Perform the actual force update on the layout."""
|
|
||||||
if self.gamesListLayout:
|
|
||||||
self.gamesListLayout.invalidate()
|
|
||||||
if self.gamesListWidget:
|
|
||||||
self.gamesListWidget.adjustSize()
|
|
||||||
self.gamesListWidget.updateGeometry()
|
|
||||||
|
|
||||||
def _update_game_grid_immediate(self):
|
def _update_game_grid_immediate(self):
|
||||||
"""Updates the game grid with the provided or current game list."""
|
"""Updates the game grid with the provided or current game list."""
|
||||||
if self.gamesListLayout is None or self.gamesListWidget is None:
|
if self.gamesListLayout is None or self.gamesListWidget is None:
|
||||||
@@ -264,9 +224,8 @@ class GameLibraryManager:
|
|||||||
search_text = self.main_window.searchEdit.text().strip().lower()
|
search_text = self.main_window.searchEdit.text().strip().lower()
|
||||||
|
|
||||||
if self.is_filtering:
|
if self.is_filtering:
|
||||||
# Filter mode: use the pre-computed filtered_games from optimized search
|
# Filter mode: do not change layout, only hide/show cards
|
||||||
# This means we already have the exact games to show
|
self._apply_filter_visibility(search_text)
|
||||||
self._update_search_results()
|
|
||||||
else:
|
else:
|
||||||
# Full update: sorting, removal/addition, reorganization
|
# Full update: sorting, removal/addition, reorganization
|
||||||
games_list = self.filtered_games if self.filtered_games else self.games
|
games_list = self.filtered_games if self.filtered_games else self.games
|
||||||
@@ -294,9 +253,8 @@ class GameLibraryManager:
|
|||||||
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
|
return (fav_order, -game[10] if game[10] else 0, -game[11] if game[11] else 0)
|
||||||
|
|
||||||
# Quick partition: Sort favorites and non-favorites separately, then merge
|
# Quick partition: Sort favorites and non-favorites separately, then merge
|
||||||
favorites_set = set(favorites) # Convert to set for O(1) lookup
|
fav_games = [g for g in games_list if g[0] in favorites]
|
||||||
fav_games = [g for g in games_list if g[0] in favorites_set]
|
non_fav_games = [g for g in games_list if g[0] not in favorites]
|
||||||
non_fav_games = [g for g in games_list if g[0] not in favorites_set]
|
|
||||||
sorted_fav = sorted(fav_games, key=partition_sort_key)
|
sorted_fav = sorted(fav_games, key=partition_sort_key)
|
||||||
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
|
sorted_non_fav = sorted(non_fav_games, key=partition_sort_key)
|
||||||
sorted_games = sorted_fav + sorted_non_fav
|
sorted_games = sorted_fav + sorted_non_fav
|
||||||
@@ -387,77 +345,10 @@ class GameLibraryManager:
|
|||||||
self.gamesListWidget.updateGeometry()
|
self.gamesListWidget.updateGeometry()
|
||||||
self.main_window._last_card_width = self.card_width
|
self.main_window._last_card_width = self.card_width
|
||||||
|
|
||||||
self.force_update_cards_library()
|
|
||||||
|
|
||||||
self.is_filtering = False # Reset flag in any case
|
self.is_filtering = False # Reset flag in any case
|
||||||
|
|
||||||
def _update_search_results(self):
|
|
||||||
"""Update the grid with pre-computed search results."""
|
|
||||||
if self.gamesListLayout is None or self.gamesListWidget is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Batch layout updates
|
|
||||||
self.gamesListWidget.setUpdatesEnabled(False)
|
|
||||||
if self.gamesListLayout is not None:
|
|
||||||
self.gamesListLayout.setEnabled(False) # Disable layout during batch
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Create set of keys for current filtered games for fast lookup
|
|
||||||
filtered_keys = {(game[0], game[4]) for game in self.filtered_games} # (name, exec_line)
|
|
||||||
|
|
||||||
# Process existing cards: show cards that are in filtered results, hide others
|
|
||||||
cards_to_hide = []
|
|
||||||
for card_key, card in self.game_card_cache.items():
|
|
||||||
if card_key in filtered_keys:
|
|
||||||
# Card should be visible
|
|
||||||
if not card.isVisible():
|
|
||||||
card.setVisible(True)
|
|
||||||
else:
|
|
||||||
# Card should be hidden
|
|
||||||
if card.isVisible():
|
|
||||||
card.setVisible(False)
|
|
||||||
cards_to_hide.append(card_key)
|
|
||||||
|
|
||||||
# Now add any missing cards that are in filtered results but not in cache
|
|
||||||
cards_to_add = []
|
|
||||||
for game_data in self.filtered_games:
|
|
||||||
game_name = game_data[0]
|
|
||||||
exec_line = game_data[4]
|
|
||||||
game_key = (game_name, exec_line)
|
|
||||||
|
|
||||||
if game_key not in self.game_card_cache:
|
|
||||||
if self.context_menu_manager is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
card = self._create_game_card(game_data)
|
|
||||||
self.game_card_cache[game_key] = card
|
|
||||||
card.setVisible(True) # New cards should be visible
|
|
||||||
cards_to_add.append((game_key, card))
|
|
||||||
|
|
||||||
# Add new cards to layout
|
|
||||||
for _game_key, card in cards_to_add:
|
|
||||||
self.gamesListLayout.addWidget(card)
|
|
||||||
|
|
||||||
# Remove cards that are no longer needed (if any)
|
|
||||||
# Note: we're not removing them completely as they might be needed later
|
|
||||||
# Instead, we just hide them and they'll be reused if needed
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if self.gamesListLayout is not None:
|
|
||||||
self.gamesListLayout.setEnabled(True)
|
|
||||||
self.gamesListWidget.setUpdatesEnabled(True)
|
|
||||||
if self.gamesListLayout is not None:
|
|
||||||
self.gamesListLayout.update()
|
|
||||||
self.gamesListWidget.updateGeometry()
|
|
||||||
self.main_window._last_card_width = self.card_width
|
|
||||||
|
|
||||||
self.force_update_cards_library()
|
|
||||||
|
|
||||||
def _apply_filter_visibility(self, search_text: str):
|
def _apply_filter_visibility(self, search_text: str):
|
||||||
"""Applies visibility to cards based on search, without changing the layout."""
|
"""Applies visibility to cards based on search, without changing the layout."""
|
||||||
# This method is used for simple substring matching
|
|
||||||
# For the new optimized search, we'll use a different approach in update_game_grid
|
|
||||||
# when is_filter=True
|
|
||||||
visible_count = 0
|
visible_count = 0
|
||||||
for game_key, card in self.game_card_cache.items():
|
for game_key, card in self.game_card_cache.items():
|
||||||
game_name = card.name # Assume GameCard has 'name' attribute
|
game_name = card.name # Assume GameCard has 'name' attribute
|
||||||
@@ -471,9 +362,8 @@ class GameLibraryManager:
|
|||||||
cover_path, width, height, callback = self.pending_images.pop(game_key)
|
cover_path, width, height, callback = self.pending_images.pop(game_key)
|
||||||
load_pixmap_async(cover_path, width, height, callback)
|
load_pixmap_async(cover_path, width, height, callback)
|
||||||
|
|
||||||
# Force full relayout after visibility changes
|
# Force geometry update so FlowLayout accounts for hidden widgets
|
||||||
if self.gamesListLayout is not None:
|
if self.gamesListLayout is not None:
|
||||||
self.gamesListLayout.invalidate() # Принудительно инвалидируем для пересчёта
|
|
||||||
self.gamesListLayout.update()
|
self.gamesListLayout.update()
|
||||||
if self.gamesListWidget is not None:
|
if self.gamesListWidget is not None:
|
||||||
self.gamesListWidget.updateGeometry()
|
self.gamesListWidget.updateGeometry()
|
||||||
@@ -490,7 +380,6 @@ class GameLibraryManager:
|
|||||||
select_callback=self.main_window.openGameDetailPage,
|
select_callback=self.main_window.openGameDetailPage,
|
||||||
theme=self.theme,
|
theme=self.theme,
|
||||||
card_width=self.card_width,
|
card_width=self.card_width,
|
||||||
parent=self.gamesListWidget,
|
|
||||||
context_menu_manager=self.context_menu_manager
|
context_menu_manager=self.context_menu_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -513,11 +402,6 @@ class GameLibraryManager:
|
|||||||
def _flush_deletions(self):
|
def _flush_deletions(self):
|
||||||
"""Delete pending widgets off the main update cycle."""
|
"""Delete pending widgets off the main update cycle."""
|
||||||
for card in list(self.pending_deletions):
|
for card in list(self.pending_deletions):
|
||||||
# Clear any references to this card if it's currently focused/hovered
|
|
||||||
if self.main_window.current_focused_card == card:
|
|
||||||
self.main_window.current_focused_card = None
|
|
||||||
if self.main_window.current_hovered_card == card:
|
|
||||||
self.main_window.current_hovered_card = None
|
|
||||||
card.deleteLater()
|
card.deleteLater()
|
||||||
self.pending_deletions.remove(card)
|
self.pending_deletions.remove(card)
|
||||||
|
|
||||||
@@ -525,61 +409,24 @@ class GameLibraryManager:
|
|||||||
"""Clears all widgets from the layout."""
|
"""Clears all widgets from the layout."""
|
||||||
if layout is None:
|
if layout is None:
|
||||||
return
|
return
|
||||||
# Remove all widgets from the layout and clean up caches
|
|
||||||
while layout.count():
|
while layout.count():
|
||||||
child = layout.takeAt(0)
|
child = layout.takeAt(0)
|
||||||
if child.widget():
|
if child.widget():
|
||||||
widget = child.widget()
|
widget = child.widget()
|
||||||
# Clean up cache if widget exists in it
|
|
||||||
for key, card in list(self.game_card_cache.items()):
|
for key, card in list(self.game_card_cache.items()):
|
||||||
if card == widget:
|
if card == widget:
|
||||||
del self.game_card_cache[key]
|
del self.game_card_cache[key]
|
||||||
if key in self.pending_images:
|
if key in self.pending_images:
|
||||||
del self.pending_images[key]
|
del self.pending_images[key]
|
||||||
break
|
|
||||||
# Always schedule widget for deletion regardless of cache state
|
|
||||||
widget.deleteLater()
|
widget.deleteLater()
|
||||||
|
|
||||||
# Also clear the cache completely if needed (in case layout wasn't in sync)
|
|
||||||
self.game_card_cache.clear()
|
|
||||||
self.pending_images.clear()
|
|
||||||
|
|
||||||
def set_games(self, games: list[tuple]):
|
def set_games(self, games: list[tuple]):
|
||||||
"""Sets the games list and updates the filtered games."""
|
"""Sets the games list and updates the filtered games."""
|
||||||
self.games = games
|
self.games = games
|
||||||
self.filtered_games = self.games
|
self.filtered_games = self.games
|
||||||
|
|
||||||
# Build search indices for fast searching
|
|
||||||
self._build_search_indices(games)
|
|
||||||
|
|
||||||
self.dirty = True # Full resort needed
|
self.dirty = True # Full resort needed
|
||||||
self.update_game_grid()
|
self.update_game_grid()
|
||||||
|
|
||||||
def _build_search_indices(self, games: list[tuple]):
|
|
||||||
"""Build search indices for fast searching."""
|
|
||||||
# Prepare items for indexing: (search_key, game_data)
|
|
||||||
# We'll index by game name (index 0) and potentially other fields
|
|
||||||
items = []
|
|
||||||
for game in games:
|
|
||||||
# game is a tuple: (name, description, cover, appid, exec_line, controller_support,
|
|
||||||
# last_launch, formatted_playtime, protondb_tier, anticheat_status,
|
|
||||||
# last_played_timestamp, playtime_seconds, game_source)
|
|
||||||
name = str(game[0]).lower() if game[0] else ""
|
|
||||||
description = str(game[1]).lower() if game[1] else ""
|
|
||||||
|
|
||||||
# Create multiple search entries for better matching
|
|
||||||
items.append((name, game)) # Exact name
|
|
||||||
# Add other searchable fields if needed
|
|
||||||
if description:
|
|
||||||
items.append((description, game))
|
|
||||||
|
|
||||||
# Also add individual words from the name for partial matching
|
|
||||||
for word in name.split():
|
|
||||||
if len(word) > 2: # Only index words longer than 2 characters
|
|
||||||
items.append((word, game))
|
|
||||||
|
|
||||||
self.search_optimizer.build_indices(items)
|
|
||||||
|
|
||||||
def add_game_incremental(self, game_data: tuple):
|
def add_game_incremental(self, game_data: tuple):
|
||||||
"""Add a single game without full reload."""
|
"""Add a single game without full reload."""
|
||||||
self.games.append(game_data)
|
self.games.append(game_data)
|
||||||
@@ -603,54 +450,4 @@ class GameLibraryManager:
|
|||||||
|
|
||||||
def filter_games_delayed(self):
|
def filter_games_delayed(self):
|
||||||
"""Filters games based on search text and updates the grid."""
|
"""Filters games based on search text and updates the grid."""
|
||||||
search_text = self.main_window.searchEdit.text().strip().lower()
|
self.update_game_grid(is_filter=True)
|
||||||
|
|
||||||
if not search_text:
|
|
||||||
# If search is empty, show all games
|
|
||||||
self.filtered_games = self.games
|
|
||||||
self.update_game_grid(is_filter=True)
|
|
||||||
else:
|
|
||||||
# Use the optimized search
|
|
||||||
self._perform_search(search_text)
|
|
||||||
|
|
||||||
def _perform_search(self, search_text: str):
|
|
||||||
"""Perform the actual search using optimized search algorithms."""
|
|
||||||
if not search_text:
|
|
||||||
self.filtered_games = self.games
|
|
||||||
self.update_game_grid(is_filter=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Use exact search first
|
|
||||||
exact_result = self.search_optimizer.exact_search(search_text)
|
|
||||||
if exact_result:
|
|
||||||
# If exact match found, show only that game
|
|
||||||
self.filtered_games = [exact_result]
|
|
||||||
self.update_game_grid(is_filter=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Try prefix search
|
|
||||||
prefix_results = self.search_optimizer.prefix_search(search_text)
|
|
||||||
if prefix_results:
|
|
||||||
# Get the actual game data from the prefix matches
|
|
||||||
filtered_games = []
|
|
||||||
for _match_text, game_data in prefix_results:
|
|
||||||
if game_data not in filtered_games: # Avoid duplicates
|
|
||||||
filtered_games.append(game_data)
|
|
||||||
self.filtered_games = filtered_games
|
|
||||||
self.update_game_grid(is_filter=True)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Finally, try fuzzy search
|
|
||||||
fuzzy_results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=60.0)
|
|
||||||
if fuzzy_results:
|
|
||||||
# Get the actual game data from the fuzzy matches
|
|
||||||
filtered_games = []
|
|
||||||
for _match_text, game_data, _score in fuzzy_results:
|
|
||||||
if game_data not in filtered_games: # Avoid duplicates
|
|
||||||
filtered_games.append(game_data)
|
|
||||||
self.filtered_games = filtered_games
|
|
||||||
self.update_game_grid(is_filter=True)
|
|
||||||
else:
|
|
||||||
# If no results found, show empty list
|
|
||||||
self.filtered_games = []
|
|
||||||
self.update_game_grid(is_filter=True)
|
|
||||||
|
|||||||
@@ -36,22 +36,11 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
current_theme_name = read_theme_from_config()
|
current_theme_name = read_theme_from_config()
|
||||||
|
|
||||||
def finish_with(pixmap: QPixmap):
|
def finish_with(pixmap: QPixmap):
|
||||||
# Check if pixmap is valid before attempting to scale it
|
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
||||||
if pixmap.isNull():
|
x = (scaled.width() - width) // 2
|
||||||
# Create a default placeholder pixmap instead of trying to scale a null pixmap
|
y = (scaled.height() - height) // 2
|
||||||
placeholder_pixmap = QPixmap(width, height)
|
cropped = scaled.copy(x, y, width, height)
|
||||||
placeholder_pixmap.fill(QColor("#333333"))
|
callback(cropped)
|
||||||
painter = QPainter(placeholder_pixmap)
|
|
||||||
painter.setPen(QPen(QColor("white")))
|
|
||||||
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
|
||||||
painter.end()
|
|
||||||
callback(placeholder_pixmap)
|
|
||||||
else:
|
|
||||||
scaled = pixmap.scaled(width, height, Qt.AspectRatioMode.KeepAspectRatioByExpanding, Qt.TransformationMode.SmoothTransformation)
|
|
||||||
x = (scaled.width() - width) // 2
|
|
||||||
y = (scaled.height() - height) // 2
|
|
||||||
cropped = scaled.copy(x, y, width, height)
|
|
||||||
callback(cropped)
|
|
||||||
|
|
||||||
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
xdg_cache_home = os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache"))
|
||||||
image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
|
image_folder = os.path.join(xdg_cache_home, "PortProtonQt", "images")
|
||||||
@@ -69,9 +58,6 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
local_path = os.path.join(image_folder, f"{appid}.jpg")
|
local_path = os.path.join(image_folder, f"{appid}.jpg")
|
||||||
if os.path.exists(local_path):
|
if os.path.exists(local_path):
|
||||||
pixmap = QPixmap(local_path)
|
pixmap = QPixmap(local_path)
|
||||||
# Check if the pixmap loaded successfully
|
|
||||||
if pixmap.isNull():
|
|
||||||
logger.warning(f"Failed to load image from {local_path}")
|
|
||||||
finish_with(pixmap)
|
finish_with(pixmap)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -83,8 +69,6 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||||
if placeholder_path and QFile.exists(placeholder_path):
|
if placeholder_path and QFile.exists(placeholder_path):
|
||||||
pixmap.load(placeholder_path)
|
pixmap.load(placeholder_path)
|
||||||
if pixmap.isNull():
|
|
||||||
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
|
||||||
else:
|
else:
|
||||||
pixmap = QPixmap(width, height)
|
pixmap = QPixmap(width, height)
|
||||||
pixmap.fill(QColor("#333333"))
|
pixmap.fill(QColor("#333333"))
|
||||||
@@ -99,56 +83,11 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка обработки URL {cover}: {e}")
|
logger.error(f"Ошибка обработки URL {cover}: {e}")
|
||||||
|
|
||||||
# SteamGridDB (SGDB)
|
|
||||||
if cover and cover.startswith("https://cdn2.steamgriddb.com"):
|
|
||||||
try:
|
|
||||||
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
|
|
||||||
|
|
||||||
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"))
|
|
||||||
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://")):
|
if cover and cover.startswith(("http://", "https://")):
|
||||||
try:
|
try:
|
||||||
local_path = os.path.join(image_folder, f"{app_name}.jpg")
|
local_path = os.path.join(image_folder, f"{app_name}.jpg")
|
||||||
if os.path.exists(local_path):
|
if os.path.exists(local_path):
|
||||||
pixmap = QPixmap(local_path)
|
pixmap = QPixmap(local_path)
|
||||||
# Check if the pixmap loaded successfully
|
|
||||||
if pixmap.isNull():
|
|
||||||
logger.warning(f"Failed to load image from {local_path}")
|
|
||||||
finish_with(pixmap)
|
finish_with(pixmap)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -160,8 +99,6 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
placeholder_path = theme_manager.get_theme_image("placeholder", current_theme_name)
|
||||||
if placeholder_path and QFile.exists(placeholder_path):
|
if placeholder_path and QFile.exists(placeholder_path):
|
||||||
pixmap.load(placeholder_path)
|
pixmap.load(placeholder_path)
|
||||||
if pixmap.isNull():
|
|
||||||
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
|
||||||
else:
|
else:
|
||||||
pixmap = QPixmap(width, height)
|
pixmap = QPixmap(width, height)
|
||||||
pixmap.fill(QColor("#333333"))
|
pixmap.fill(QColor("#333333"))
|
||||||
@@ -178,9 +115,6 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
|
|
||||||
if cover and QFile.exists(cover):
|
if cover and QFile.exists(cover):
|
||||||
pixmap = QPixmap(cover)
|
pixmap = QPixmap(cover)
|
||||||
# Check if the pixmap loaded successfully
|
|
||||||
if pixmap.isNull():
|
|
||||||
logger.warning(f"Failed to load image from {cover}")
|
|
||||||
finish_with(pixmap)
|
finish_with(pixmap)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -188,8 +122,6 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
pixmap = QPixmap()
|
pixmap = QPixmap()
|
||||||
if placeholder_path and QFile.exists(placeholder_path):
|
if placeholder_path and QFile.exists(placeholder_path):
|
||||||
pixmap.load(placeholder_path)
|
pixmap.load(placeholder_path)
|
||||||
if pixmap.isNull():
|
|
||||||
logger.warning(f"Failed to load placeholder image from {placeholder_path}")
|
|
||||||
else:
|
else:
|
||||||
pixmap = QPixmap(width, height)
|
pixmap = QPixmap(width, height)
|
||||||
pixmap.fill(QColor("#333333"))
|
pixmap.fill(QColor("#333333"))
|
||||||
@@ -199,9 +131,9 @@ def load_pixmap_async(cover: str, width: int, height: int, callback: Callable[[Q
|
|||||||
painter.end()
|
painter.end()
|
||||||
finish_with(pixmap)
|
finish_with(pixmap)
|
||||||
|
|
||||||
# Submit the process_image function directly to the executor
|
with queue_lock:
|
||||||
# This avoids the potential blocking issue with queue.get() in PySide 6.10.1
|
image_load_queue.put(process_image)
|
||||||
image_executor.submit(process_image)
|
image_executor.submit(lambda: image_load_queue.get()())
|
||||||
|
|
||||||
def round_corners(pixmap, radius):
|
def round_corners(pixmap, radius):
|
||||||
"""
|
"""
|
||||||
@@ -209,15 +141,7 @@ def round_corners(pixmap, radius):
|
|||||||
"""
|
"""
|
||||||
if pixmap.isNull():
|
if pixmap.isNull():
|
||||||
return pixmap
|
return pixmap
|
||||||
|
|
||||||
# Check if radius is valid to prevent issues
|
|
||||||
if radius <= 0:
|
|
||||||
return pixmap
|
|
||||||
|
|
||||||
size = pixmap.size()
|
size = pixmap.size()
|
||||||
if size.width() <= 0 or size.height() <= 0:
|
|
||||||
return pixmap
|
|
||||||
|
|
||||||
rounded = QPixmap(size)
|
rounded = QPixmap(size)
|
||||||
rounded.fill(QColor(0, 0, 0, 0))
|
rounded.fill(QColor(0, 0, 0, 0))
|
||||||
painter = QPainter(rounded)
|
painter = QPainter(rounded)
|
||||||
@@ -320,31 +244,20 @@ class FullscreenDialog(QDialog):
|
|||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
pixmap, caption = self.images[self.current_index]
|
pixmap, caption = self.images[self.current_index]
|
||||||
# Check if pixmap is valid before attempting to scale it
|
# Учитываем devicePixelRatio для масштабирования высокого качества
|
||||||
if pixmap.isNull():
|
device_pixel_ratio = get_device_pixel_ratio()
|
||||||
# Create a default placeholder pixmap instead of trying to scale a null pixmap
|
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
|
||||||
placeholder_pixmap = QPixmap(self.FIXED_WIDTH - 80, self.FIXED_HEIGHT)
|
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
|
||||||
placeholder_pixmap.fill(QColor("#333333"))
|
|
||||||
painter = QPainter(placeholder_pixmap)
|
|
||||||
painter.setPen(QPen(QColor("white")))
|
|
||||||
painter.drawText(placeholder_pixmap.rect(), Qt.AlignmentFlag.AlignCenter, "No Image")
|
|
||||||
painter.end()
|
|
||||||
self.imageLabel.setPixmap(placeholder_pixmap)
|
|
||||||
else:
|
|
||||||
# Учитываем devicePixelRatio для масштабирования высокого качества
|
|
||||||
device_pixel_ratio = get_device_pixel_ratio()
|
|
||||||
target_width = int((self.FIXED_WIDTH - 80) * device_pixel_ratio)
|
|
||||||
target_height = int(self.FIXED_HEIGHT * device_pixel_ratio)
|
|
||||||
|
|
||||||
# Масштабируем изображение из оригинального pixmap
|
# Масштабируем изображение из оригинального pixmap
|
||||||
scaled_pixmap = pixmap.scaled(
|
scaled_pixmap = pixmap.scaled(
|
||||||
target_width,
|
target_width,
|
||||||
target_height,
|
target_height,
|
||||||
Qt.AspectRatioMode.KeepAspectRatio,
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
Qt.TransformationMode.SmoothTransformation
|
Qt.TransformationMode.SmoothTransformation
|
||||||
)
|
)
|
||||||
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
scaled_pixmap.setDevicePixelRatio(device_pixel_ratio)
|
||||||
self.imageLabel.setPixmap(scaled_pixmap)
|
self.imageLabel.setPixmap(scaled_pixmap)
|
||||||
self.captionLabel.setText(caption)
|
self.captionLabel.setText(caption)
|
||||||
self.setWindowTitle(caption)
|
self.setWindowTitle(caption)
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
# keyboard_layouts.py
|
|
||||||
keyboard_layouts = {
|
|
||||||
'en': {
|
|
||||||
'normal': [
|
|
||||||
['`', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='],
|
|
||||||
['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', '\\'],
|
|
||||||
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', "'"],
|
|
||||||
['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['~', '!', '@', '#', '$', '%', '^', '&&', '*', '(', ')', '_', '+'],
|
|
||||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'],
|
|
||||||
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"'],
|
|
||||||
['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'ru': {
|
|
||||||
'normal': [
|
|
||||||
['ё', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='],
|
|
||||||
['TAB', 'й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х', 'ъ', '\\'],
|
|
||||||
['CAPS', 'ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'],
|
|
||||||
['⬆', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', '.']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['Ё', '!', '"', '№', ';', '%', ':', '?', '*', '(', ')', '_', '+'],
|
|
||||||
['TAB', 'Й', 'Ц', 'У', 'К', 'Е', 'Н', 'Г', 'Ш', 'Щ', 'З', 'Х', 'Ъ', '/'],
|
|
||||||
['CAPS', 'Ф', 'Ы', 'В', 'А', 'П', 'Р', 'О', 'Л', 'Д', 'Ж', 'Э'],
|
|
||||||
['⬆', 'Я', 'Ч', 'С', 'М', 'И', 'Т', 'Ь', 'Б', 'Ю', ',']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'fr': {
|
|
||||||
'normal': [
|
|
||||||
['²', '&', 'é', '"', "'", '(', '-', 'è', '_', 'ç', 'à', ')', '='],
|
|
||||||
['TAB', 'a', 'z', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '^', '$', '*'],
|
|
||||||
['CAPS', 'q', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'ù'],
|
|
||||||
['⬆', 'w', 'x', 'c', 'v', 'b', 'n', ',', ';', ':', '!']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '°', '+'],
|
|
||||||
['TAB', 'A', 'Z', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '¨', '£', 'µ'],
|
|
||||||
['CAPS', 'Q', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', '%'],
|
|
||||||
['⬆', 'W', 'X', 'C', 'V', 'B', 'N', '?', '.', '/', '§']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'es': {
|
|
||||||
'normal': [
|
|
||||||
['º', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', "'", '¡'],
|
|
||||||
['TAB', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '`', '+', '\\'],
|
|
||||||
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ñ', "'", 'ç'],
|
|
||||||
['⬆', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['ª', '!', '"', '·', '$', '%', '&', '/', '(', ')', '=', '?', '¿'],
|
|
||||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '^', '*', '|'],
|
|
||||||
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ñ', '"', 'Ç'],
|
|
||||||
['⬆', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_']
|
|
||||||
]
|
|
||||||
},
|
|
||||||
'de': {
|
|
||||||
'normal': [
|
|
||||||
['^', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'ß', '´'],
|
|
||||||
['TAB', 'q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p', 'ü', '+', '#'],
|
|
||||||
['CAPS', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ö', 'ä'],
|
|
||||||
['⬆', '<', 'y', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '-']
|
|
||||||
],
|
|
||||||
'shift': [
|
|
||||||
['°', '!', '"', '§', '$', '%', '&', '/', '(', ')', '=', '?', '`'],
|
|
||||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Z', 'U', 'I', 'O', 'P', 'Ü', '*', '\''],
|
|
||||||
['CAPS', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Ö', 'Ä'],
|
|
||||||
['⬆', '>', 'Y', 'X', 'C', 'V', 'B', 'N', 'M', ';', ':', '_']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-11-30 13:20+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: de_DE\n"
|
"Language: de_DE\n"
|
||||||
@@ -76,6 +76,10 @@ msgstr ""
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -120,10 +124,6 @@ msgstr ""
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -191,10 +191,6 @@ msgstr ""
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -217,10 +213,6 @@ msgstr ""
|
|||||||
msgid "Failed to copy cover image: {error}"
|
msgid "Failed to copy cover image: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Unsupported image format: {extension}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to add '{game_name}' to Steam: {error}"
|
msgid "Failed to add '{game_name}' to Steam: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -256,43 +248,13 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Open"
|
#, python-brace-format
|
||||||
msgstr ""
|
msgid "Launching {0}"
|
||||||
|
|
||||||
msgid "Select Dir"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prev Dir"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Toggle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prev Tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Next Tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Save"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Launching {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -324,9 +286,6 @@ msgstr ""
|
|||||||
msgid "Custom Cover:"
|
msgid "Custom Cover:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enter local path or URL for cover image"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Cover Preview:"
|
msgid "Cover Preview:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -345,78 +304,6 @@ msgstr ""
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Exe Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search settings..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Main"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Advanced"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Setting"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "disabled"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Info"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No changes to apply."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to apply changes. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings updated successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -459,15 +346,15 @@ msgstr ""
|
|||||||
msgid "Unknown Game"
|
msgid "Unknown Game"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Starting PortProton..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Library"
|
msgid "Library"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -483,31 +370,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refresh Grid"
|
|
||||||
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 "Game library refreshed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -520,122 +382,13 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "A refresh is already in progress..."
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refreshing..."
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refreshing game library..."
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{name}'"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
@@ -671,9 +424,6 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Gamepad Type:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -698,12 +448,6 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Minimize to tray on close"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Application Close Mode:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -778,10 +522,6 @@ msgstr ""
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Executable not found: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -840,262 +580,6 @@ msgstr ""
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
|
||||||
"combination - right Shift + F12)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable vkBasalt by default to improve graphics in games running on "
|
|
||||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
|
||||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Super + F : Toggle fullscreen\n"
|
|
||||||
"Super + N : Toggle nearest neighbour filtering\n"
|
|
||||||
"Super + U : Toggle FSR upscaling\n"
|
|
||||||
"Super + Y : Toggle NIS upscaling\n"
|
|
||||||
"Super + I : Increase FSR sharpness by 1\n"
|
|
||||||
"Super + O : Decrease FSR sharpness by 1\n"
|
|
||||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
|
||||||
"Super + G : Toggle keyboard grab\n"
|
|
||||||
"Super + C : Update clipboard"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable futex-based in-process synchronization primitives."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable vkd3d support - Ray Tracing"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Disguise all NVIDIA GPU features"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Run the application in WINE virtual desktop"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Run the application in a terminal"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use system GameMode for performance optimization"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable forced use of third-party DirectX libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fix pink-tinted video playback in some games"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force US keyboard layout"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use WINE shader caching"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force use of built-in DXGI library"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Disable desktop compositing for performance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use container launch mode (recommended default)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force DirectInput protocol instead of XInput"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable experimental native Wayland support"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable HDR settings under native Wayland"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use async dxvk-sarek (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Select the Wine or Proton version to use for this executable."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix Name"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Specify the Wine prefix to run this game with"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Newest"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Stable"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Vulkan Backend"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
|
||||||
"\n"
|
|
||||||
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
|
||||||
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
|
||||||
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
|
||||||
"driver)\n"
|
|
||||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
|
||||||
"Vulkan 1.1+)\n"
|
|
||||||
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Windows version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Changing the WINDOWS emulation version may be required to run older "
|
|
||||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "DLL Overrides"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Forced to use/disable the library only for the given application.\n"
|
|
||||||
"\n"
|
|
||||||
"A brief instruction:\n"
|
|
||||||
"* libraries are written WITHOUT the .dll file extension\n"
|
|
||||||
"* libraries are separated by semicolons - ;\n"
|
|
||||||
"* library=n - use the WINDOWS (third-party) library\n"
|
|
||||||
"* library=b - use WINE (built-in) library\n"
|
|
||||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
|
||||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
|
||||||
"* library= - disable the use of this library\n"
|
|
||||||
"\n"
|
|
||||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launch Arguments"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Adding an argument after the .exe file, just like you would add an "
|
|
||||||
"argument in a shortcut on a WINDOWS system.\n"
|
|
||||||
"\n"
|
|
||||||
"Example: -dx11 -skipintro 1"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "CPU Cores Limit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
|
||||||
"recommended to set the value equal to 8)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "OpenGL Version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"You can select the required OpenGL version, some games require a forced "
|
|
||||||
"Compatibility Profile (COMP)."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "VKD3D Feature Level"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Locale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Window Mode"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Window mode (for Vulkan and OpenGL):\n"
|
|
||||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
|
||||||
"immediate - Unlimited frame rate + tearing.\n"
|
|
||||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
|
||||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
|
||||||
" rate."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "AMD Vulkan Driver"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
|
||||||
" vulkan will be used to run the game"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "NUMA Node"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
|
||||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
|
||||||
" single node reduces memory-access latency and limits costly core-to-core"
|
|
||||||
" switches."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reboot"
|
msgid "Reboot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-11-30 13:20+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@@ -76,6 +76,10 @@ msgstr ""
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -120,10 +124,6 @@ msgstr ""
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -191,10 +191,6 @@ msgstr ""
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -217,10 +213,6 @@ msgstr ""
|
|||||||
msgid "Failed to copy cover image: {error}"
|
msgid "Failed to copy cover image: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Unsupported image format: {extension}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to add '{game_name}' to Steam: {error}"
|
msgid "Failed to add '{game_name}' to Steam: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -256,43 +248,13 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Open"
|
#, python-brace-format
|
||||||
msgstr ""
|
msgid "Launching {0}"
|
||||||
|
|
||||||
msgid "Select Dir"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prev Dir"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Toggle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prev Tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Next Tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Save"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Launching {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -324,9 +286,6 @@ msgstr ""
|
|||||||
msgid "Custom Cover:"
|
msgid "Custom Cover:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enter local path or URL for cover image"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Cover Preview:"
|
msgid "Cover Preview:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -345,78 +304,6 @@ msgstr ""
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Exe Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search settings..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Main"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Advanced"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Setting"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "disabled"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Info"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No changes to apply."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to apply changes. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings updated successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -459,15 +346,15 @@ msgstr ""
|
|||||||
msgid "Unknown Game"
|
msgid "Unknown Game"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Starting PortProton..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Library"
|
msgid "Library"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -483,31 +370,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refresh Grid"
|
|
||||||
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 "Game library refreshed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -520,122 +382,13 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "A refresh is already in progress..."
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refreshing..."
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refreshing game library..."
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{name}'"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
@@ -671,9 +424,6 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Gamepad Type:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -698,12 +448,6 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Minimize to tray on close"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Application Close Mode:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -778,10 +522,6 @@ msgstr ""
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Executable not found: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -840,262 +580,6 @@ msgstr ""
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
|
||||||
"combination - right Shift + F12)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable vkBasalt by default to improve graphics in games running on "
|
|
||||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
|
||||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Super + F : Toggle fullscreen\n"
|
|
||||||
"Super + N : Toggle nearest neighbour filtering\n"
|
|
||||||
"Super + U : Toggle FSR upscaling\n"
|
|
||||||
"Super + Y : Toggle NIS upscaling\n"
|
|
||||||
"Super + I : Increase FSR sharpness by 1\n"
|
|
||||||
"Super + O : Decrease FSR sharpness by 1\n"
|
|
||||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
|
||||||
"Super + G : Toggle keyboard grab\n"
|
|
||||||
"Super + C : Update clipboard"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable futex-based in-process synchronization primitives."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable vkd3d support - Ray Tracing"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Disguise all NVIDIA GPU features"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Run the application in WINE virtual desktop"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Run the application in a terminal"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use system GameMode for performance optimization"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable forced use of third-party DirectX libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fix pink-tinted video playback in some games"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force US keyboard layout"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use WINE shader caching"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force use of built-in DXGI library"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Disable desktop compositing for performance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use container launch mode (recommended default)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force DirectInput protocol instead of XInput"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable experimental native Wayland support"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable HDR settings under native Wayland"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use async dxvk-sarek (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Select the Wine or Proton version to use for this executable."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix Name"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Specify the Wine prefix to run this game with"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Newest"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Stable"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Vulkan Backend"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
|
||||||
"\n"
|
|
||||||
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
|
||||||
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
|
||||||
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
|
||||||
"driver)\n"
|
|
||||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
|
||||||
"Vulkan 1.1+)\n"
|
|
||||||
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Windows version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Changing the WINDOWS emulation version may be required to run older "
|
|
||||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "DLL Overrides"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Forced to use/disable the library only for the given application.\n"
|
|
||||||
"\n"
|
|
||||||
"A brief instruction:\n"
|
|
||||||
"* libraries are written WITHOUT the .dll file extension\n"
|
|
||||||
"* libraries are separated by semicolons - ;\n"
|
|
||||||
"* library=n - use the WINDOWS (third-party) library\n"
|
|
||||||
"* library=b - use WINE (built-in) library\n"
|
|
||||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
|
||||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
|
||||||
"* library= - disable the use of this library\n"
|
|
||||||
"\n"
|
|
||||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launch Arguments"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Adding an argument after the .exe file, just like you would add an "
|
|
||||||
"argument in a shortcut on a WINDOWS system.\n"
|
|
||||||
"\n"
|
|
||||||
"Example: -dx11 -skipintro 1"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "CPU Cores Limit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
|
||||||
"recommended to set the value equal to 8)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "OpenGL Version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"You can select the required OpenGL version, some games require a forced "
|
|
||||||
"Compatibility Profile (COMP)."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "VKD3D Feature Level"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Locale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Window Mode"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Window mode (for Vulkan and OpenGL):\n"
|
|
||||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
|
||||||
"immediate - Unlimited frame rate + tearing.\n"
|
|
||||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
|
||||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
|
||||||
" rate."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "AMD Vulkan Driver"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
|
||||||
" vulkan will be used to run the game"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "NUMA Node"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
|
||||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
|
||||||
" single node reduces memory-access latency and limits costly core-to-core"
|
|
||||||
" switches."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reboot"
|
msgid "Reboot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
"Project-Id-Version: PortProtonQt 0.1.1\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-11-30 13:20+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -74,6 +74,10 @@ msgstr ""
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -118,10 +122,6 @@ msgstr ""
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -189,10 +189,6 @@ msgstr ""
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -215,10 +211,6 @@ msgstr ""
|
|||||||
msgid "Failed to copy cover image: {error}"
|
msgid "Failed to copy cover image: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Unsupported image format: {extension}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to add '{game_name}' to Steam: {error}"
|
msgid "Failed to add '{game_name}' to Steam: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -254,43 +246,13 @@ msgstr ""
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Open"
|
#, python-brace-format
|
||||||
msgstr ""
|
msgid "Launching {0}"
|
||||||
|
|
||||||
msgid "Select Dir"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prev Dir"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Toggle"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prev Tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Next Tab"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Save"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Launching {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -322,9 +284,6 @@ msgstr ""
|
|||||||
msgid "Custom Cover:"
|
msgid "Custom Cover:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Enter local path or URL for cover image"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Cover Preview:"
|
msgid "Cover Preview:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -343,78 +302,6 @@ msgstr ""
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Exe Settings"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Search settings..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Main"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Advanced"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Setting"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Value"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "disabled"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Info"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "No changes to apply."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to apply changes. Check logs."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Settings updated successfully."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -457,15 +344,15 @@ msgstr ""
|
|||||||
msgid "Unknown Game"
|
msgid "Unknown Game"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Starting PortProton..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Library"
|
msgid "Library"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -481,31 +368,6 @@ msgstr ""
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refresh Grid"
|
|
||||||
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 "Game library refreshed"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -518,122 +380,13 @@ msgstr ""
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "A refresh is already in progress..."
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refreshing..."
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Refreshing game library..."
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{name}'"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
@@ -669,9 +422,6 @@ msgstr ""
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Gamepad Type:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -696,12 +446,6 @@ msgstr ""
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Minimize to tray on close"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Application Close Mode:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -776,10 +520,6 @@ msgstr ""
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Executable not found: {0}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -838,262 +578,6 @@ msgstr ""
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
|
||||||
"combination - right Shift + F12)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable vkBasalt by default to improve graphics in games running on "
|
|
||||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
|
||||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Super + F : Toggle fullscreen\n"
|
|
||||||
"Super + N : Toggle nearest neighbour filtering\n"
|
|
||||||
"Super + U : Toggle FSR upscaling\n"
|
|
||||||
"Super + Y : Toggle NIS upscaling\n"
|
|
||||||
"Super + I : Increase FSR sharpness by 1\n"
|
|
||||||
"Super + O : Decrease FSR sharpness by 1\n"
|
|
||||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
|
||||||
"Super + G : Toggle keyboard grab\n"
|
|
||||||
"Super + C : Update clipboard"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable futex-based in-process synchronization primitives."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable vkd3d support - Ray Tracing"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Disguise all NVIDIA GPU features"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Run the application in WINE virtual desktop"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Run the application in a terminal"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use system GameMode for performance optimization"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable forced use of third-party DirectX libraries"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Fix pink-tinted video playback in some games"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force US keyboard layout"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use WINE shader caching"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force use of built-in DXGI library"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Disable desktop compositing for performance"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use container launch mode (recommended default)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force DirectInput protocol instead of XInput"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable experimental native Wayland support"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Enable HDR settings under native Wayland"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Use async dxvk-sarek (experimental)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Wine Version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Select the Wine or Proton version to use for this executable."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Prefix Name"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Specify the Wine prefix to run this game with"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Newest"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Stable"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Vulkan Backend"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
|
||||||
"\n"
|
|
||||||
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
|
||||||
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
|
||||||
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
|
||||||
"driver)\n"
|
|
||||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
|
||||||
"Vulkan 1.1+)\n"
|
|
||||||
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Windows version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Changing the WINDOWS emulation version may be required to run older "
|
|
||||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "DLL Overrides"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Forced to use/disable the library only for the given application.\n"
|
|
||||||
"\n"
|
|
||||||
"A brief instruction:\n"
|
|
||||||
"* libraries are written WITHOUT the .dll file extension\n"
|
|
||||||
"* libraries are separated by semicolons - ;\n"
|
|
||||||
"* library=n - use the WINDOWS (third-party) library\n"
|
|
||||||
"* library=b - use WINE (built-in) library\n"
|
|
||||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
|
||||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
|
||||||
"* library= - disable the use of this library\n"
|
|
||||||
"\n"
|
|
||||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Launch Arguments"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Adding an argument after the .exe file, just like you would add an "
|
|
||||||
"argument in a shortcut on a WINDOWS system.\n"
|
|
||||||
"\n"
|
|
||||||
"Example: -dx11 -skipintro 1"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "CPU Cores Limit"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
|
||||||
"recommended to set the value equal to 8)"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "OpenGL Version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"You can select the required OpenGL version, some games require a forced "
|
|
||||||
"Compatibility Profile (COMP)."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "VKD3D Feature Level"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Locale"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Window Mode"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Window mode (for Vulkan and OpenGL):\n"
|
|
||||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
|
||||||
"immediate - Unlimited frame rate + tearing.\n"
|
|
||||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
|
||||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
|
||||||
" rate."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "AMD Vulkan Driver"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
|
||||||
" vulkan will be used to run the game"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "NUMA Node"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
|
||||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
|
||||||
" single node reduces memory-access latency and limits costly core-to-core"
|
|
||||||
" switches."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Reboot"
|
msgid "Reboot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -9,17 +9,18 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PROJECT VERSION\n"
|
"Project-Id-Version: PROJECT VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||||
"POT-Creation-Date: 2025-11-30 13:20+0500\n"
|
"POT-Creation-Date: 2025-09-23 22:23+0500\n"
|
||||||
"PO-Revision-Date: 2025-11-30 13:18+0500\n"
|
"PO-Revision-Date: 2025-09-23 22:23+0500\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language: ru_RU\n"
|
|
||||||
"Language-Team: ru_RU <LL@li.org>\n"
|
"Language-Team: ru_RU <LL@li.org>\n"
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
"Language: ru_RU\n"
|
||||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 "
|
||||||
|
"&& (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||||
"Generated-By: Babel 2.17.0\n"
|
"Generated-By: Babel 2.17.0\n"
|
||||||
|
"X-Generator: Poedit 3.6\n"
|
||||||
|
|
||||||
msgid "Error"
|
msgid "Error"
|
||||||
msgstr "Ошибка"
|
msgstr "Ошибка"
|
||||||
@@ -77,16 +78,20 @@ msgstr "Остановлен(а) '{game_name}'"
|
|||||||
msgid "Legendary executable not found at {path}"
|
msgid "Legendary executable not found at {path}"
|
||||||
msgstr "Legendary не найден по пути {path}"
|
msgstr "Legendary не найден по пути {path}"
|
||||||
|
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "start.sh not found at {path}"
|
||||||
|
msgstr "start.sh не найден по адресу {path}"
|
||||||
|
|
||||||
msgid "Success"
|
msgid "Success"
|
||||||
msgstr "Успешно"
|
msgstr "Успешно"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'{game_name}' was added to Steam. Please restart Steam for changes to "
|
"'{game_name}' was added to Steam. Please restart Steam for changes to take "
|
||||||
"take effect."
|
"effect."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите "
|
"'{game_name}' был(а) добавлен(а) в Steam. Пожалуйста, перезапустите Steam, "
|
||||||
"Steam, чтобы изменения вступили в силу."
|
"чтобы изменения вступили в силу."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Executable not found for game: {game_name}"
|
msgid "Executable not found for game: {game_name}"
|
||||||
@@ -123,10 +128,6 @@ msgstr "'{game_name}' был(а) добавлен(а) в избранное"
|
|||||||
msgid "Removed '{game_name}' from favorites"
|
msgid "Removed '{game_name}' from favorites"
|
||||||
msgstr "'{game_name}' был(а) удалён(а) из избранного"
|
msgstr "'{game_name}' был(а) удалён(а) из избранного"
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "start.sh not found at {path}"
|
|
||||||
msgstr "start.sh не найден по адресу {path}"
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launch game \"{name}\" with PortProton"
|
msgid "Launch game \"{name}\" with PortProton"
|
||||||
msgstr "Запустить игру \"{name}\" с помощью PortProton"
|
msgstr "Запустить игру \"{name}\" с помощью PortProton"
|
||||||
@@ -178,11 +179,11 @@ msgstr "Подтвердите удаление"
|
|||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure you want to delete '{game_name}'? This will remove the "
|
"Are you sure you want to delete '{game_name}'? This will remove the .desktop "
|
||||||
".desktop file and custom data."
|
"file and custom data."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению "
|
"Вы уверены, что хотите удалить '{game_name}'? Это приведёт к удалению файла ."
|
||||||
"файла .desktop и пользовательских данных."
|
"desktop и пользовательских данных."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to delete .desktop file: {error}"
|
msgid "Failed to delete .desktop file: {error}"
|
||||||
@@ -196,10 +197,6 @@ msgstr "'{game_name}' был(а) успешно удалён(а)"
|
|||||||
msgid "Failed to delete custom data: {error}"
|
msgid "Failed to delete custom data: {error}"
|
||||||
msgstr "Не удалось удалить пользовательские данные: {error}"
|
msgstr "Не удалось удалить пользовательские данные: {error}"
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{game_name}' successfully"
|
|
||||||
msgstr "'{game_name}' успешно добавлен(а)"
|
|
||||||
|
|
||||||
msgid "Game name and executable path are required"
|
msgid "Game name and executable path are required"
|
||||||
msgstr "Требуются название игры и путь к исполняемому файлу"
|
msgstr "Требуются название игры и путь к исполняемому файлу"
|
||||||
|
|
||||||
@@ -222,21 +219,17 @@ msgstr "Не удалось сохранить файл .desktop: {error}"
|
|||||||
msgid "Failed to copy cover image: {error}"
|
msgid "Failed to copy cover image: {error}"
|
||||||
msgstr "Не удалось скопировать обложку: {error}"
|
msgstr "Не удалось скопировать обложку: {error}"
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Unsupported image format: {extension}"
|
|
||||||
msgstr "Неподдерживаемый формат изображения: {extension}"
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to add '{game_name}' to Steam: {error}"
|
msgid "Failed to add '{game_name}' to Steam: {error}"
|
||||||
msgstr "Не удалось добавить '{game_name}' в Steam: {error}"
|
msgstr "Не удалось добавить '{game_name}' в Steam: {error}"
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'{game_name}' was removed from Steam. Please restart Steam for changes to"
|
"'{game_name}' was removed from Steam. Please restart Steam for changes to take "
|
||||||
" take effect."
|
"effect."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam,"
|
"'{game_name}' был(а) удалён(а) из Steam. Пожалуйста, перезапустите Steam, чтобы "
|
||||||
" чтобы изменения вступили в силу."
|
"изменения вступили в силу."
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Failed to remove game '{game_name}' from Steam: {error}"
|
msgid "Failed to remove game '{game_name}' from Steam: {error}"
|
||||||
@@ -263,43 +256,13 @@ msgstr "Удалить"
|
|||||||
msgid "Select All"
|
msgid "Select All"
|
||||||
msgstr "Выбрать всё"
|
msgstr "Выбрать всё"
|
||||||
|
|
||||||
msgid "Open"
|
|
||||||
msgstr "Открыть"
|
|
||||||
|
|
||||||
msgid "Select Dir"
|
|
||||||
msgstr "Выбрать папку"
|
|
||||||
|
|
||||||
msgid "Prev Dir"
|
|
||||||
msgstr "Предыдущий каталог"
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Отмена"
|
|
||||||
|
|
||||||
msgid "Toggle"
|
|
||||||
msgstr "Переключить"
|
|
||||||
|
|
||||||
msgid "Install"
|
|
||||||
msgstr "Установить"
|
|
||||||
|
|
||||||
msgid "Force Install"
|
|
||||||
msgstr "Принудительно установить"
|
|
||||||
|
|
||||||
msgid "Prev Tab"
|
|
||||||
msgstr "Предыдущая вкладка"
|
|
||||||
|
|
||||||
msgid "Next Tab"
|
|
||||||
msgstr "Следующая вкладка"
|
|
||||||
|
|
||||||
msgid "Save"
|
|
||||||
msgstr "Сохранить"
|
|
||||||
|
|
||||||
msgid "Search"
|
|
||||||
msgstr "Поиск"
|
|
||||||
|
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Launching {0}"
|
msgid "Launching {0}"
|
||||||
msgstr "Идёт запуск {0}"
|
msgstr "Идёт запуск {0}"
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Отмена"
|
||||||
|
|
||||||
msgid "File Explorer"
|
msgid "File Explorer"
|
||||||
msgstr "Проводник"
|
msgstr "Проводник"
|
||||||
|
|
||||||
@@ -311,7 +274,7 @@ msgstr "Путь: "
|
|||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Access denied: %s"
|
msgid "Access denied: %s"
|
||||||
msgstr "Доступ запрещён: %s"
|
msgstr "Доступ запрещен: %s"
|
||||||
|
|
||||||
msgid "Edit Game"
|
msgid "Edit Game"
|
||||||
msgstr "Редактировать игру"
|
msgstr "Редактировать игру"
|
||||||
@@ -331,9 +294,6 @@ msgstr "Обзор..."
|
|||||||
msgid "Custom Cover:"
|
msgid "Custom Cover:"
|
||||||
msgstr "Обложка:"
|
msgstr "Обложка:"
|
||||||
|
|
||||||
msgid "Enter local path or URL for cover image"
|
|
||||||
msgstr "Введите локальный путь или URL обложки"
|
|
||||||
|
|
||||||
msgid "Cover Preview:"
|
msgid "Cover Preview:"
|
||||||
msgstr "Предпросмотр обложки:"
|
msgstr "Предпросмотр обложки:"
|
||||||
|
|
||||||
@@ -352,78 +312,6 @@ msgstr "Скачивание обложки..."
|
|||||||
msgid "No cover selected"
|
msgid "No cover selected"
|
||||||
msgstr "Обложка не выбрана"
|
msgstr "Обложка не выбрана"
|
||||||
|
|
||||||
msgid "Prefix Manager"
|
|
||||||
msgstr "Менеджер префиксов"
|
|
||||||
|
|
||||||
msgid "Set"
|
|
||||||
msgstr "Выбор"
|
|
||||||
|
|
||||||
msgid "Libraries"
|
|
||||||
msgstr "Библиотеки"
|
|
||||||
|
|
||||||
msgid "Information"
|
|
||||||
msgstr "Описание"
|
|
||||||
|
|
||||||
msgid "Fonts"
|
|
||||||
msgstr "Шрифты"
|
|
||||||
|
|
||||||
msgid "Settings"
|
|
||||||
msgstr "Настройки"
|
|
||||||
|
|
||||||
msgid "Winetricks not found. Please try again."
|
|
||||||
msgstr "Winetricks не найден. Повторите попытку."
|
|
||||||
|
|
||||||
msgid "Warning"
|
|
||||||
msgstr "Предупреждение"
|
|
||||||
|
|
||||||
msgid "No components selected."
|
|
||||||
msgstr "Не выбрано ни одного компонента."
|
|
||||||
|
|
||||||
msgid "Installation failed. Check logs."
|
|
||||||
msgstr "Установка не удалась. Проверьте журналы."
|
|
||||||
|
|
||||||
msgid "Components installed successfully."
|
|
||||||
msgstr "Компоненты успешно установлены."
|
|
||||||
|
|
||||||
msgid "Exe Settings"
|
|
||||||
msgstr "Настройки EXE"
|
|
||||||
|
|
||||||
msgid "Search:"
|
|
||||||
msgstr "Поиск:"
|
|
||||||
|
|
||||||
msgid "Search settings..."
|
|
||||||
msgstr "Поиск настроек..."
|
|
||||||
|
|
||||||
msgid "Main"
|
|
||||||
msgstr "Основные"
|
|
||||||
|
|
||||||
msgid "Advanced"
|
|
||||||
msgstr "Расширенные"
|
|
||||||
|
|
||||||
msgid "Setting"
|
|
||||||
msgstr "Параметр"
|
|
||||||
|
|
||||||
msgid "Value"
|
|
||||||
msgstr "Значение"
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr "Описание"
|
|
||||||
|
|
||||||
msgid "disabled"
|
|
||||||
msgstr "отключено"
|
|
||||||
|
|
||||||
msgid "Info"
|
|
||||||
msgstr "Информация"
|
|
||||||
|
|
||||||
msgid "No changes to apply."
|
|
||||||
msgstr "Изменений для применения нет."
|
|
||||||
|
|
||||||
msgid "Failed to apply changes. Check logs."
|
|
||||||
msgstr "Не удалось применить изменения. Проверьте логи."
|
|
||||||
|
|
||||||
msgid "Settings updated successfully."
|
|
||||||
msgstr "Настройки успешно обновлены."
|
|
||||||
|
|
||||||
msgid "Loading Epic Games Store games..."
|
msgid "Loading Epic Games Store games..."
|
||||||
msgstr "Загрузка игр из Epic Games Store..."
|
msgstr "Загрузка игр из Epic Games Store..."
|
||||||
|
|
||||||
@@ -466,15 +354,15 @@ msgstr "В ожидании"
|
|||||||
msgid "Unknown Game"
|
msgid "Unknown Game"
|
||||||
msgstr "Неизвестная игра"
|
msgstr "Неизвестная игра"
|
||||||
|
|
||||||
msgid "Starting PortProton..."
|
|
||||||
msgstr "Инициализация PortProton"
|
|
||||||
|
|
||||||
msgid "Library"
|
msgid "Library"
|
||||||
msgstr "Библиотека"
|
msgstr "Библиотека"
|
||||||
|
|
||||||
msgid "Auto Install"
|
msgid "Auto Install"
|
||||||
msgstr "Автоустановка"
|
msgstr "Автоустановка"
|
||||||
|
|
||||||
|
msgid "Emulators"
|
||||||
|
msgstr "Эмуляторы"
|
||||||
|
|
||||||
msgid "Wine Settings"
|
msgid "Wine Settings"
|
||||||
msgstr "Настройки wine"
|
msgstr "Настройки wine"
|
||||||
|
|
||||||
@@ -490,31 +378,6 @@ msgstr "Назад"
|
|||||||
msgid "Fullscreen"
|
msgid "Fullscreen"
|
||||||
msgstr "Полный экран"
|
msgstr "Полный экран"
|
||||||
|
|
||||||
msgid "Refresh Grid"
|
|
||||||
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 "Game library refreshed"
|
|
||||||
msgstr "Игровая библиотека обновлена"
|
|
||||||
|
|
||||||
msgid "Loading Steam games..."
|
msgid "Loading Steam games..."
|
||||||
msgstr "Загрузка игр из Steam..."
|
msgstr "Загрузка игр из Steam..."
|
||||||
|
|
||||||
@@ -527,123 +390,14 @@ msgstr "Игровая библиотека"
|
|||||||
msgid "Find Games ..."
|
msgid "Find Games ..."
|
||||||
msgstr "Найти игры..."
|
msgstr "Найти игры..."
|
||||||
|
|
||||||
msgid "A refresh is already in progress..."
|
msgid "Here you can configure automatic game installation..."
|
||||||
msgstr "Обновление уже выполняется..."
|
msgstr "Здесь можно настроить автоматическую установку игр..."
|
||||||
|
|
||||||
msgid "Refreshing..."
|
msgid "List of available emulators and their configuration..."
|
||||||
msgstr "Обновление..."
|
msgstr "Список доступных эмуляторов и их настройка..."
|
||||||
|
|
||||||
msgid "Refreshing game library..."
|
msgid "Various Wine parameters and versions..."
|
||||||
msgstr "Обновление игровой библиотеки..."
|
msgstr "Различные параметры и версии wine..."
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Added '{name}'"
|
|
||||||
msgstr "'{name}' добавлен(а)"
|
|
||||||
|
|
||||||
msgid "Compatibility tool:"
|
|
||||||
msgstr "Инструмент совместимости:"
|
|
||||||
|
|
||||||
msgid "Prefix:"
|
|
||||||
msgstr "Префикс:"
|
|
||||||
|
|
||||||
msgid "Wine Configuration"
|
|
||||||
msgstr "Конфигурация Wine"
|
|
||||||
|
|
||||||
msgid "Registry Editor"
|
|
||||||
msgstr "Редактор реестра"
|
|
||||||
|
|
||||||
msgid "Command Prompt"
|
|
||||||
msgstr "Командная строка"
|
|
||||||
|
|
||||||
msgid "Uninstaller"
|
|
||||||
msgstr "Удаление программ"
|
|
||||||
|
|
||||||
msgid "Create Prefix Backup"
|
|
||||||
msgstr "Создать резервную копию префикса"
|
|
||||||
|
|
||||||
msgid "Load Prefix Backup"
|
|
||||||
msgstr "Загрузить резервную копию префикса"
|
|
||||||
|
|
||||||
msgid "Delete Compatibility Tool"
|
|
||||||
msgstr "Удалить Инструмент совместимости"
|
|
||||||
|
|
||||||
msgid "Delete Prefix"
|
|
||||||
msgstr "Удалить Префикс"
|
|
||||||
|
|
||||||
msgid "Clear Prefix"
|
|
||||||
msgstr "Очистить Префикс"
|
|
||||||
|
|
||||||
msgid "Launching tool..."
|
|
||||||
msgstr "Запуск инструмента..."
|
|
||||||
|
|
||||||
msgid "Failed to start process."
|
|
||||||
msgstr "Не удалось запустить процесс."
|
|
||||||
|
|
||||||
msgid "Confirm Clear"
|
|
||||||
msgstr "Подтвердите очистку"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to clear prefix '{}'?"
|
|
||||||
msgstr "Вы уверены, что хотите очистить префикс «{}»?"
|
|
||||||
|
|
||||||
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 "Не удалось запустить процесс резервного копирования."
|
|
||||||
|
|
||||||
msgid "Failed to start restore process."
|
|
||||||
msgstr "Не удалось запустить процесс восстановления."
|
|
||||||
|
|
||||||
msgid "Prefix backup completed."
|
|
||||||
msgstr "Резервное копирование префикса завершено."
|
|
||||||
|
|
||||||
msgid "Prefix backup failed."
|
|
||||||
msgstr "Сбой резервного копирования префикса."
|
|
||||||
|
|
||||||
msgid "Prefix restore completed."
|
|
||||||
msgstr "Восстановление префикса завершено."
|
|
||||||
|
|
||||||
msgid "Prefix restore failed."
|
|
||||||
msgstr "Восстановление префикса не удалось."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete prefix '{}'?"
|
|
||||||
msgstr "Вы уверены, что хотите удалить префикс «{}»?"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Prefix '{}' deleted."
|
|
||||||
msgstr "Префикс «{}» удален."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete prefix: {}"
|
|
||||||
msgstr "Не удалось удалить префикс: {}"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Are you sure you want to delete compatibility tool '{}'?"
|
|
||||||
msgstr "Вы уверены, что хотите удалить инструмент совместимости «{}»?"
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Compatibility tool '{}' deleted."
|
|
||||||
msgstr "Инструмент совместимости «{}» удален."
|
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Failed to delete compatibility tool: {}"
|
|
||||||
msgstr "Не удалось удалить инструмент совместимости: {}"
|
|
||||||
|
|
||||||
msgid "Main PortProton parameters..."
|
msgid "Main PortProton parameters..."
|
||||||
msgstr "Основные параметры PortProton..."
|
msgstr "Основные параметры PortProton..."
|
||||||
@@ -678,9 +432,6 @@ msgstr "все"
|
|||||||
msgid "Games Display Filter:"
|
msgid "Games Display Filter:"
|
||||||
msgstr "Фильтр игр:"
|
msgstr "Фильтр игр:"
|
||||||
|
|
||||||
msgid "Gamepad Type:"
|
|
||||||
msgstr "Тип геймпада:"
|
|
||||||
|
|
||||||
msgid "Proxy URL"
|
msgid "Proxy URL"
|
||||||
msgstr "Адрес прокси"
|
msgstr "Адрес прокси"
|
||||||
|
|
||||||
@@ -705,12 +456,6 @@ msgstr "Запуск приложения в полноэкранном режи
|
|||||||
msgid "Application Fullscreen Mode:"
|
msgid "Application Fullscreen Mode:"
|
||||||
msgstr "Режим полноэкранного отображения приложения:"
|
msgstr "Режим полноэкранного отображения приложения:"
|
||||||
|
|
||||||
msgid "Minimize to tray on close"
|
|
||||||
msgstr "Сворачивать в трей при закрытии"
|
|
||||||
|
|
||||||
msgid "Application Close Mode:"
|
|
||||||
msgstr "Режим закрытия приложения:"
|
|
||||||
|
|
||||||
msgid "Auto Fullscreen on Gamepad connected"
|
msgid "Auto Fullscreen on Gamepad connected"
|
||||||
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
msgstr "Режим полноэкранного отображения приложения при подключении геймпада"
|
||||||
|
|
||||||
@@ -737,8 +482,7 @@ msgstr "Подтвердите удаление"
|
|||||||
|
|
||||||
msgid "Are you sure you want to reset all settings? This action cannot be undone."
|
msgid "Are you sure you want to reset all settings? This action cannot be undone."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя "
|
"Вы уверены, что хотите сбросить все настройки? Это действие нельзя отменить."
|
||||||
"отменить."
|
|
||||||
|
|
||||||
msgid "Settings reset. Restarting..."
|
msgid "Settings reset. Restarting..."
|
||||||
msgstr "Настройки сброшены. Перезапуск..."
|
msgstr "Настройки сброшены. Перезапуск..."
|
||||||
@@ -787,10 +531,6 @@ msgstr "Тема '{0}' применена успешно"
|
|||||||
msgid "Error applying theme '{0}'"
|
msgid "Error applying theme '{0}'"
|
||||||
msgstr "Ошибка при применение темы '{0}'"
|
msgstr "Ошибка при применение темы '{0}'"
|
||||||
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Executable not found: {0}"
|
|
||||||
msgstr "Исполняемый файл не найден: {0}"
|
|
||||||
|
|
||||||
msgid "LAST LAUNCH"
|
msgid "LAST LAUNCH"
|
||||||
msgstr "Последний запуск"
|
msgstr "Последний запуск"
|
||||||
|
|
||||||
@@ -849,329 +589,6 @@ msgstr "Неправильный формат команды (flatpak)"
|
|||||||
msgid "File not found: {0}"
|
msgid "File not found: {0}"
|
||||||
msgstr "Файл не найден: {0}"
|
msgstr "Файл не найден: {0}"
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Using FPS and system load monitoring (Turns on and off by the key "
|
|
||||||
"combination - right Shift + F12)"
|
|
||||||
msgstr ""
|
|
||||||
"Использование мониторинга FPS и нагрузки системы (включается и "
|
|
||||||
"выключается комбинацией клавиш - правая Shift + F12)"
|
|
||||||
|
|
||||||
msgid "Forced use of MANGOHUD system settings (GOverlay, etc.)"
|
|
||||||
msgstr "Принудительное использование системных настроек MANGOHUD (GOverlay и т.д.)"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable vkBasalt by default to improve graphics in games running on "
|
|
||||||
"Vulkan. (The HOME hotkey disables vkbasalt)"
|
|
||||||
msgstr ""
|
|
||||||
"Включить vkBasalt по умолчанию для улучшения графики в играх на Vulkan. "
|
|
||||||
"(Горячая клавиша HOME отключает vkbasalt)"
|
|
||||||
|
|
||||||
msgid "Forced use of VKBASALT system settings (GOverlay, etc.)"
|
|
||||||
msgstr "Принудительное использование системных настроек VKBASALT (GOverlay и т.д.)"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, "
|
|
||||||
"DirectDraw 1-7, Direct3D 2-9) on all 3D API."
|
|
||||||
msgstr ""
|
|
||||||
"Включить dgVoodoo2. Принудительное использование всех библиотек dgVoodoo2"
|
|
||||||
" (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) на всех 3D API."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Super + F : Toggle fullscreen\n"
|
|
||||||
"Super + N : Toggle nearest neighbour filtering\n"
|
|
||||||
"Super + U : Toggle FSR upscaling\n"
|
|
||||||
"Super + Y : Toggle NIS upscaling\n"
|
|
||||||
"Super + I : Increase FSR sharpness by 1\n"
|
|
||||||
"Super + O : Decrease FSR sharpness by 1\n"
|
|
||||||
"Super + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\n"
|
|
||||||
"Super + G : Toggle keyboard grab\n"
|
|
||||||
"Super + C : Update clipboard"
|
|
||||||
msgstr ""
|
|
||||||
"Super + F: Переключить полноэкранный режим\n"
|
|
||||||
"Super + N: Переключить фильтрацию ближайшего соседа\n"
|
|
||||||
"Super + U: Переключить апскейлинг FSR\n"
|
|
||||||
"Super + Y: Переключить апскейлинг NIS\n"
|
|
||||||
"Super + I: Увеличить резкость FSR на 1\n"
|
|
||||||
"Super + O: Уменьшить резкость FSR на 1\n"
|
|
||||||
"Super + S: Сделать скриншот (сейчас сохраняется в "
|
|
||||||
"/tmp/gamescope_DATE.png)\n"
|
|
||||||
"Super + G: Переключить захват клавиатуры\n"
|
|
||||||
"Super + C: Обновить буфер обмена"
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization primitives based on eventfd."
|
|
||||||
msgstr "Включить примитивы синхронизации в процессе на основе eventfd."
|
|
||||||
|
|
||||||
msgid "Enable futex-based in-process synchronization primitives."
|
|
||||||
msgstr "Включить примитивы синхронизации в процессе на основе futex."
|
|
||||||
|
|
||||||
msgid "Enable in-process synchronization via the Linux ntsync driver."
|
|
||||||
msgstr "Включить синхронизацию в процессе через драйвер ntsync в Linux."
|
|
||||||
|
|
||||||
msgid "Enable vkd3d support - Ray Tracing"
|
|
||||||
msgstr "Включить поддержку vkd3d — трассировка лучей"
|
|
||||||
|
|
||||||
msgid "Enable DLSS on supported NVIDIA graphics cards"
|
|
||||||
msgstr "Включить DLSS на поддерживаемых видеокартах NVIDIA"
|
|
||||||
|
|
||||||
msgid "Enable OptiScaler (replacement upscaler / frame generator)"
|
|
||||||
msgstr "Включить OptiScaler (замена апскейлера / генератора кадров)"
|
|
||||||
|
|
||||||
msgid "Enable Lossless Scaling frame generation (experimental)"
|
|
||||||
msgstr "Включить генерацию кадров Lossless Scaling (экспериментально)"
|
|
||||||
|
|
||||||
msgid "FSR upscaling in fullscreen with ProtonGE below native resolution"
|
|
||||||
msgstr "Апскейлинг FSR в полноэкранном режиме с ProtonGE ниже родного разрешения"
|
|
||||||
|
|
||||||
msgid "Disguise all NVIDIA GPU features"
|
|
||||||
msgstr "Маскировать все функции GPU NVIDIA"
|
|
||||||
|
|
||||||
msgid "Run the application in WINE virtual desktop"
|
|
||||||
msgstr "Запускать приложение в виртуальном рабочем столе WINE"
|
|
||||||
|
|
||||||
msgid "Run the application in a terminal"
|
|
||||||
msgstr "Запускать приложение в терминале"
|
|
||||||
|
|
||||||
msgid "Use system GameMode for performance optimization"
|
|
||||||
msgstr "Использовать системный GameMode для оптимизации производительности"
|
|
||||||
|
|
||||||
msgid "Enable forced use of third-party DirectX libraries"
|
|
||||||
msgstr "Включить принудительное использование сторонних библиотек DirectX"
|
|
||||||
|
|
||||||
msgid "Fix pink-tinted video playback in some games"
|
|
||||||
msgstr "Исправить розовый оттенок видео в некоторых играх"
|
|
||||||
|
|
||||||
msgid "Reduce PulseAudio latency to fix intermittent sound"
|
|
||||||
msgstr "Уменьшить задержку PulseAudio для исправления прерывистого звука"
|
|
||||||
|
|
||||||
msgid "Force US keyboard layout"
|
|
||||||
msgstr "Принудительно использовать раскладку клавиатуры US"
|
|
||||||
|
|
||||||
msgid "Use GStreamer for in-game clips (WMF support)"
|
|
||||||
msgstr "Использовать GStreamer для внутриигровых клипов (поддержка WMF)"
|
|
||||||
|
|
||||||
msgid "Use WINE shader caching"
|
|
||||||
msgstr "Использовать кэширование шейдеров WINE"
|
|
||||||
|
|
||||||
msgid "Force use of built-in DXGI library"
|
|
||||||
msgstr "Принудительно использовать встроенную библиотеку DXGI"
|
|
||||||
|
|
||||||
msgid "Enable Easy Anti-Cheat and BattlEye runtimes"
|
|
||||||
msgstr "Включить среды выполнения Easy Anti-Cheat и BattlEye"
|
|
||||||
|
|
||||||
msgid "Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"
|
|
||||||
msgstr "Использовать системные слои Vulkan (MangoHud, vkBasalt, OBS и т.д.)"
|
|
||||||
|
|
||||||
msgid "Enable OBS Studio capture via obs-vkcapture"
|
|
||||||
msgstr "Включить захват OBS Studio через obs-vkcapture"
|
|
||||||
|
|
||||||
msgid "Disable desktop compositing for performance"
|
|
||||||
msgstr "Отключить композицию рабочего стола для производительности"
|
|
||||||
|
|
||||||
msgid "Use container launch mode (recommended default)"
|
|
||||||
msgstr "Использовать режим запуска в контейнере (рекомендуемый по умолчанию)"
|
|
||||||
|
|
||||||
msgid "Force DirectInput protocol instead of XInput"
|
|
||||||
msgstr "Принудительно использовать протокол DirectInput вместо XInput"
|
|
||||||
|
|
||||||
msgid "Enable experimental native Wayland support"
|
|
||||||
msgstr "Включить экспериментальную нативную поддержку Wayland"
|
|
||||||
|
|
||||||
msgid "Enable HDR settings under native Wayland"
|
|
||||||
msgstr "Включить настройки HDR под нативным Wayland"
|
|
||||||
|
|
||||||
msgid "Use Gallium Zink (OpenGL via Vulkan)"
|
|
||||||
msgstr "Использовать Gallium Zink (OpenGL через Vulkan)"
|
|
||||||
|
|
||||||
msgid "Use Gallium Nine (native DirectX 9 for Mesa)"
|
|
||||||
msgstr "Использовать Gallium Nine (нативный DirectX 9 для Mesa)"
|
|
||||||
|
|
||||||
msgid "Use WineD3D Vulkan backend (Damavand)"
|
|
||||||
msgstr "Использовать бэкенд Vulkan WineD3D (Damavand)"
|
|
||||||
|
|
||||||
msgid "Use bundled dxvk/vkd3d from Wine/Proton"
|
|
||||||
msgstr "Использовать встроенные dxvk/vkd3d из Wine/Proton"
|
|
||||||
|
|
||||||
msgid "Use async dxvk-sarek (experimental)"
|
|
||||||
msgstr "Использовать асинхронный dxvk-sarek (экспериментально)"
|
|
||||||
|
|
||||||
msgid "Wine Version"
|
|
||||||
msgstr "Версия Wine"
|
|
||||||
|
|
||||||
msgid "Select the Wine or Proton version to use for this executable."
|
|
||||||
msgstr "Выбор версии Wine или Proton для использования с этим исполняемым файлом."
|
|
||||||
|
|
||||||
msgid "Prefix Name"
|
|
||||||
msgstr "Имя префикса"
|
|
||||||
|
|
||||||
msgid "Specify the Wine prefix to run this game with"
|
|
||||||
msgstr "Укажите префикс Wine для запуска этой игры"
|
|
||||||
|
|
||||||
msgid "Newest"
|
|
||||||
msgstr "Новейший"
|
|
||||||
|
|
||||||
msgid "Stable"
|
|
||||||
msgstr "Стабильный"
|
|
||||||
|
|
||||||
msgid "Vulkan Backend"
|
|
||||||
msgstr "Vulkan рендеринг"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select the DirectX → Vulkan/OpenGL backend:\n"
|
|
||||||
"\n"
|
|
||||||
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires "
|
|
||||||
"modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
|
||||||
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ "
|
|
||||||
"driver)\n"
|
|
||||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, "
|
|
||||||
"Vulkan 1.1+)\n"
|
|
||||||
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
|
||||||
msgstr ""
|
|
||||||
"Выберите бэкэнд DirectX → Vulkan/OpenGL:\n"
|
|
||||||
"\n"
|
|
||||||
"• Новейший — последние версии DXVK + VKD3D (наилучшая "
|
|
||||||
"совместимость/производительность, требует современных драйверов: AMD Mesa"
|
|
||||||
" 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
|
||||||
"• Стабильный — более старая, хорошо протестированная версия DXVK + VKD3D "
|
|
||||||
"(работает с любыми драйверами Vulkan 1.3+)\n"
|
|
||||||
"• Sarek — экспериментальная версия DXVK-Sarek + VKD3D-Sarek (поддерживает"
|
|
||||||
" более старые драйверы, Vulkan 1.1+)\n"
|
|
||||||
"• WINED3D — резервный вариант OpenGL (наименьшая производительность, "
|
|
||||||
"используйте только в случае сбоя других вариантов)"
|
|
||||||
|
|
||||||
msgid "Windows version"
|
|
||||||
msgstr "Версия Windows"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Changing the WINDOWS emulation version may be required to run older "
|
|
||||||
"games. WINDOWS versions below 10 do not support new games with DirectX 12"
|
|
||||||
msgstr ""
|
|
||||||
"Изменение версии эмуляции WINDOWS может потребоваться для запуска старых "
|
|
||||||
"игр. Версии WINDOWS ниже 10 не поддерживают новые игры с DirectX 12"
|
|
||||||
|
|
||||||
msgid "DLL Overrides"
|
|
||||||
msgstr "Переопределения DLL"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Forced to use/disable the library only for the given application.\n"
|
|
||||||
"\n"
|
|
||||||
"A brief instruction:\n"
|
|
||||||
"* libraries are written WITHOUT the .dll file extension\n"
|
|
||||||
"* libraries are separated by semicolons - ;\n"
|
|
||||||
"* library=n - use the WINDOWS (third-party) library\n"
|
|
||||||
"* library=b - use WINE (built-in) library\n"
|
|
||||||
"* library=n,b - use WINDOWS library and then WINE\n"
|
|
||||||
"* library=b,n - use WINE library and then WINDOWS\n"
|
|
||||||
"* library= - disable the use of this library\n"
|
|
||||||
"\n"
|
|
||||||
"Example: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
|
||||||
msgstr ""
|
|
||||||
"Принудительное использование/отключение библиотеки только для данного "
|
|
||||||
"приложения.\n"
|
|
||||||
"\n"
|
|
||||||
"Краткая инструкция:\n"
|
|
||||||
"* библиотеки пишутся БЕЗ расширения .dll\n"
|
|
||||||
"* библиотеки разделяются точкой с запятой - ;\n"
|
|
||||||
"* library=n — использовать библиотеку WINDOWS (стороннюю)\n"
|
|
||||||
"* library=b — использовать библиотеку WINE (встроенную)\n"
|
|
||||||
"* library=n,b — использовать библиотеку WINDOWS, затем WINE\n"
|
|
||||||
"* library=b,n — использовать библиотеку WINE, затем WINDOWS\n"
|
|
||||||
"* library= — отключить использование этой библиотеки\n"
|
|
||||||
"\n"
|
|
||||||
"Пример: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"
|
|
||||||
|
|
||||||
msgid "Launch Arguments"
|
|
||||||
msgstr "Аргументы запуска"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Adding an argument after the .exe file, just like you would add an "
|
|
||||||
"argument in a shortcut on a WINDOWS system.\n"
|
|
||||||
"\n"
|
|
||||||
"Example: -dx11 -skipintro 1"
|
|
||||||
msgstr ""
|
|
||||||
"Добавление аргумента после файла .exe, как вы бы добавили аргумент в "
|
|
||||||
"ярлыке на системе WINDOWS.\n"
|
|
||||||
"\n"
|
|
||||||
"Пример: -dx11 -skipintro 1"
|
|
||||||
|
|
||||||
msgid "CPU Cores Limit"
|
|
||||||
msgstr "Ограничение ядер CPU"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Limiting the number of CPU cores is useful for Unity games (It is "
|
|
||||||
"recommended to set the value equal to 8)"
|
|
||||||
msgstr ""
|
|
||||||
"Ограничение количества ядер CPU полезно для игр на Unity (рекомендуется "
|
|
||||||
"установить значение равным 8)"
|
|
||||||
|
|
||||||
msgid "OpenGL Version"
|
|
||||||
msgstr "Версия OpenGL"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"You can select the required OpenGL version, some games require a forced "
|
|
||||||
"Compatibility Profile (COMP)."
|
|
||||||
msgstr ""
|
|
||||||
"Вы можете выбрать требуемую версию OpenGL, некоторые игры требуют "
|
|
||||||
"принудительного профиля совместимости (COMP)."
|
|
||||||
|
|
||||||
msgid "VKD3D Feature Level"
|
|
||||||
msgstr "Уровень возможностей VKD3D"
|
|
||||||
|
|
||||||
msgid "You can set a forced feature level VKD3D for games on DirectX12"
|
|
||||||
msgstr ""
|
|
||||||
"Вы можете установить принудительный уровень возможностей VKD3D для игр на"
|
|
||||||
" DirectX12"
|
|
||||||
|
|
||||||
msgid "Locale"
|
|
||||||
msgstr "Локаль"
|
|
||||||
|
|
||||||
msgid "Force certain locale for an app. Fixes encoding issues in legacy software"
|
|
||||||
msgstr ""
|
|
||||||
"Принудительно установить определённую локаль для приложения. Исправляет "
|
|
||||||
"проблемы с кодировкой в устаревшем ПО"
|
|
||||||
|
|
||||||
msgid "Window Mode"
|
|
||||||
msgstr "Режим окна"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Window mode (for Vulkan and OpenGL):\n"
|
|
||||||
"fifo - First in, first out. Limits the frame rate + no tearing. (VSync)\n"
|
|
||||||
"immediate - Unlimited frame rate + tearing.\n"
|
|
||||||
"mailbox - Triple buffering. Unlimited frame rate + no tearing.\n"
|
|
||||||
"relaxed - Same as fifo but allows tearing when below the monitors refresh"
|
|
||||||
" rate."
|
|
||||||
msgstr ""
|
|
||||||
"Режим окна (для Vulkan и OpenGL):\n"
|
|
||||||
"fifo — Первый вошёл, первый вышел. Ограничивает частоту кадров + без "
|
|
||||||
"разрывов. (VSync)\n"
|
|
||||||
"immediate — Неограниченная частота кадров + разрывы.\n"
|
|
||||||
"mailbox — Трёхбуферная. Неограниченная частота кадров + без разрывов.\n"
|
|
||||||
"relaxed — То же, что fifo, но позволяет разрывы при частоте ниже частоты "
|
|
||||||
"обновления монитора."
|
|
||||||
|
|
||||||
msgid "AMD Vulkan Driver"
|
|
||||||
msgstr "Драйвер Vulkan AMD"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Select needed AMD vulkan implementation. Choosing which implementation of"
|
|
||||||
" vulkan will be used to run the game"
|
|
||||||
msgstr ""
|
|
||||||
"Выберите нужную реализацию Vulkan AMD. Выбор, какая реализация Vulkan "
|
|
||||||
"будет использоваться для запуска игры"
|
|
||||||
|
|
||||||
msgid "NUMA Node"
|
|
||||||
msgstr "Узел NUMA"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"NUMA node for CPU affinity. In multi-core systems, CPUs are split into "
|
|
||||||
"NUMA nodes, each with its own local memory and cores. Binding a game to a"
|
|
||||||
" single node reduces memory-access latency and limits costly core-to-core"
|
|
||||||
" switches."
|
|
||||||
msgstr ""
|
|
||||||
"Узел NUMA для аффинности CPU. В многоядерных системах CPU разделены на "
|
|
||||||
"узлы NUMA, каждый со своей локальной памятью и ядрами. Привязка игры к "
|
|
||||||
"одному узлу уменьшает задержку доступа к памяти и ограничивает "
|
|
||||||
"дорогостоящие переключения между ядрами."
|
|
||||||
|
|
||||||
msgid "Reboot"
|
msgid "Reboot"
|
||||||
msgstr "Перезагрузить"
|
msgstr "Перезагрузить"
|
||||||
|
|
||||||
@@ -1237,4 +654,3 @@ msgstr "Нет избранных"
|
|||||||
|
|
||||||
msgid "No recent games"
|
msgid "No recent games"
|
||||||
msgstr "Нет недавних игр"
|
msgstr "Нет недавних игр"
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,12 @@ import orjson
|
|||||||
import requests
|
import requests
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import time
|
import time
|
||||||
import glob
|
|
||||||
import re
|
|
||||||
import hashlib
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from PySide6.QtCore import QThread, Signal
|
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.logger import get_logger
|
from portprotonqt.logger import get_logger
|
||||||
from portprotonqt.config_utils import get_portproton_location
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
|
||||||
AUTOINSTALL_CACHE_DURATION = 3600 # 1 hour for autoinstall cache
|
|
||||||
|
|
||||||
def normalize_name(s):
|
def normalize_name(s):
|
||||||
"""
|
"""
|
||||||
@@ -58,11 +52,7 @@ class PortProtonAPI:
|
|||||||
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
self.xdg_data_home = os.getenv("XDG_DATA_HOME", os.path.join(os.path.expanduser("~"), ".local", "share"))
|
||||||
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
self.custom_data_dir = os.path.join(self.xdg_data_home, "PortProtonQt", "custom_data")
|
||||||
os.makedirs(self.custom_data_dir, exist_ok=True)
|
os.makedirs(self.custom_data_dir, exist_ok=True)
|
||||||
self.portproton_location = get_portproton_location()
|
|
||||||
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
|
|
||||||
self._topics_data = None
|
self._topics_data = None
|
||||||
self._autoinstall_cache = None # New: In-memory cache
|
|
||||||
|
|
||||||
def _get_game_dir(self, exe_name: str) -> str:
|
def _get_game_dir(self, exe_name: str) -> str:
|
||||||
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
game_dir = os.path.join(self.custom_data_dir, exe_name)
|
||||||
@@ -78,6 +68,40 @@ class PortProtonAPI:
|
|||||||
logger.debug(f"Failed to check file at {url}: {e}")
|
logger.debug(f"Failed to check file at {url}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def download_game_assets(self, exe_name: str, timeout: int = 5) -> dict[str, str | None]:
|
||||||
|
game_dir = self._get_game_dir(exe_name)
|
||||||
|
results: dict[str, str | None] = {"cover": None, "metadata": None}
|
||||||
|
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
||||||
|
cover_url_base = f"{self.base_url}/{exe_name}/cover"
|
||||||
|
metadata_url = f"{self.base_url}/{exe_name}/metadata.txt"
|
||||||
|
|
||||||
|
for ext in cover_extensions:
|
||||||
|
cover_url = f"{cover_url_base}{ext}"
|
||||||
|
if self._check_file_exists(cover_url, timeout):
|
||||||
|
local_cover_path = os.path.join(game_dir, f"cover{ext}")
|
||||||
|
result = self.downloader.download(cover_url, local_cover_path, timeout=timeout)
|
||||||
|
if result:
|
||||||
|
results["cover"] = result
|
||||||
|
logger.info(f"Downloaded cover for {exe_name} to {result}")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to download cover for {exe_name} from {cover_url}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"No cover found for {exe_name} with extension {ext}")
|
||||||
|
|
||||||
|
if self._check_file_exists(metadata_url, timeout):
|
||||||
|
local_metadata_path = os.path.join(game_dir, "metadata.txt")
|
||||||
|
result = self.downloader.download(metadata_url, local_metadata_path, timeout=timeout)
|
||||||
|
if result:
|
||||||
|
results["metadata"] = result
|
||||||
|
logger.info(f"Downloaded metadata for {exe_name} to {result}")
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to download metadata for {exe_name} from {metadata_url}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"No metadata found for {exe_name}")
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None:
|
def download_game_assets_async(self, exe_name: str, timeout: int = 5, callback: Callable[[dict[str, str | None]], None] | None = None) -> None:
|
||||||
game_dir = self._get_game_dir(exe_name)
|
game_dir = self._get_game_dir(exe_name)
|
||||||
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
cover_extensions = [".png", ".jpg", ".jpeg", ".bmp"]
|
||||||
@@ -139,247 +163,6 @@ class PortProtonAPI:
|
|||||||
if callback:
|
if callback:
|
||||||
callback(results)
|
callback(results)
|
||||||
|
|
||||||
def download_autoinstall_cover_async(self, exe_name: str, timeout: int = 5, callback: Callable[[str | None], None] | None = None) -> None:
|
|
||||||
"""Download only autoinstall cover image (PNG only, no metadata)."""
|
|
||||||
xdg_data_home = os.getenv("XDG_DATA_HOME",
|
|
||||||
os.path.join(os.path.expanduser("~"), ".local", "share"))
|
|
||||||
autoinstall_root = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", "autoinstall")
|
|
||||||
user_game_folder = os.path.join(autoinstall_root, exe_name)
|
|
||||||
|
|
||||||
if not os.path.isdir(user_game_folder):
|
|
||||||
try:
|
|
||||||
os.mkdir(user_game_folder)
|
|
||||||
except FileExistsError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
cover_url = f"{self.base_url}/{exe_name}/cover.png"
|
|
||||||
local_cover_path = os.path.join(user_game_folder, "cover.png")
|
|
||||||
|
|
||||||
def on_cover_downloaded(local_path: str | None):
|
|
||||||
if local_path:
|
|
||||||
logger.info(f"Async autoinstall cover downloaded for {exe_name}: {local_path}")
|
|
||||||
else:
|
|
||||||
logger.debug(f"No autoinstall cover downloaded for {exe_name}")
|
|
||||||
if callback:
|
|
||||||
callback(local_path)
|
|
||||||
|
|
||||||
if self._check_file_exists(cover_url, timeout):
|
|
||||||
self.downloader.download_async(
|
|
||||||
cover_url,
|
|
||||||
local_cover_path,
|
|
||||||
timeout=timeout,
|
|
||||||
callback=on_cover_downloaded
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
logger.debug(f"No autoinstall cover found for {exe_name}")
|
|
||||||
if callback:
|
|
||||||
callback(None)
|
|
||||||
|
|
||||||
def parse_autoinstall_script(self, file_path: str) -> tuple[str | None, str | None]:
|
|
||||||
"""Extract display_name from # name comment and exe_name from autoinstall bash script."""
|
|
||||||
try:
|
|
||||||
with open(file_path, encoding='utf-8') as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# Skip emulators
|
|
||||||
if re.search(r'#\s*type\s*:\s*emulators', content, re.IGNORECASE):
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
display_name = None
|
|
||||||
exe_name = None
|
|
||||||
|
|
||||||
# Extract display_name from "# name:" comment
|
|
||||||
name_match = re.search(r'#\s*name\s*:\s*(.+)', content, re.IGNORECASE)
|
|
||||||
if name_match:
|
|
||||||
display_name = name_match.group(1).strip()
|
|
||||||
|
|
||||||
# --- pw_create_unique_exe ---
|
|
||||||
pw_match = re.search(r'pw_create_unique_exe(?:\s+["\']([^"\']+)["\'])?', content)
|
|
||||||
if pw_match:
|
|
||||||
arg = pw_match.group(1)
|
|
||||||
if arg:
|
|
||||||
exe_name = arg.strip()
|
|
||||||
if not exe_name.lower().endswith(".exe"):
|
|
||||||
exe_name += ".exe"
|
|
||||||
else:
|
|
||||||
export_match = re.search(
|
|
||||||
r'export\s+PORTWINE_CREATE_SHORTCUT_NAME\s*=\s*["\']([^"\']+)["\']',
|
|
||||||
content, re.IGNORECASE)
|
|
||||||
if export_match:
|
|
||||||
exe_name = f"{export_match.group(1).strip()}.exe"
|
|
||||||
|
|
||||||
else:
|
|
||||||
portwine_match = None
|
|
||||||
for line in content.splitlines():
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith("#"):
|
|
||||||
continue
|
|
||||||
if "portwine_exe" in stripped and "=" in stripped:
|
|
||||||
portwine_match = stripped
|
|
||||||
break
|
|
||||||
|
|
||||||
if portwine_match:
|
|
||||||
exe_expr = portwine_match.split("=", 1)[1].strip().strip("'\" ")
|
|
||||||
exe_candidates = re.findall(r'[-\w\s/\\\.]+\.exe', exe_expr)
|
|
||||||
if exe_candidates:
|
|
||||||
exe_name = os.path.basename(exe_candidates[-1].strip())
|
|
||||||
|
|
||||||
|
|
||||||
# Fallback
|
|
||||||
if not display_name and exe_name:
|
|
||||||
display_name = exe_name
|
|
||||||
|
|
||||||
return display_name, exe_name
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to parse {file_path}: {e}")
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
def _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:
|
|
||||||
# Add timeout protection for file operations
|
|
||||||
start_time = time.time()
|
|
||||||
with open(cache_file, "rb") as f:
|
|
||||||
data = orjson.loads(f.read())
|
|
||||||
# Check signature
|
|
||||||
cached_signature = data.get("scripts_signature", "")
|
|
||||||
current_signature = self._compute_scripts_signature(
|
|
||||||
os.path.join(self.portproton_location or "", "data", "scripts", "pw_autoinstall")
|
|
||||||
)
|
|
||||||
# Check for timeout during signature computation
|
|
||||||
if time.time() - start_time > 3: # 3 second timeout
|
|
||||||
logger.warning("Cache loading took too long, skipping cache")
|
|
||||||
return None
|
|
||||||
if cached_signature != current_signature:
|
|
||||||
logger.info("Scripts signature mismatch; invalidating cache")
|
|
||||||
return None
|
|
||||||
self._autoinstall_cache = data["games"]
|
|
||||||
logger.info(f"Loaded {len(self._autoinstall_cache)} cached autoinstall games")
|
|
||||||
return self._autoinstall_cache
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to load autoinstall cache: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
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."""
|
|
||||||
class AutoinstallWorker(QThread):
|
|
||||||
finished = Signal(list)
|
|
||||||
api: "PortProtonAPI"
|
|
||||||
portproton_location: str | None
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
import time
|
|
||||||
# Check cache in this background thread, not in main thread
|
|
||||||
start_time = time.time()
|
|
||||||
cached_games = self.api._load_autoinstall_cache()
|
|
||||||
# If cache loading took too long (>2 seconds), skip cache and load directly
|
|
||||||
if time.time() - start_time > 2:
|
|
||||||
logger.warning("Cache loading took too long, proceeding without cache")
|
|
||||||
cached_games = None
|
|
||||||
|
|
||||||
if cached_games is not None:
|
|
||||||
self.finished.emit(cached_games)
|
|
||||||
return
|
|
||||||
|
|
||||||
# No cache: Load games from scratch
|
|
||||||
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):
|
def _load_topics_data(self):
|
||||||
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
"""Load and cache linux_gaming_topics_min.json from the archive."""
|
||||||
if self._topics_data is not None:
|
if self._topics_data is not None:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from PySide6.QtCore import QRect
|
|||||||
from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient
|
from PySide6.QtGui import QPainter, QPen, QBrush, Qt, QColor, QConicalGradient
|
||||||
from PySide6.QtWidgets import QWidget
|
from PySide6.QtWidgets import QWidget
|
||||||
|
|
||||||
|
|
||||||
class Preloader(QWidget):
|
class Preloader(QWidget):
|
||||||
def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None):
|
def __init__(self, speed=180.0, line_line_width=20, color=QColor(0, 120, 215), parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|||||||
@@ -1,379 +0,0 @@
|
|||||||
"""
|
|
||||||
Utility module for search optimizations including Trie, hash tables, and fuzzy matching.
|
|
||||||
"""
|
|
||||||
import concurrent.futures
|
|
||||||
import threading
|
|
||||||
from collections.abc import Callable
|
|
||||||
from typing import Any
|
|
||||||
from rapidfuzz import fuzz
|
|
||||||
from threading import Lock
|
|
||||||
from portprotonqt.logger import get_logger
|
|
||||||
from PySide6.QtCore import QThread, QRunnable, Signal, QObject, QTimer
|
|
||||||
import requests
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
class TrieNode:
|
|
||||||
"""Node in the Trie data structure."""
|
|
||||||
def __init__(self):
|
|
||||||
self.children = {}
|
|
||||||
self.is_end_word = False
|
|
||||||
self.payload = None # Store the original data in leaf nodes
|
|
||||||
|
|
||||||
class Trie:
|
|
||||||
"""Trie data structure for efficient prefix-based searching."""
|
|
||||||
def __init__(self):
|
|
||||||
self.root = TrieNode()
|
|
||||||
self._lock = Lock() # Thread safety for concurrent access
|
|
||||||
|
|
||||||
def insert(self, key: str, payload: Any):
|
|
||||||
"""Insert a key with payload into the Trie."""
|
|
||||||
with self._lock:
|
|
||||||
node = self.root
|
|
||||||
for char in key.lower():
|
|
||||||
if char not in node.children:
|
|
||||||
node.children[char] = TrieNode()
|
|
||||||
node = node.children[char]
|
|
||||||
node.is_end_word = True
|
|
||||||
node.payload = payload
|
|
||||||
|
|
||||||
def search_prefix(self, prefix: str) -> list[tuple[str, Any]]:
|
|
||||||
"""Find all entries with the given prefix."""
|
|
||||||
with self._lock:
|
|
||||||
node = self.root
|
|
||||||
for char in prefix.lower():
|
|
||||||
if char not in node.children:
|
|
||||||
return []
|
|
||||||
node = node.children[char]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
self._collect_all(node, prefix.lower(), results)
|
|
||||||
return results
|
|
||||||
|
|
||||||
def _collect_all(self, node: TrieNode, current_prefix: str, results: list[tuple[str, Any]]):
|
|
||||||
"""Collect all entries from the current node."""
|
|
||||||
if node.is_end_word:
|
|
||||||
results.append((current_prefix, node.payload))
|
|
||||||
|
|
||||||
for char, child_node in node.children.items():
|
|
||||||
self._collect_all(child_node, current_prefix + char, results)
|
|
||||||
|
|
||||||
class FuzzySearchIndex:
|
|
||||||
"""Index for fuzzy string matching with rapidfuzz."""
|
|
||||||
def __init__(self, items: list[tuple[str, Any]] | None = None):
|
|
||||||
self.items: list[tuple[str, Any]] = items or []
|
|
||||||
self.normalized_items: list[tuple[str, Any]] = []
|
|
||||||
self._lock = Lock()
|
|
||||||
self._build_normalized_index()
|
|
||||||
|
|
||||||
def _build_normalized_index(self):
|
|
||||||
"""Build a normalized index for fuzzy matching."""
|
|
||||||
with self._lock:
|
|
||||||
self.normalized_items = [(self._normalize(item[0]), item[1]) for item in self.items]
|
|
||||||
|
|
||||||
def _normalize(self, s: str) -> str:
|
|
||||||
"""Normalize string for fuzzy matching."""
|
|
||||||
s = s.lower()
|
|
||||||
for ch in ["™", "®"]:
|
|
||||||
s = s.replace(ch, "")
|
|
||||||
for ch in ["-", ":", ","]:
|
|
||||||
s = s.replace(ch, " ")
|
|
||||||
s = " ".join(s.split())
|
|
||||||
for suffix in ["bin", "app"]:
|
|
||||||
if s.endswith(suffix):
|
|
||||||
s = s[:-len(suffix)].strip()
|
|
||||||
keywords_to_remove = {"ultimate", "edition", "definitive", "complete", "remastered"}
|
|
||||||
words = s.split()
|
|
||||||
filtered_words = [word for word in words if word not in keywords_to_remove]
|
|
||||||
return " ".join(filtered_words)
|
|
||||||
|
|
||||||
def fuzzy_search(self, query: str, limit: int = 5, min_score: float = 60.0) -> list[tuple[str, Any, float]]:
|
|
||||||
"""Perform fuzzy search using rapidfuzz."""
|
|
||||||
with self._lock:
|
|
||||||
if not query or not self.normalized_items:
|
|
||||||
return []
|
|
||||||
|
|
||||||
query_normalized = self._normalize(query)
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for i, (item_text, item_data) in enumerate(self.normalized_items):
|
|
||||||
score = fuzz.ratio(query_normalized, item_text)
|
|
||||||
if score >= min_score:
|
|
||||||
results.append((self.items[i][0], item_data, score))
|
|
||||||
|
|
||||||
# Sort by score descending
|
|
||||||
results.sort(key=lambda x: x[2], reverse=True)
|
|
||||||
return results[:limit]
|
|
||||||
|
|
||||||
class SearchOptimizer:
|
|
||||||
"""Main search optimization class combining multiple approaches."""
|
|
||||||
def __init__(self):
|
|
||||||
self.hash_index: dict[str, Any] = {}
|
|
||||||
self.trie_index = Trie()
|
|
||||||
self.fuzzy_index = None
|
|
||||||
self._lock = Lock()
|
|
||||||
|
|
||||||
def build_indices(self, items: list[tuple[str, Any]]):
|
|
||||||
"""Build all search indices from items."""
|
|
||||||
with self._lock:
|
|
||||||
self.hash_index = {item[0].lower(): item[1] for item in items}
|
|
||||||
self.trie_index = Trie()
|
|
||||||
for key, value in self.hash_index.items():
|
|
||||||
self.trie_index.insert(key, value)
|
|
||||||
self.fuzzy_index = FuzzySearchIndex(items)
|
|
||||||
|
|
||||||
def exact_search(self, key: str) -> Any | None:
|
|
||||||
"""Perform exact hash-based lookup."""
|
|
||||||
with self._lock:
|
|
||||||
return self.hash_index.get(key.lower())
|
|
||||||
|
|
||||||
def prefix_search(self, prefix: str) -> list[tuple[str, Any]]:
|
|
||||||
"""Perform prefix search using Trie."""
|
|
||||||
with self._lock:
|
|
||||||
return self.trie_index.search_prefix(prefix)
|
|
||||||
|
|
||||||
def fuzzy_search(self, query: str, limit: int = 5, min_score: float = 60.0) -> list[tuple[str, Any, float]]:
|
|
||||||
"""Perform fuzzy search."""
|
|
||||||
if self.fuzzy_index:
|
|
||||||
return self.fuzzy_index.fuzzy_search(query, limit, min_score)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class RequestRunnable(QRunnable):
|
|
||||||
"""Runnable for executing HTTP requests in a thread."""
|
|
||||||
|
|
||||||
def __init__(self, method: str, url: str, on_success=None, on_error=None, **kwargs):
|
|
||||||
super().__init__()
|
|
||||||
self.method = method
|
|
||||||
self.url = url
|
|
||||||
self.kwargs = kwargs
|
|
||||||
self.result = None
|
|
||||||
self.error = None
|
|
||||||
self.on_success: Callable | None = on_success
|
|
||||||
self.on_error: Callable | None = on_error
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
if self.method.lower() == 'get':
|
|
||||||
self.result = requests.get(self.url, **self.kwargs)
|
|
||||||
elif self.method.lower() == 'post':
|
|
||||||
self.result = requests.post(self.url, **self.kwargs)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported HTTP method: {self.method}")
|
|
||||||
|
|
||||||
# Execute success callback if provided
|
|
||||||
if self.on_success is not None:
|
|
||||||
success_callback = self.on_success # Capture the callback
|
|
||||||
def success_handler():
|
|
||||||
if success_callback is not None: # Re-check to satisfy Pyright
|
|
||||||
success_callback(self.result)
|
|
||||||
QTimer.singleShot(0, success_handler)
|
|
||||||
except Exception as e:
|
|
||||||
self.error = e
|
|
||||||
# Execute error callback if provided
|
|
||||||
if self.on_error is not None:
|
|
||||||
error_callback = self.on_error # Capture the callback
|
|
||||||
captured_error = e # Capture the exception
|
|
||||||
def error_handler():
|
|
||||||
error_callback(captured_error)
|
|
||||||
QTimer.singleShot(0, error_handler)
|
|
||||||
|
|
||||||
|
|
||||||
def run_request_in_thread(method: str, url: str, on_success: Callable | None = None, on_error: Callable | None = None, **kwargs):
|
|
||||||
"""Run HTTP request in a separate thread using Qt's thread system."""
|
|
||||||
runnable = RequestRunnable(method, url, on_success=on_success, on_error=on_error, **kwargs)
|
|
||||||
|
|
||||||
# Use QThreadPool to execute the runnable
|
|
||||||
from PySide6.QtCore import QThreadPool
|
|
||||||
thread_pool = QThreadPool.globalInstance()
|
|
||||||
thread_pool.start(runnable)
|
|
||||||
|
|
||||||
return runnable # Return the runnable to allow for potential cancellation if needed
|
|
||||||
|
|
||||||
|
|
||||||
def run_function_in_thread(func, *args, on_success: Callable | None = None, on_error: Callable | None = None, **kwargs):
|
|
||||||
"""Run a function in a separate thread."""
|
|
||||||
def execute():
|
|
||||||
try:
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
if on_success:
|
|
||||||
on_success(result)
|
|
||||||
except Exception as e:
|
|
||||||
if on_error:
|
|
||||||
on_error(e)
|
|
||||||
|
|
||||||
thread = threading.Thread(target=execute)
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
return thread
|
|
||||||
|
|
||||||
|
|
||||||
def run_in_thread(func, *args, **kwargs):
|
|
||||||
"""Run a function in a separate thread."""
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
||||||
future = executor.submit(func, *args, **kwargs)
|
|
||||||
return future.result()
|
|
||||||
|
|
||||||
|
|
||||||
def run_in_thread_async(func, *args, callback: Callable | None = None, **kwargs):
|
|
||||||
"""Run a function in a separate thread asynchronously."""
|
|
||||||
import threading
|
|
||||||
def target():
|
|
||||||
try:
|
|
||||||
result = func(*args, **kwargs)
|
|
||||||
if callback:
|
|
||||||
callback(result)
|
|
||||||
except Exception as e:
|
|
||||||
if callback:
|
|
||||||
callback(None) # or handle error in callback
|
|
||||||
logger.error(f"Error in threaded operation: {e}")
|
|
||||||
|
|
||||||
thread = threading.Thread(target=target)
|
|
||||||
thread.daemon = True
|
|
||||||
thread.start()
|
|
||||||
return thread
|
|
||||||
|
|
||||||
|
|
||||||
# Threaded search implementation using QThread for performance optimization
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadedSearchWorker(QObject):
|
|
||||||
"""
|
|
||||||
A threaded worker for performing search operations without blocking the UI.
|
|
||||||
"""
|
|
||||||
search_started = Signal()
|
|
||||||
search_finished = Signal(list)
|
|
||||||
search_error = Signal(str)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.search_optimizer = SearchOptimizer()
|
|
||||||
self.games_data = []
|
|
||||||
|
|
||||||
def set_games_data(self, games_data: list):
|
|
||||||
"""Set the games data to be searched."""
|
|
||||||
self.games_data = games_data
|
|
||||||
# Build indices from the games data (name, description, etc.)
|
|
||||||
items = [(game[0], game) for game in games_data] # game[0] is the name
|
|
||||||
self.search_optimizer.build_indices(items)
|
|
||||||
|
|
||||||
def execute_search(self, search_text: str, search_type: str = "auto"):
|
|
||||||
"""
|
|
||||||
Execute search in a separate thread.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
search_text: Text to search for
|
|
||||||
search_type: Type of search ("exact", "prefix", "fuzzy", "auto")
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.search_started.emit()
|
|
||||||
import time
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
if search_type == "exact" or (search_type == "auto" and len(search_text) > 2):
|
|
||||||
exact_result = self.search_optimizer.exact_search(search_text)
|
|
||||||
if exact_result:
|
|
||||||
results = [exact_result]
|
|
||||||
elif search_type == "prefix":
|
|
||||||
results = self.search_optimizer.prefix_search(search_text)
|
|
||||||
elif search_type == "fuzzy" or search_type == "auto":
|
|
||||||
results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=50.0)
|
|
||||||
else:
|
|
||||||
# Auto-detect search type based on input
|
|
||||||
if len(search_text) < 3:
|
|
||||||
results = self.search_optimizer.prefix_search(search_text)
|
|
||||||
else:
|
|
||||||
# Try exact first, then fuzzy
|
|
||||||
exact_result = self.search_optimizer.exact_search(search_text)
|
|
||||||
if exact_result:
|
|
||||||
results = [exact_result]
|
|
||||||
else:
|
|
||||||
results = self.search_optimizer.fuzzy_search(search_text, limit=20, min_score=50.0)
|
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
print(f"Search completed in {end_time - start_time:.4f} seconds")
|
|
||||||
|
|
||||||
self.search_finished.emit(results)
|
|
||||||
except Exception as e:
|
|
||||||
self.search_error.emit(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadedSearch(QThread):
|
|
||||||
"""
|
|
||||||
QThread implementation for running search operations in the background.
|
|
||||||
"""
|
|
||||||
search_started = Signal()
|
|
||||||
search_finished = Signal(list)
|
|
||||||
search_error = Signal(str)
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.worker = ThreadedSearchWorker()
|
|
||||||
self.search_text = ""
|
|
||||||
self.search_type = "auto"
|
|
||||||
self.games_data = []
|
|
||||||
|
|
||||||
# Connect worker signals to thread signals
|
|
||||||
self.worker.search_started.connect(self.search_started)
|
|
||||||
self.worker.search_finished.connect(self.search_finished)
|
|
||||||
self.worker.search_error.connect(self.search_error)
|
|
||||||
|
|
||||||
def set_search_params(self, search_text: str, games_data: list, search_type: str = "auto"):
|
|
||||||
"""Set parameters for the search operation."""
|
|
||||||
self.search_text = search_text
|
|
||||||
self.games_data = games_data
|
|
||||||
self.search_type = search_type
|
|
||||||
|
|
||||||
def set_games_data(self, games_data: list):
|
|
||||||
"""Set the games data to be searched."""
|
|
||||||
self.games_data = games_data
|
|
||||||
self.worker.set_games_data(games_data)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Run the search operation in the thread."""
|
|
||||||
self.worker.execute_search(self.search_text, self.search_type)
|
|
||||||
|
|
||||||
|
|
||||||
class SearchThreadPool:
|
|
||||||
"""
|
|
||||||
A simple thread pool for managing multiple search operations.
|
|
||||||
"""
|
|
||||||
def __init__(self, max_threads: int = 3):
|
|
||||||
self.max_threads = max_threads
|
|
||||||
self.active_threads = []
|
|
||||||
self.thread_queue = []
|
|
||||||
|
|
||||||
def submit_search(self, search_text: str, games_data: list, search_type: str = "auto",
|
|
||||||
on_start: Callable | None = None, on_finish: Callable | None = None, on_error: Callable | None = None):
|
|
||||||
"""
|
|
||||||
Submit a search operation to the pool.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
search_text: Text to search for
|
|
||||||
games_data: List of game data tuples to search in
|
|
||||||
search_type: Type of search ("exact", "prefix", "fuzzy", "auto")
|
|
||||||
on_start: Callback when search starts
|
|
||||||
on_finish: Callback when search finishes (receives results)
|
|
||||||
on_error: Callback when search errors (receives error message)
|
|
||||||
"""
|
|
||||||
search_thread = ThreadedSearch()
|
|
||||||
search_thread.set_search_params(search_text, games_data, search_type)
|
|
||||||
|
|
||||||
# Connect callbacks if provided
|
|
||||||
if on_start:
|
|
||||||
search_thread.search_started.connect(on_start)
|
|
||||||
if on_finish:
|
|
||||||
search_thread.search_finished.connect(on_finish)
|
|
||||||
if on_error:
|
|
||||||
search_thread.search_error.connect(on_error)
|
|
||||||
|
|
||||||
# Start the thread
|
|
||||||
search_thread.start()
|
|
||||||
self.active_threads.append(search_thread)
|
|
||||||
|
|
||||||
# Clean up finished threads
|
|
||||||
self.active_threads = [thread for thread in self.active_threads if thread.isRunning()]
|
|
||||||
|
|
||||||
return search_thread
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
def get_toggle_settings():
|
|
||||||
"""Get predefined toggle settings with descriptions."""
|
|
||||||
from portprotonqt.localization import _
|
|
||||||
|
|
||||||
return {
|
|
||||||
'PW_MANGOHUD': _("Using FPS and system load monitoring (Turns on and off by the key combination - right Shift + F12)"),
|
|
||||||
'PW_MANGOHUD_USER_CONF': _("Forced use of MANGOHUD system settings (GOverlay, etc.)"),
|
|
||||||
'PW_VKBASALT': _("Enable vkBasalt by default to improve graphics in games running on Vulkan. (The HOME hotkey disables vkbasalt)"),
|
|
||||||
'PW_VKBASALT_USER_CONF': _("Forced use of VKBASALT system settings (GOverlay, etc.)"),
|
|
||||||
'PW_DGVOODOO2': _("Enable dgVoodoo2. Forced use all dgVoodoo2 libs (Glide 2.11-3.1, DirectDraw 1-7, Direct3D 2-9) on all 3D API."),
|
|
||||||
'PW_GAMESCOPE': _("Super + F : Toggle fullscreen\nSuper + N : Toggle nearest neighbour filtering\nSuper + U : Toggle FSR upscaling\nSuper + Y : Toggle NIS upscaling\nSuper + I : Increase FSR sharpness by 1\nSuper + O : Decrease FSR sharpness by 1\nSuper + S : Take screenshot (currently goes to /tmp/gamescope_DATE.png)\nSuper + G : Toggle keyboard grab\nSuper + C : Update clipboard"),
|
|
||||||
'PW_USE_ESYNC': _("Enable in-process synchronization primitives based on eventfd."),
|
|
||||||
'PW_USE_FSYNC': _("Enable futex-based in-process synchronization primitives."),
|
|
||||||
'PW_USE_NTSYNC': _("Enable in-process synchronization via the Linux ntsync driver."),
|
|
||||||
'PW_USE_RAY_TRACING': _("Enable vkd3d support - Ray Tracing"),
|
|
||||||
'PW_USE_NVAPI_AND_DLSS': _("Enable DLSS on supported NVIDIA graphics cards"),
|
|
||||||
'PW_USE_OPTISCALER': _("Enable OptiScaler (replacement upscaler / frame generator)"),
|
|
||||||
'PW_USE_LS_FRAME_GEN': _("Enable Lossless Scaling frame generation (experimental)"),
|
|
||||||
'PW_WINE_FULLSCREEN_FSR': _("FSR upscaling in fullscreen with ProtonGE below native resolution"),
|
|
||||||
'PW_HIDE_NVIDIA_GPU': _("Disguise all NVIDIA GPU features"),
|
|
||||||
'PW_VIRTUAL_DESKTOP': _("Run the application in WINE virtual desktop"),
|
|
||||||
'PW_USE_TERMINAL': _("Run the application in a terminal"),
|
|
||||||
'PW_USE_GAMEMODE': _("Use system GameMode for performance optimization"),
|
|
||||||
'PW_USE_D3D_EXTRAS': _("Enable forced use of third-party DirectX libraries"),
|
|
||||||
'PW_FIX_VIDEO_IN_GAME': _("Fix pink-tinted video playback in some games"),
|
|
||||||
'PW_REDUCE_PULSE_LATENCY': _("Reduce PulseAudio latency to fix intermittent sound"),
|
|
||||||
'PW_USE_US_LAYOUT': _("Force US keyboard layout"),
|
|
||||||
'PW_USE_GSTREAMER': _("Use GStreamer for in-game clips (WMF support)"),
|
|
||||||
'PW_USE_SHADER_CACHE': _("Use WINE shader caching"),
|
|
||||||
'PW_USE_WINE_DXGI': _("Force use of built-in DXGI library"),
|
|
||||||
'PW_USE_EAC_AND_BE': _("Enable Easy Anti-Cheat and BattlEye runtimes"),
|
|
||||||
'PW_USE_SYSTEM_VK_LAYERS': _("Use system Vulkan layers (MangoHud, vkBasalt, OBS, etc.)"),
|
|
||||||
'PW_USE_OBS_VKCAPTURE': _("Enable OBS Studio capture via obs-vkcapture"),
|
|
||||||
'PW_DISABLE_COMPOSITING': _("Disable desktop compositing for performance"),
|
|
||||||
'PW_USE_RUNTIME': _("Use container launch mode (recommended default)"),
|
|
||||||
'PW_DINPUT_PROTOCOL': _("Force DirectInput protocol instead of XInput"),
|
|
||||||
'PW_USE_NATIVE_WAYLAND': _("Enable experimental native Wayland support"),
|
|
||||||
'PW_USE_DXVK_HDR': _("Enable HDR settings under native Wayland"),
|
|
||||||
'PW_USE_GALLIUM_ZINK': _("Use Gallium Zink (OpenGL via Vulkan)"),
|
|
||||||
'PW_USE_GALLIUM_NINE': _("Use Gallium Nine (native DirectX 9 for Mesa)"),
|
|
||||||
'PW_USE_WINED3D_VULKAN': _("Use WineD3D Vulkan backend (Damavand)"),
|
|
||||||
'PW_USE_SUPPLIED_DXVK_VKD3D': _("Use bundled dxvk/vkd3d from Wine/Proton"),
|
|
||||||
'PW_USE_SAREK_ASYNC': _("Use async dxvk-sarek (experimental)")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_advanced_settings(disabled_text, logical_core_options, locale_options,
|
|
||||||
amd_vulkan_drivers, is_amd, numa_nodes, dist_options=None, prefix_options=None):
|
|
||||||
"""Get advanced settings configuration."""
|
|
||||||
from portprotonqt.localization import _
|
|
||||||
|
|
||||||
advanced_settings = []
|
|
||||||
if dist_options is None:
|
|
||||||
dist_options = []
|
|
||||||
if prefix_options is None:
|
|
||||||
prefix_options = []
|
|
||||||
|
|
||||||
# 1. Wine Version
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_WINE_USE',
|
|
||||||
'name': _("Wine Version"),
|
|
||||||
'description': _("Select the Wine or Proton version to use for this executable."),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': dist_options,
|
|
||||||
'default': ''
|
|
||||||
})
|
|
||||||
|
|
||||||
# 2. Prefix Name
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_PREFIX_NAME',
|
|
||||||
'name': _("Prefix Name"),
|
|
||||||
'description': _("Specify the Wine prefix to run this game with"),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': prefix_options,
|
|
||||||
'default': 'DEFAULT'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 3. Vulkan Backend
|
|
||||||
vulkan_options = [
|
|
||||||
_("Newest"), # → 6
|
|
||||||
_("Stable"), # → 2
|
|
||||||
("Sarek"), # → 1
|
|
||||||
("WINED3D – OpenGL") # → 0
|
|
||||||
]
|
|
||||||
|
|
||||||
# Маппинг: отображаемый текст → реальное значение в ppdb
|
|
||||||
vulkan_value_map = {
|
|
||||||
vulkan_options[0]: "6",
|
|
||||||
vulkan_options[1]: "2",
|
|
||||||
vulkan_options[2]: "1",
|
|
||||||
vulkan_options[3]: "0",
|
|
||||||
}
|
|
||||||
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_VULKAN_USE',
|
|
||||||
'name': _("Vulkan Backend"),
|
|
||||||
'description': _(
|
|
||||||
"Select the DirectX → Vulkan/OpenGL backend:\n\n"
|
|
||||||
"• Newest – latest DXVK + VKD3D (best compatibility/performance, requires modern drivers: AMD Mesa 25+, NVIDIA 550.54.14+, Intel Mesa 24.2+)\n"
|
|
||||||
"• Stable – older, well-tested DXVK + VKD3D (works on any Vulkan 1.3+ driver)\n"
|
|
||||||
"• Sarek – experimental DXVK-Sarek + VKD3D-Sarek (supports older drivers, Vulkan 1.1+)\n"
|
|
||||||
"• WINED3D – OpenGL fallback (lowest performance, use only if others fail)"
|
|
||||||
),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': vulkan_options,
|
|
||||||
'default': '6',
|
|
||||||
'_value_map': vulkan_value_map
|
|
||||||
})
|
|
||||||
|
|
||||||
# 4. Windows version
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_WINDOWS_VER',
|
|
||||||
'name': _("Windows version"),
|
|
||||||
'description': _("Changing the WINDOWS emulation version may be required to run older games. WINDOWS versions below 10 do not support new games with DirectX 12"),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': ['11', '10', '7', 'XP'],
|
|
||||||
'default': '10'
|
|
||||||
})
|
|
||||||
|
|
||||||
# 5. DLL Overrides
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'WINEDLLOVERRIDES',
|
|
||||||
'name': _("DLL Overrides"),
|
|
||||||
'description': _("Forced to use/disable the library only for the given application.\n\nA brief instruction:\n* libraries are written WITHOUT the .dll file extension\n* libraries are separated by semicolons - ;\n* library=n - use the WINDOWS (third-party) library\n* library=b - use WINE (built-in) library\n* library=n,b - use WINDOWS library and then WINE\n* library=b,n - use WINE library and then WINDOWS\n* library= - disable the use of this library\n\nExample: libglesv2=;d3dx9_36,d3dx9_42=n,b;mfc120=b,n"),
|
|
||||||
'type': 'text',
|
|
||||||
'default': ''
|
|
||||||
})
|
|
||||||
|
|
||||||
# 6. Launch arguments
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'LAUNCH_PARAMETERS',
|
|
||||||
'name': _("Launch Arguments"),
|
|
||||||
'description': _("Adding an argument after the .exe file, just like you would add an argument in a shortcut on a WINDOWS system.\n\nExample: -dx11 -skipintro 1"),
|
|
||||||
'type': 'text',
|
|
||||||
'default': ''
|
|
||||||
})
|
|
||||||
|
|
||||||
# 7. CPU cores limit
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_WINE_CPU_TOPOLOGY',
|
|
||||||
'name': _("CPU Cores Limit"),
|
|
||||||
'description': _("Limiting the number of CPU cores is useful for Unity games (It is recommended to set the value equal to 8)"),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': [disabled_text] + logical_core_options,
|
|
||||||
'default': disabled_text
|
|
||||||
})
|
|
||||||
|
|
||||||
# 8. OpenGL version
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_MESA_GL_VERSION_OVERRIDE',
|
|
||||||
'name': _("OpenGL Version"),
|
|
||||||
'description': _("You can select the required OpenGL version, some games require a forced Compatibility Profile (COMP)."),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': [disabled_text, '4.6COMPAT', '4.5COMPAT', '4.3COMPAT', '4.1COMPAT', '3.3COMPAT', '3.2COMPAT'],
|
|
||||||
'default': disabled_text
|
|
||||||
})
|
|
||||||
|
|
||||||
# 9. VKD3D feature level
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_VKD3D_FEATURE_LEVEL',
|
|
||||||
'name': _("VKD3D Feature Level"),
|
|
||||||
'description': _("You can set a forced feature level VKD3D for games on DirectX12"),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': [disabled_text, '12_2', '12_1', '12_0', '11_1', '11_0'],
|
|
||||||
'default': disabled_text
|
|
||||||
})
|
|
||||||
|
|
||||||
# 10. Locale
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_LOCALE_SELECT',
|
|
||||||
'name': _("Locale"),
|
|
||||||
'description': _("Force certain locale for an app. Fixes encoding issues in legacy software"),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': [disabled_text] + locale_options,
|
|
||||||
'default': disabled_text
|
|
||||||
})
|
|
||||||
|
|
||||||
# 11. Present mode
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_MESA_VK_WSI_PRESENT_MODE',
|
|
||||||
'name': _("Window Mode"),
|
|
||||||
'description': _("Window mode (for Vulkan and OpenGL):\nfifo - First in, first out. Limits the frame rate + no tearing. (VSync)\nimmediate - Unlimited frame rate + tearing.\nmailbox - Triple buffering. Unlimited frame rate + no tearing.\nrelaxed - Same as fifo but allows tearing when below the monitors refresh rate."),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': [disabled_text, 'fifo', 'immediate', 'mailbox', 'relaxed'],
|
|
||||||
'default': disabled_text
|
|
||||||
})
|
|
||||||
|
|
||||||
# 12. AMD Vulkan driver
|
|
||||||
amd_options = [disabled_text] + amd_vulkan_drivers if is_amd and amd_vulkan_drivers else [disabled_text]
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_AMD_VULKAN_USE',
|
|
||||||
'name': _("AMD Vulkan Driver"),
|
|
||||||
'description': _("Select needed AMD vulkan implementation. Choosing which implementation of vulkan will be used to run the game"),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': amd_options,
|
|
||||||
'default': disabled_text
|
|
||||||
})
|
|
||||||
|
|
||||||
# 13. NUMA node
|
|
||||||
numa_ids = sorted(numa_nodes.keys())
|
|
||||||
numa_options = [disabled_text] + numa_ids if len(numa_ids) > 1 else [disabled_text]
|
|
||||||
advanced_settings.append({
|
|
||||||
'key': 'PW_CPU_NUMA_NODE_INDEX',
|
|
||||||
'name': _("NUMA Node"),
|
|
||||||
'description': _("NUMA node for CPU affinity. In multi-core systems, CPUs are split into NUMA nodes, each with its own local memory and cores. Binding a game to a single node reduces memory-access latency and limits costly core-to-core switches."),
|
|
||||||
'type': 'combo',
|
|
||||||
'options': numa_options,
|
|
||||||
'default': disabled_text
|
|
||||||
})
|
|
||||||
|
|
||||||
return advanced_settings
|
|
||||||
|
|
||||||
|
|
||||||
# Keys that should be recognized as advanced settings
|
|
||||||
ADVANCED_SETTING_KEYS = [
|
|
||||||
'PW_WINE_USE',
|
|
||||||
'PW_PREFIX_NAME',
|
|
||||||
'PW_VULKAN_USE',
|
|
||||||
'PW_WINDOWS_VER',
|
|
||||||
'WINEDLLOVERRIDES',
|
|
||||||
'LAUNCH_PARAMETERS',
|
|
||||||
'PW_WINE_CPU_TOPOLOGY',
|
|
||||||
'PW_MESA_GL_VERSION_OVERRIDE',
|
|
||||||
'PW_VKD3D_FEATURE_LEVEL',
|
|
||||||
'PW_LOCALE_SELECT',
|
|
||||||
'PW_MESA_VK_WSI_PRESENT_MODE',
|
|
||||||
'PW_AMD_VULKAN_USE',
|
|
||||||
'PW_CPU_NUMA_NODE_INDEX',
|
|
||||||
]
|
|
||||||
@@ -13,7 +13,7 @@ from portprotonqt.logger import get_logger
|
|||||||
from portprotonqt.localization import get_steam_language
|
from portprotonqt.localization import get_steam_language
|
||||||
from portprotonqt.downloader import Downloader
|
from portprotonqt.downloader import Downloader
|
||||||
from portprotonqt.dialogs import generate_thumbnail
|
from portprotonqt.dialogs import generate_thumbnail
|
||||||
from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command
|
from portprotonqt.config_utils import get_portproton_location
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -23,7 +23,6 @@ import requests
|
|||||||
import random
|
import random
|
||||||
import base64
|
import base64
|
||||||
import glob
|
import glob
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
downloader = Downloader()
|
downloader = Downloader()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -212,28 +211,14 @@ def normalize_name(s):
|
|||||||
|
|
||||||
def is_valid_candidate(candidate):
|
def is_valid_candidate(candidate):
|
||||||
"""
|
"""
|
||||||
Determines whether a given candidate string is valid for use as a game name.
|
Checks if a candidate contains forbidden substrings:
|
||||||
|
- win32
|
||||||
The function performs the following checks:
|
- win64
|
||||||
1. Normalizes the candidate using `normalize_name()`.
|
- gamelauncher
|
||||||
2. Rejects the candidate if the normalized name is exactly "game"
|
Additionally checks the string without spaces.
|
||||||
(to avoid overly generic names).
|
Returns True if the candidate is valid, otherwise False.
|
||||||
3. Removes spaces and checks for forbidden substrings:
|
|
||||||
- "win32"
|
|
||||||
- "win64"
|
|
||||||
- "gamelauncher"
|
|
||||||
These are checked in the space-free version of the string.
|
|
||||||
4. Returns True only if none of the forbidden conditions are met.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
candidate (str): The candidate string to validate.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if the candidate is valid, False otherwise.
|
|
||||||
"""
|
"""
|
||||||
normalized_candidate = normalize_name(candidate)
|
normalized_candidate = normalize_name(candidate)
|
||||||
if normalized_candidate == "game":
|
|
||||||
return False
|
|
||||||
normalized_no_space = normalized_candidate.replace(" ", "")
|
normalized_no_space = normalized_candidate.replace(" ", "")
|
||||||
forbidden = ["win32", "win64", "gamelauncher"]
|
forbidden = ["win32", "win64", "gamelauncher"]
|
||||||
for token in forbidden:
|
for token in forbidden:
|
||||||
@@ -412,52 +397,6 @@ def save_app_details(app_id, data):
|
|||||||
with open(cache_file, "wb") as f:
|
with open(cache_file, "wb") as f:
|
||||||
f.write(orjson.dumps(data))
|
f.write(orjson.dumps(data))
|
||||||
|
|
||||||
def fetch_sgdb_cover(game_name: str) -> str:
|
|
||||||
"""
|
|
||||||
Fetch a cover image URL from steamgrid.usebottles.com for the given game.
|
|
||||||
The API returns a single string (quoted URL).
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
encoded = urllib.parse.quote(game_name)
|
|
||||||
url = f"https://steamgrid.usebottles.com/api/search/{encoded}"
|
|
||||||
resp = requests.get(url, timeout=10)
|
|
||||||
if resp.status_code != 200:
|
|
||||||
logger.warning("SGDB request failed for %s: %s", game_name, resp.status_code)
|
|
||||||
return ""
|
|
||||||
text = resp.text.strip()
|
|
||||||
# Убираем возможные кавычки вокруг строки
|
|
||||||
if text.startswith('"') and text.endswith('"'):
|
|
||||||
text = text[1:-1]
|
|
||||||
if text:
|
|
||||||
logger.info("Fetched SGDB cover for %s: %s", game_name, text)
|
|
||||||
return text
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
logger.warning(f"SGDB request timed out for {game_name}")
|
|
||||||
return ""
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.warning(f"SGDB request error for {game_name}: {e}")
|
|
||||||
return ""
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Unexpected error while fetching SGDB cover for {game_name}: {e}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def check_url_exists(url: str) -> bool:
|
|
||||||
"""Check whether a URL returns HTTP 200."""
|
|
||||||
try:
|
|
||||||
r = requests.head(url, timeout=5)
|
|
||||||
return r.status_code == 200
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
logger.warning(f"URL check timed out for: {url}")
|
|
||||||
return False
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
logger.warning(f"Request error when checking URL {url}: {e}")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Unexpected error when checking URL {url}: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
|
def fetch_app_info_async(app_id: int, callback: Callable[[dict | None], None]):
|
||||||
"""
|
"""
|
||||||
Asynchronously fetches detailed app info from Steam API.
|
Asynchronously fetches detailed app info from Steam API.
|
||||||
@@ -676,11 +615,6 @@ def get_full_steam_game_info_async(appid: int, callback: Callable[[dict], None])
|
|||||||
title = decode_text(app_info.get("name", ""))
|
title = decode_text(app_info.get("name", ""))
|
||||||
description = decode_text(app_info.get("short_description", ""))
|
description = decode_text(app_info.get("short_description", ""))
|
||||||
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
||||||
if not check_url_exists(cover):
|
|
||||||
logger.info("Steam cover not found for %s, trying SGDB", title)
|
|
||||||
alt_cover = fetch_sgdb_cover(title)
|
|
||||||
if alt_cover:
|
|
||||||
cover = alt_cover
|
|
||||||
|
|
||||||
def on_protondb_tier(tier: str):
|
def on_protondb_tier(tier: str):
|
||||||
def on_anticheat_status(anticheat_status: str):
|
def on_anticheat_status(anticheat_status: str):
|
||||||
@@ -774,15 +708,12 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
|
|||||||
game_name = desktop_name or exe_name.capitalize()
|
game_name = desktop_name or exe_name.capitalize()
|
||||||
|
|
||||||
if not matching_app:
|
if not matching_app:
|
||||||
cover = fetch_sgdb_cover(game_name) or ""
|
|
||||||
logger.info("Using SGDB cover for non-Steam game '%s': %s", game_name, cover)
|
|
||||||
|
|
||||||
def on_anticheat_status(anticheat_status: str):
|
def on_anticheat_status(anticheat_status: str):
|
||||||
callback({
|
callback({
|
||||||
"appid": "",
|
"appid": "",
|
||||||
"name": decode_text(game_name),
|
"name": decode_text(game_name),
|
||||||
"description": "",
|
"description": "",
|
||||||
"cover": cover,
|
"cover": "",
|
||||||
"controller_support": "",
|
"controller_support": "",
|
||||||
"protondb_tier": "",
|
"protondb_tier": "",
|
||||||
"steam_game": "false",
|
"steam_game": "false",
|
||||||
@@ -813,11 +744,6 @@ def get_steam_game_info_async(desktop_name: str, exec_line: str, callback: Calla
|
|||||||
title = decode_text(app_info.get("name", game_name))
|
title = decode_text(app_info.get("name", game_name))
|
||||||
description = decode_text(app_info.get("short_description", ""))
|
description = decode_text(app_info.get("short_description", ""))
|
||||||
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
cover = f"https://steamcdn-a.akamaihd.net/steam/apps/{appid}/library_600x900_2x.jpg"
|
||||||
if not check_url_exists(cover):
|
|
||||||
logger.info("Steam cover not found for %s, trying SGDB", title)
|
|
||||||
alt_cover = fetch_sgdb_cover(title)
|
|
||||||
if alt_cover:
|
|
||||||
cover = alt_cover
|
|
||||||
controller_support = app_info.get("controller_support", "")
|
controller_support = app_info.get("controller_support", "")
|
||||||
|
|
||||||
def on_protondb_tier(tier: str):
|
def on_protondb_tier(tier: str):
|
||||||
@@ -1017,8 +943,7 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
|
|||||||
return (False, f"Executable file not found: {exe_path}")
|
return (False, f"Executable file not found: {exe_path}")
|
||||||
|
|
||||||
portproton_dir = get_portproton_location()
|
portproton_dir = get_portproton_location()
|
||||||
start_sh = get_portproton_start_command()
|
if not portproton_dir:
|
||||||
if not portproton_dir or not start_sh:
|
|
||||||
logger.error("PortProton directory not found")
|
logger.error("PortProton directory not found")
|
||||||
return (False, "PortProton directory not found")
|
return (False, "PortProton directory not found")
|
||||||
|
|
||||||
@@ -1027,12 +952,17 @@ def add_to_steam(game_name: str, exec_line: str, cover_path: str) -> tuple[bool,
|
|||||||
|
|
||||||
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
|
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
|
||||||
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
|
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}.sh")
|
||||||
|
start_sh_path = os.path.join(portproton_dir, "data", "scripts", "start.sh")
|
||||||
|
|
||||||
|
if not os.path.exists(start_sh_path):
|
||||||
|
logger.error(f"start.sh not found at {start_sh_path}")
|
||||||
|
return (False, f"start.sh not found at {start_sh_path}")
|
||||||
|
|
||||||
if not os.path.exists(script_path):
|
if not os.path.exists(script_path):
|
||||||
script_content = f"""#!/usr/bin/env bash
|
script_content = f"""#!/usr/bin/env bash
|
||||||
export LD_PRELOAD=
|
export LD_PRELOAD=
|
||||||
export START_FROM_STEAM=1
|
export START_FROM_STEAM=1
|
||||||
"{start_sh}" "{exe_path}" "$@"
|
"{start_sh_path}" "{exe_path}" "$@"
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(script_path, "w", encoding="utf-8") as f:
|
with open(script_path, "w", encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ from portprotonqt.logger import get_logger
|
|||||||
from PySide6.QtGui import QIcon, QFontDatabase, QPixmap
|
from PySide6.QtGui import QIcon, QFontDatabase, QPixmap
|
||||||
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
from portprotonqt.config_utils import save_theme_to_config, load_theme_metainfo
|
||||||
|
|
||||||
# Icon caching for performance optimization
|
|
||||||
_icon_cache = {}
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# Папка, где располагаются все дополнительные темы
|
# Папка, где располагаются все дополнительные темы
|
||||||
@@ -111,65 +108,34 @@ def load_theme_fonts(theme_name):
|
|||||||
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
logger.debug(f"Fonts for theme '{theme_name}' already loaded, skipping")
|
||||||
return
|
return
|
||||||
|
|
||||||
def load_fonts_delayed():
|
QFontDatabase.removeAllApplicationFonts()
|
||||||
global _loaded_theme
|
fonts_folder = None
|
||||||
try:
|
if theme_name == "standart":
|
||||||
# Only remove fonts if this is a theme change (not initial load)
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
current_loaded_theme = _loaded_theme # Capture the current value
|
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
||||||
if current_loaded_theme is not None and current_loaded_theme != theme_name:
|
else:
|
||||||
# Run font removal in the GUI thread with delay
|
for themes_dir in THEMES_DIRS:
|
||||||
QFontDatabase.removeAllApplicationFonts()
|
theme_folder = os.path.join(themes_dir, theme_name)
|
||||||
|
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
||||||
|
if os.path.exists(possible_fonts_folder):
|
||||||
|
fonts_folder = possible_fonts_folder
|
||||||
|
break
|
||||||
|
|
||||||
import time
|
if not fonts_folder or not os.path.exists(fonts_folder):
|
||||||
import os
|
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
||||||
start_time = time.time()
|
return
|
||||||
timeout = 3 # Reduced timeout to 3 seconds for faster loading
|
|
||||||
|
|
||||||
fonts_folder = None
|
for filename in os.listdir(fonts_folder):
|
||||||
if theme_name == "standart":
|
if filename.lower().endswith((".ttf", ".otf")):
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
font_path = os.path.join(fonts_folder, filename)
|
||||||
fonts_folder = os.path.join(base_dir, "themes", "standart", "fonts")
|
font_id = QFontDatabase.addApplicationFont(font_path)
|
||||||
|
if font_id != -1:
|
||||||
|
families = QFontDatabase.applicationFontFamilies(font_id)
|
||||||
|
logger.info(f"Font {filename} successfully loaded: {families}")
|
||||||
else:
|
else:
|
||||||
for themes_dir in THEMES_DIRS:
|
logger.error(f"Error loading font: {filename}")
|
||||||
theme_folder = os.path.join(themes_dir, theme_name)
|
|
||||||
possible_fonts_folder = os.path.join(theme_folder, "fonts")
|
|
||||||
if os.path.exists(possible_fonts_folder):
|
|
||||||
fonts_folder = possible_fonts_folder
|
|
||||||
break
|
|
||||||
|
|
||||||
if not fonts_folder or not os.path.exists(fonts_folder):
|
_loaded_theme = theme_name
|
||||||
logger.error(f"Fonts folder not found for theme '{theme_name}'")
|
|
||||||
return
|
|
||||||
|
|
||||||
font_files = []
|
|
||||||
for filename in os.listdir(fonts_folder):
|
|
||||||
if filename.lower().endswith((".ttf", ".otf")):
|
|
||||||
font_files.append(filename)
|
|
||||||
|
|
||||||
# Limit number of fonts loaded to prevent too much blocking
|
|
||||||
font_files = font_files[:10] # Only load first 10 fonts to prevent too much blocking
|
|
||||||
|
|
||||||
for filename in font_files:
|
|
||||||
if time.time() - start_time > timeout:
|
|
||||||
logger.warning(f"Font loading timed out for theme '{theme_name}' after loading {len(font_files)} fonts")
|
|
||||||
break
|
|
||||||
|
|
||||||
font_path = os.path.join(fonts_folder, filename)
|
|
||||||
font_id = QFontDatabase.addApplicationFont(font_path)
|
|
||||||
if font_id != -1:
|
|
||||||
families = QFontDatabase.applicationFontFamilies(font_id)
|
|
||||||
logger.info(f"Font {filename} successfully loaded: {families}")
|
|
||||||
else:
|
|
||||||
logger.error(f"Error loading font: {filename}")
|
|
||||||
|
|
||||||
# Update the global variable in the main thread
|
|
||||||
_loaded_theme = theme_name
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error loading fonts for theme '{theme_name}': {e}")
|
|
||||||
|
|
||||||
# Use QTimer to delay font loading until after the UI is rendered
|
|
||||||
from PySide6.QtCore import QTimer
|
|
||||||
QTimer.singleShot(100, load_fonts_delayed) # Delay font loading by 100ms
|
|
||||||
|
|
||||||
class ThemeWrapper:
|
class ThemeWrapper:
|
||||||
"""
|
"""
|
||||||
@@ -266,14 +232,6 @@ class ThemeManager:
|
|||||||
а если файл не найден, то из стандартной темы.
|
а если файл не найден, то из стандартной темы.
|
||||||
Если as_path=True, возвращает путь к иконке вместо QIcon.
|
Если as_path=True, возвращает путь к иконке вместо QIcon.
|
||||||
"""
|
"""
|
||||||
# Create cache key
|
|
||||||
cache_key = f"{icon_name}_{theme_name or self.current_theme_name}_{as_path}"
|
|
||||||
|
|
||||||
# Check if we already have this icon cached
|
|
||||||
if cache_key in _icon_cache:
|
|
||||||
logger.debug(f"Using cached icon for {icon_name}")
|
|
||||||
return _icon_cache[cache_key]
|
|
||||||
|
|
||||||
icon_path = None
|
icon_path = None
|
||||||
theme_name = theme_name or self.current_theme_name
|
theme_name = theme_name or self.current_theme_name
|
||||||
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
|
supported_extensions = ['.svg', '.png', '.jpg', '.jpeg']
|
||||||
@@ -321,20 +279,12 @@ class ThemeManager:
|
|||||||
# Если иконка всё равно не найдена
|
# Если иконка всё равно не найдена
|
||||||
if not icon_path or not os.path.exists(icon_path):
|
if not icon_path or not os.path.exists(icon_path):
|
||||||
logger.error(f"Warning: icon '{icon_name}' not found")
|
logger.error(f"Warning: icon '{icon_name}' not found")
|
||||||
result = QIcon() if not as_path else None
|
return QIcon() if not as_path else None
|
||||||
# Cache the result even if it's None
|
|
||||||
_icon_cache[cache_key] = result
|
|
||||||
return result
|
|
||||||
|
|
||||||
if as_path:
|
if as_path:
|
||||||
# Cache the path
|
|
||||||
_icon_cache[cache_key] = icon_path
|
|
||||||
return icon_path
|
return icon_path
|
||||||
|
|
||||||
# Create QIcon and cache it
|
return QIcon(icon_path)
|
||||||
icon = QIcon(icon_path)
|
|
||||||
_icon_cache[cache_key] = icon
|
|
||||||
return icon
|
|
||||||
|
|
||||||
def get_theme_image(self, image_name, theme_name=None):
|
def get_theme_image(self, image_name, theme_name=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
BIN
portprotonqt/themes/standart/fonts/Orbitron-Regular.ttf
Normal file
BIN
portprotonqt/themes/standart/fonts/RASKHAL-Regular.ttf
Executable file
@@ -1 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m24 9.185c-2.0302 0-3.7027 1.6725-3.7027 3.7027v7.4096h-7.4096c-2.0302 0-3.7027 1.6725-3.7027 3.7027s1.6725 3.7027 3.7027 3.7027h7.4096v7.4096c0 2.0302 1.6725 3.7027 3.7027 3.7027s3.7027-1.6725 3.7027-3.7027v-7.4096h7.4096c2.0302 0 3.7027-1.6725 3.7027-3.7027s-1.6725-3.7027-3.7027-3.7027h-7.4096v-7.4096c0-2.0302-1.6725-3.7027-3.7027-3.7027zm0 2.9613c0.41396 0 0.74137 0.32742 0.74137 0.74137v10.371h10.371c0.41396 0 0.74137 0.32742 0.74137 0.74137s-0.32742 0.74137-0.74137 0.74137h-10.371v10.371c0 0.41396-0.32742 0.74137-0.74137 0.74137s-0.74137-0.32742-0.74137-0.74137v-10.371h-10.371c-0.41396 0-0.74137-0.32742-0.74137-0.74137s0.32742-0.74137 0.74137-0.74137h10.371v-10.371c0-0.41396 0.32742-0.74137 0.74137-0.74137z" fill="#3f424d" stop-color="#000000" stroke-width="1.0662"/><path d="m24 11.494c1.1462 0 2.0844 0.93819 2.0844 2.0844v8.3375h8.3375c1.1462 0 2.0844 0.93819 2.0844 2.0844s-0.93819 2.0844-2.0844 2.0844h-8.3375v8.3375c0 1.1462-0.93819 2.0844-2.0844 2.0844s-2.0844-0.93819-2.0844-2.0844v-8.3375h-8.3375c-1.1462 0-2.0844-0.93819-2.0844-2.0844s0.93819-2.0844 2.0844-2.0844h8.3375v-8.3375c0-1.1462 0.93819-2.0844 2.0844-2.0844z" fill="#fff" stop-color="#000000" stroke-width="0"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m12 6c-3.3238 0-5.9998 2.6763-5.9998 5.9998v24c0 3.3238 2.6763 5.9998 5.9998 5.9998h24c3.3238 0 5.9998-2.6763 5.9998-5.9998v-24c0-3.3238-2.6763-5.9998-5.9998-5.9998z" fill="#3f424d" stroke-width="2.5714"/><path d="m13.697 8.5452c-2.8537 0-5.1515 2.2979-5.1515 5.1515v20.606c0 2.8537 2.2979 5.1515 5.1515 5.1515h20.606c2.8537 0 5.1515-2.2979 5.1515-5.1515v-20.606c0-2.8537-2.2979-5.1515-5.1515-5.1515z" fill="#fff" stroke-width="2.2078"/><path d="m15.222 18.538h8.5002v1.8684h-5.8744v2.6763h5.3189v1.8684h-5.3189v4.511h-2.6258zm13.6 11.092q-1.1614 0-3.4842-0.18515v-2.0367q2.0872 0.38714 3.1476 0.38714 0.92576 0 1.3466-0.20198 0.4208-0.21882 0.4208-0.67328v-1.6327q0-0.45446-0.30298-0.63962-0.30298-0.20198-0.99309-0.20198h-3.3664v-5.908h6.9011v1.8852h-4.4436v2.2218h1.7169q0.97626 0 1.902 0.3703 0.50496 0.21882 0.80794 0.67328t0.30298 1.0604v2.2892q0 0.65645-0.25248 1.1782-0.25248 0.50496-0.63962 0.77427-0.33664 0.25248-0.90893 0.40397-0.55546 0.15149-1.0772 0.20198-0.60595 0.03366-1.0772 0.03366z" fill="#3f424d" stroke-width="0" aria-label="F5"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m20.375 10.836v21.898l4.4513 1.4106v-18.447c0-1.1417 0.63761-1.8498 1.4528-1.5795 1.2195 0.33732 1.2205 1.7172 1.2205 2.2383v7.34c0.69194 0.30705 1.3479 0.46456 1.9511 0.46456 0.90817 0 1.6684-0.36594 2.2003-1.06 0.57517-0.75032 0.87844-1.8745 0.87844-3.2519 0-3.9851-1.3499-5.7267-5.562-7.1711-1.3341-0.45192-4.3372-1.3851-6.5925-1.8413zm-1.6344 13.688-6.8797 2.441c-0.02379 0.0087-1.7358 0.57835-2.724 1.3092-0.35029 0.25948-0.50605 0.57737-0.44766 0.89955 0.11028 0.60112 0.89455 1.164 2.099 1.5035 2.5191 0.83248 5.1622 1.0445 7.7159 0.62504l0.23228-0.03801v-1.9131l-2.0863 0.75596c-0.96438 0.34597-2.458 0.42428-3.2646 0.16049-0.5795-0.19028-0.70734-0.49524-0.70951-0.71795-0.0043-0.26596 0.17441-0.64535 1.022-0.95023l5.0426-1.8033zm13.269 1.1529c-0.59973-0.0075-1.1869 0.016-1.7442 0.07603-2.1515 0.23785-3.7118 0.78326-3.7291 0.78975l-0.09714 0.0338v2.3692l5.0299-1.7695c0.96439-0.34597 2.4538-0.42428 3.2604-0.16048 0.5795 0.19028 0.70734 0.49523 0.70951 0.71795 0.0021 0.26596-0.17234 0.64329-1.0178 0.94601l-7.9819 2.8465v2.2594l10.706-3.8432c0.01297-0.0043 1.421-0.53074 1.968-1.2205 0.19245-0.24218 0.25306-0.4904 0.17737-0.73907-0.09298-0.30488-0.49051-0.89192-2.0863-1.3979-1.4887-0.56436-3.3954-0.88519-5.1946-0.908z" fill="#3f424d" stroke-width="2.1623"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg width="48" height="48" version="1.1" viewBox="0 0 48 48" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="m47 24a23 23 0 0 1-23 23 23 23 0 0 1-23-23 23 23 0 0 1 23-23 23 23 0 0 1 23 23z" fill="#3f424d"/><path d="M 42.513,24 A 18.513,18.513 0 0 1 24,42.513 18.513,18.513 0 0 1 5.4869995,24 18.513,18.513 0 0 1 24,5.4869995 18.513,18.513 0 0 1 42.513,24 Z" fill="#fff"/><path d="m33.019 13.476h-18.037c-0.8274 0-1.5059 0.67847-1.5059 1.5059v18.037c0 0.8274 0.67847 1.5059 1.5059 1.5059h18.037c0.8274 0 1.5059-0.67847 1.5059-1.5059v-18.037c0-0.8274-0.66192-1.5059-1.5059-1.5059zm-1.4893 18.037h-15.026v-15.026h15.026z" fill="#3f424d" stroke-width="1.6548"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 682 B |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 225 KiB |
BIN
portprotonqt/themes/standart/images/screenshots/Библиотека.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 70 KiB |
BIN
portprotonqt/themes/standart/images/screenshots/Карточка.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 38 KiB |
BIN
portprotonqt/themes/standart/images/screenshots/Настройки.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
portprotonqt/themes/standart/images/screenshots/Оверлей.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |