11 Commits

Author SHA1 Message Date
b6062f1ee0 fix(gamecard): correct argument order when creating GameCard
All checks were successful
Code check / Check code (push) Successful in 1m15s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-26 23:28:06 +05:00
651040de70 feat: replace linux-gaming topics to ppdb.linux-gaming.ru api calls
All checks were successful
Code check / Check code (push) Successful in 1m19s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-26 23:09:02 +05:00
Renovate Bot
3f0b3d65ad chore(deps): update archlinux:base-devel docker digest to d2bd09b
All checks were successful
Code check / Check code (push) Successful in 1m19s
2026-01-26 12:17:08 +00:00
Renovate Bot
ec449f12d1 chore(deps): update https://gitea.com/actions/setup-python digest to a309ff8
Some checks failed
Code check / Check code (push) Has been cancelled
2026-01-26 12:16:50 +00:00
2d5932f144 fix: prevent QMessageBox from closing immediately on gamepad
Some checks failed
Code check / Check code (push) Has been cancelled
2026-01-26 17:16:05 +05:00
0a9381b1d2 fix: open virtual keyboard on all QLineEdit
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-26 17:12:02 +05:00
20fb7174c1 fix: update virtual keyboard icons on change gamepad type
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-26 17:07:22 +05:00
8f9e6ea958 chore(build): switch from pyproject to meson build system
All checks were successful
Check Translations / check-translations (push) Successful in 17s
Code check / Check code (push) Successful in 1m11s
renovate / renovate (push) Successful in 44s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-19 06:19:04 +00:00
Renovate Bot
19aecd75c4 chore(deps): update archlinux:base-devel docker digest to 84c36fa
All checks were successful
Code check / Check code (push) Successful in 1m20s
2026-01-18 05:56:49 +00:00
Renovate Bot
905ebe1d61 chore(deps): update https://gitea.com/actions/setup-node digest to 6044e13
Some checks failed
Code check / Check code (pull_request) Successful in 1m47s
Code check / Check code (push) Has been cancelled
2026-01-18 00:01:59 +00:00
ae0b3a0f1a chore(build): added qt6-svg
All checks were successful
Code check / Check code (push) Successful in 1m9s
renovate / renovate (push) Successful in 1m24s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2026-01-14 18:08:32 +05:00
35 changed files with 1834 additions and 235 deletions

View File

@@ -2,8 +2,14 @@ name: Nightly Build - AppImage, Arch, Fedora
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
branch:
description: 'Branch to build'
required: false
default: 'main'
env: env:
BUILD_BRANCH: ${{ inputs.branch || 'main' }}
PKGDEST: "/tmp/portprotonqt" PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt" PACKAGE: "portprotonqt"
@@ -12,7 +18,7 @@ jobs:
name: Build AppImage name: Build AppImage
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
container: container:
image: archlinux:base-devel@sha256:ebcaeca69c4d416f848aedcd27fe224384fd506f86046526a5d49ec6d9e29db1 image: archlinux:base-devel@sha256:d2bd09bd30dc1199ba6f35bc575292957a4a24377fb137d0f999817bc29adc17
options: --privileged --device /dev/fuse options: --privileged --device /dev/fuse
steps: steps:
- name: Prepare container - name: Prepare container
@@ -22,12 +28,14 @@ jobs:
pacman -Syu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm xorg-server-xvfb zsync pacman -Syu --noconfirm --disable-download-timeout --needed git wget gnupg nodejs npm xorg-server-xvfb zsync
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
ref: ${{ env.BUILD_BRANCH }}
- name: Install appimage dependencies - name: Install appimage dependencies
run: | run: |
cd build-aux/AppImage cd build-aux/AppImage
chmod +x get-dependencies.sh portprotonqt-appimage.sh chmod +x get-dependencies.sh portprotonqt-appimage.sh
./get-dependencies.sh --git ./get-dependencies.sh --local --branch "${{ env.BUILD_BRANCH }}"
- name: Build AppImage - name: Build AppImage
run: | run: |
@@ -55,9 +63,8 @@ jobs:
steps: steps:
- 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 meson ninja-build python3-devel \
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \ systemd-rpm-macros redhat-rpm-config nodejs npm
redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment - name: Setup rpmbuild environment
run: | run: |
@@ -68,10 +75,13 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
ref: ${{ env.BUILD_BRANCH }}
- name: Copy fedora.spec - name: Copy fedora.spec
run: | run: |
cp build-aux/fedora-git.spec /home/rpmbuild/SPECS/${{ env.PACKAGE }}.spec sed "s|git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git|git clone -b ${{ env.BUILD_BRANCH }} https://git.linux-gaming.ru/Boria138/PortProtonQt.git|" \
build-aux/fedora-git.spec > /home/rpmbuild/SPECS/${{ env.PACKAGE }}.spec
chown -R rpmbuild:users /home/rpmbuild chown -R rpmbuild:users /home/rpmbuild
- name: Build RPM - name: Build RPM
@@ -88,7 +98,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:ebcaeca69c4d416f848aedcd27fe224384fd506f86046526a5d49ec6d9e29db1 image: archlinux:base-devel@sha256:d2bd09bd30dc1199ba6f35bc575292957a4a24377fb137d0f999817bc29adc17
steps: steps:
- name: Prepare container - name: Prepare container
@@ -115,17 +125,20 @@ jobs:
chown user -R /tmp chown user -R /tmp
chown user -R .. chown user -R ..
- name: Checkout
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
ref: ${{ env.BUILD_BRANCH }}
- name: Build - name: Build
run: | run: |
cd /__w/portproton-repo cd /__w/portproton-repo
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git cp -r "$GITHUB_WORKSPACE" PortProtonQt
cd /__w/portproton-repo/PortProtonQt/build-aux cd /__w/portproton-repo/PortProtonQt/build-aux
sed -i "s|source=(\"git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git\")|source=(\"git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git#branch=${{ env.BUILD_BRANCH }}\")|" PKGBUILD-git
chown user -R .. chown user -R ..
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git" su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Upload Arch package - name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4 uses: https://gitea.com/actions/gitea-upload-artifact@v4
with: with:

View File

@@ -77,17 +77,17 @@ jobs:
chown user -R /tmp chown user -R /tmp
chown user -R .. chown user -R ..
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
- name: Build - name: Build
run: | run: |
cd /__w/portproton-repo cd /__w/portproton-repo
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git cp -r "$GITHUB_WORKSPACE" PortProtonQt
cd /__w/portproton-repo/PortProtonQt/build-aux cd /__w/portproton-repo/PortProtonQt/build-aux
chown user -R .. chown user -R ..
su user -c "yes '' | makepkg --noconfirm -s" su user -c "yes '' | makepkg --noconfirm -s"
- name: Checkout
uses: https://gitea.com/actions/checkout@v4
- 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
with: with:
@@ -109,9 +109,8 @@ jobs:
steps: steps:
- 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 meson ninja-build python3-devel \
python3-build pyproject-rpm-macros systemd-rpm-macros python3-setuptools \ systemd-rpm-macros redhat-rpm-config nodejs npm
redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment - name: Setup rpmbuild environment
run: | run: |

View File

@@ -1,4 +1,4 @@
name: Check Translations (disabled until yaspeller is fixed) name: Check Translations
run-name: Check spelling in translation files run-name: Check spelling in translation files
on: on:
push: push:
@@ -18,7 +18,7 @@ jobs:
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Python - name: Set up Python
uses: https://gitea.com/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 uses: https://gitea.com/actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with: with:
python-version-file: "pyproject.toml" python-version-file: "pyproject.toml"

View File

@@ -9,6 +9,8 @@ on:
env: env:
PKGDEST: "/tmp/portprotonqt" PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt" PACKAGE: "portprotonqt"
PR_REPO_URL: ${{ github.event.pull_request.head.repo.clone_url }}
PR_BRANCH: ${{ github.head_ref }}
jobs: jobs:
changes: changes:
@@ -63,7 +65,7 @@ jobs:
needs: changes needs: changes
if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch' if: needs.changes.outputs.appimage == 'true' || github.event_name == 'workflow_dispatch'
container: container:
image: archlinux:base-devel@sha256:ebcaeca69c4d416f848aedcd27fe224384fd506f86046526a5d49ec6d9e29db1 image: archlinux:base-devel@sha256:d2bd09bd30dc1199ba6f35bc575292957a4a24377fb137d0f999817bc29adc17
options: --privileged --device /dev/fuse options: --privileged --device /dev/fuse
steps: steps:
- name: Prepare container - name: Prepare container
@@ -78,7 +80,7 @@ jobs:
run: | run: |
cd build-aux/AppImage cd build-aux/AppImage
chmod +x get-dependencies.sh portprotonqt-appimage.sh chmod +x get-dependencies.sh portprotonqt-appimage.sh
./get-dependencies.sh ./get-dependencies.sh --local --branch "${{ env.PR_BRANCH }}" --repo "${{ env.PR_REPO_URL }}"
- name: Build AppImage - name: Build AppImage
run: | run: |
@@ -108,9 +110,8 @@ jobs:
steps: steps:
- 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 meson ninja-build python3-devel \
python3-build pyproject-rpm-macros python3-setuptools \ systemd-rpm-macros redhat-rpm-config nodejs npm
redhat-rpm-config nodejs npm
- name: Setup rpmbuild environment - name: Setup rpmbuild environment
run: | run: |
@@ -124,7 +125,8 @@ jobs:
- name: Copy fedora-git.spec - name: Copy fedora-git.spec
run: | run: |
cp build-aux/fedora-git.spec /home/rpmbuild/SPECS/${{ env.PACKAGE }}.spec sed "s|git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git|git clone -b ${{ env.PR_BRANCH }} ${{ env.PR_REPO_URL }}|" \
build-aux/fedora-git.spec > /home/rpmbuild/SPECS/${{ env.PACKAGE }}.spec
chown -R rpmbuild:users /home/rpmbuild chown -R rpmbuild:users /home/rpmbuild
- name: Build RPM - name: Build RPM
@@ -143,7 +145,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:ebcaeca69c4d416f848aedcd27fe224384fd506f86046526a5d49ec6d9e29db1 image: archlinux:base-devel@sha256:d2bd09bd30dc1199ba6f35bc575292957a4a24377fb137d0f999817bc29adc17
steps: steps:
- name: Prepare container - name: Prepare container
@@ -170,17 +172,18 @@ jobs:
chown user -R /tmp chown user -R /tmp
chown user -R .. chown user -R ..
- name: Checkout
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Build - name: Build
run: | run: |
cd /__w/portproton-repo cd /__w/portproton-repo
git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git git clone -b ${{ env.PR_BRANCH }} ${{ env.PR_REPO_URL }} PortProtonQt
cd /__w/portproton-repo/PortProtonQt/build-aux cd /__w/portproton-repo/PortProtonQt/build-aux
sed -i "s|source=(\"git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git\")|source=(\"git+${{ env.PR_REPO_URL }}#branch=${{ env.PR_BRANCH }}\")|" PKGBUILD-git
chown user -R .. chown user -R ..
su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git" su user -c "yes '' | makepkg --noconfirm -s -p PKGBUILD-git"
- name: Checkout
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Upload Arch package - name: Upload Arch package
uses: https://gitea.com/actions/gitea-upload-artifact@v4 uses: https://gitea.com/actions/gitea-upload-artifact@v4
with: with:

View File

@@ -23,7 +23,7 @@ jobs:
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- 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@6044e13b5dc448c55e2357c09f80417699197238 # v6
with: with:
node-version: 20 node-version: 20

View File

@@ -14,7 +14,7 @@ jobs:
uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Python - name: Set up Python
uses: https://gitea.com/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 uses: https://gitea.com/actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with: with:
python-version-file: "pyproject.toml" python-version-file: "pyproject.toml"

View File

@@ -13,7 +13,7 @@ jobs:
- uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- 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@6044e13b5dc448c55e2357c09f80417699197238 # v6
with: with:
node-version: 20 node-version: 20

View File

@@ -2,22 +2,52 @@
set -eu set -eu
# Determine if git mode is enabled based on the first argument # Initialize variables
if [ "${1:-}" = "--git" ] || [ "${1:-}" = "-g" ]; then LOCAL_MODE=false
GIT_MODE=true BRANCH="main"
else REPO_URL="https://git.linux-gaming.ru/Boria138/PortProtonQt.git"
GIT_MODE=false
fi # Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
--local|-l)
LOCAL_MODE=true
;;
--branch)
if [ -n "${2:-}" ] && [ "${2#-}" = "$2" ]; then
BRANCH="$2"
shift
else
echo "Error: --branch requires an argument"
exit 1
fi
;;
--repo)
if [ -n "${2:-}" ] && [ "${2#-}" = "$2" ]; then
REPO_URL="$2"
shift
else
echo "Error: --repo requires an argument"
exit 1
fi
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
shift
done
ARCH="$(uname -m)" ARCH="$(uname -m)"
PACKAGE_BUILDER="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/make-aur-package.sh" PACKAGE_BUILDER="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/make-aur-package.sh"
EXTRA_PACKAGES="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/get-debloated-pkgs.sh" EXTRA_PACKAGES="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/get-debloated-pkgs.sh"
if [ "$GIT_MODE" = true ]; then if [ "$LOCAL_MODE" = true ]; then
echo "Using git version of PortProtonQt..." echo "Using local PKGBUILD-git from repository..."
PPQT_PKGBUILD="https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/build-aux/PKGBUILD-git" PPQT_PKGBUILD=""
else else
echo "Using stable version of PortProtonQt..." echo "Using stable version of PortProtonQt from main branch..."
PPQT_PKGBUILD="https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/build-aux/PKGBUILD" PPQT_PKGBUILD="https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/build-aux/PKGBUILD"
fi fi
@@ -37,7 +67,12 @@ chmod +x ./make-aur-package.sh
echo "Building PortProtonQt from PKGBUILD..." echo "Building PortProtonQt from PKGBUILD..."
echo "---------------------------------------------------------------" echo "---------------------------------------------------------------"
wget --retry-connrefused --tries=30 "$PPQT_PKGBUILD" -O ./PKGBUILD if [ "$LOCAL_MODE" = true ]; then
cp ../PKGBUILD-git ./PKGBUILD
else
wget --retry-connrefused --tries=30 "$PPQT_PKGBUILD" -O ./PKGBUILD
fi
sed -i "s|source=(\"git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git\")|source=(\"git+${REPO_URL}#branch=$BRANCH\")|" PKGBUILD
makepkg -si --noconfirm makepkg -si --noconfirm
echo "Installing debloated packages..." echo "Installing debloated packages..."
@@ -46,7 +81,7 @@ wget --retry-connrefused --tries=30 "$EXTRA_PACKAGES" -O ./get-debloated-pkgs.sh
chmod +x ./get-debloated-pkgs.sh chmod +x ./get-debloated-pkgs.sh
./get-debloated-pkgs.sh --add-common --prefer-nano ./get-debloated-pkgs.sh --add-common --prefer-nano
if [ "$GIT_MODE" = true ]; then if [ "$LOCAL_MODE" = true ]; then
# For git version, we use portprotonqt-git # For git version, we use portprotonqt-git
pacman -Q portprotonqt-git | awk '{print $2}' | cut -d- -f1 > ~/version pacman -Q portprotonqt-git | awk '{print $2}' | cut -d- -f1 > ~/version
else else

View File

@@ -6,19 +6,16 @@ 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-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' depends=('python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' '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' 'python-libarchive-c' 'pyside6' 'python-rapidfuzz' 'icoextract' 'python-pillow' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar' 'qt6-svg')
makedepends=('python-'{'build','installer','setuptools','wheel'}) makedepends=('meson' 'ninja')
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')
build() { build() {
cd "$srcdir/PortProtonQt" arch-meson PortProtonQt build
python -m build --wheel --no-isolation meson compile -C build
} }
package() { package() {
cd "$srcdir/PortProtonQt" meson install -C build --destdir "$pkgdir"
python -m installer --destdir="$pkgdir" dist/*.whl
cp -r build-aux/share "$pkgdir/usr/"
cp -r build-aux/lib "$pkgdir/usr/"
} }

View File

@@ -6,8 +6,8 @@ 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-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson' depends=('python-requests' 'python-babel' 'python-evdev' 'python-pyudev' 'python-orjson'
'python-psutil' 'python-tqdm' 'python-vdf' 'python-libarchive-c' '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' 'python-libarchive-c' 'pyside6' 'icoextract' 'python-pillow' 'python-rapidfuzz' 'perl-image-exiftool' 'xdg-utils' 'python-beautifulsoup4' 'python-websocket-client' 'cabextract' 'unzip' 'curl' 'unrar' 'qt6-svg')
makedepends=('python-'{'build','installer','setuptools','wheel'}) makedepends=('meson' 'ninja')
source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git") source=("git+https://git.linux-gaming.ru/Boria138/PortProtonQt.git")
sha256sums=('SKIP') sha256sums=('SKIP')
@@ -17,13 +17,10 @@ pkgver() {
} }
build() { build() {
cd "$srcdir/PortProtonQt" arch-meson PortProtonQt build
python -m build --wheel --no-isolation meson compile -C build
} }
package() { package() {
cd "$srcdir/PortProtonQt" meson install -C build --destdir "$pkgdir"
python -m installer --destdir="$pkgdir" dist/*.whl
cp -r build-aux/share "$pkgdir/usr/"
cp -r build-aux/lib "$pkgdir/usr/"
} }

View File

@@ -1,12 +1,12 @@
%global pypi_name portprotonqt %global pypi_name portprotonqt
%global pypi_version 0.1.1 %global pypi_version 0.1.10
%global oname PortProtonQt %global oname PortProtonQt
%global build_timestamp %(date +"%Y%m%d") %global build_timestamp %(date +"%Y%m%d")
%global _python_no_extras_requires 1 %global _python_no_extras_requires 1
%global rel_build 1.git.%{build_timestamp}%{?dist} %global rel_build 1.git.%{build_timestamp}%{?dist}
Name: python-%{pypi_name}-git Name: %{pypi_name}-git
Version: %{pypi_version} Version: %{pypi_version}
Release: %{rel_build} Release: %{rel_build}
Summary: Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store (development build) Summary: Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store (development build)
@@ -15,21 +15,15 @@ License: GPL-3.0
URL: https://git.linux-gaming.ru/Boria138/PortProtonQt URL: https://git.linux-gaming.ru/Boria138/PortProtonQt
BuildArch: noarch BuildArch: noarch
BuildRequires: meson >= 0.61.2
BuildRequires: ninja-build
BuildRequires: python3-devel BuildRequires: python3-devel
BuildRequires: python3-wheel
BuildRequires: python3-pip
BuildRequires: python3-build
BuildRequires: pyproject-rpm-macros
BuildRequires: python3dist(setuptools)
BuildRequires: git BuildRequires: git
BuildRequires: systemd-rpm-macros BuildRequires: systemd-rpm-macros
%description Obsoletes: python3-%{pypi_name}-git < %{version}-%{release}
%{summary} Provides: python3-%{pypi_name}-git = %{version}-%{release}
%package -n python3-%{pypi_name}-git
Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3-babel Requires: python3-babel
Requires: python3-evdev Requires: python3-evdev
Requires: python3-icoextract Requires: python3-icoextract
@@ -48,13 +42,14 @@ Requires: python3-rapidfuzz
Requires: python3-libarchive-c Requires: python3-libarchive-c
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils
Requires: qt6-qtsvg
Requires: cabextract Requires: cabextract
Requires: gzip Requires: gzip
Requires: unzip Requires: unzip
Requires: curl Requires: curl
Requires: unrar Requires: unrar
%description -n python3-%{pypi_name}-git %description
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.
%{?python_disable_dependency_generator} %{?python_disable_dependency_generator}
@@ -64,17 +59,17 @@ git clone https://git.linux-gaming.ru/Boria138/PortProtonQt.git
%build %build
cd %{oname} cd %{oname}
%pyproject_wheel %meson -Dpython_libdir=%{python3_sitelib} -Dudevdir=%{_udevrulesdir}
%meson_build
%install %install
cd %{oname} cd %{oname}
%pyproject_install %meson_install
%pyproject_save_files %{pypi_name} %find_lang %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name}-git -f %{pyproject_files} %files -f %{oname}/%{pypi_name}.lang
%{_bindir}/%{pypi_name} %{_bindir}/%{pypi_name}
%{python3_sitelib}/%{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 %{_udevrulesdir}/60-portprotonqt.rules

View File

@@ -3,7 +3,7 @@
%global oname PortProtonQt %global oname PortProtonQt
%global _python_no_extras_requires 1 %global _python_no_extras_requires 1
Name: python-%{pypi_name} Name: %{pypi_name}
Version: %{pypi_version} Version: %{pypi_version}
Release: 1%{?dist} Release: 1%{?dist}
Summary: Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store Summary: Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store
@@ -12,21 +12,15 @@ License: GPL-3.0
URL: https://git.linux-gaming.ru/Boria138/PortProtonQt URL: https://git.linux-gaming.ru/Boria138/PortProtonQt
BuildArch: noarch BuildArch: noarch
BuildRequires: meson >= 0.61.2
BuildRequires: ninja-build
BuildRequires: python3-devel BuildRequires: python3-devel
BuildRequires: python3-wheel
BuildRequires: python3-pip
BuildRequires: python3-build
BuildRequires: pyproject-rpm-macros
BuildRequires: python3dist(setuptools)
BuildRequires: git BuildRequires: git
BuildRequires: systemd-rpm-macros BuildRequires: systemd-rpm-macros
%description Obsoletes: python3-%{pypi_name} < %{version}-%{release}
%{summary} Provides: python3-%{pypi_name} = %{version}-%{release}
%package -n python3-%{pypi_name}
Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3-babel Requires: python3-babel
Requires: python3-evdev Requires: python3-evdev
Requires: python3-icoextract Requires: python3-icoextract
@@ -45,13 +39,14 @@ Requires: python3-rapidfuzz
Requires: python3-libarchive-c Requires: python3-libarchive-c
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils
Requires: qt6-qtsvg
Requires: cabextract Requires: cabextract
Requires: gzip Requires: gzip
Requires: unzip Requires: unzip
Requires: curl Requires: curl
Requires: unrar Requires: unrar
%description -n python3-%{pypi_name} %description
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.
%{?python_disable_dependency_generator} %{?python_disable_dependency_generator}
@@ -63,17 +58,17 @@ git checkout v%{pypi_version}
%build %build
cd %{oname} cd %{oname}
%pyproject_wheel %meson -Dpython_libdir=%{python3_sitelib} -Dudevdir=%{_udevrulesdir}
%meson_build
%install %install
cd %{oname} cd %{oname}
%pyproject_install %meson_install
%pyproject_save_files %{pypi_name} %find_lang %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name} -f %{pyproject_files} %files -f %{oname}/%{pypi_name}.lang
%{_bindir}/%{pypi_name} %{_bindir}/%{pypi_name}
%{python3_sitelib}/%{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 %{_udevrulesdir}/60-portprotonqt.rules

9
build-aux/portprotonqt Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python3
import sys
from portprotonqt.app import main
if __name__ == "__main__":
sys.exit(main())

View File

@@ -12,7 +12,9 @@ BASE_DIR = Path(__file__).parent.parent
APPIMAGE_RECIPE = BASE_DIR / "build-aux" / "AppImageBuilder.yml" APPIMAGE_RECIPE = BASE_DIR / "build-aux" / "AppImageBuilder.yml"
ARCH_PKGBUILD = BASE_DIR / "build-aux" / "PKGBUILD" ARCH_PKGBUILD = BASE_DIR / "build-aux" / "PKGBUILD"
FEDORA_SPEC = BASE_DIR / "build-aux" / "fedora.spec" FEDORA_SPEC = BASE_DIR / "build-aux" / "fedora.spec"
FEDORA_GIT_SPEC = BASE_DIR / "build-aux" / "fedora-git.spec"
PYPROJECT = BASE_DIR / "pyproject.toml" PYPROJECT = BASE_DIR / "pyproject.toml"
MESON_BUILD = BASE_DIR / "meson.build"
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"
@@ -56,6 +58,7 @@ def bump_fedora(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_pyproject(path: Path, old: str, new: str) -> bool: def bump_pyproject(path: Path, old: str, new: str) -> bool:
""" """
Update version in pyproject.toml under [project] Update version in pyproject.toml under [project]
@@ -69,6 +72,19 @@ def bump_pyproject(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_meson(path: Path, old: str, new: str) -> bool:
"""
Update version in meson.build
"""
if not path.exists():
return False
text = path.read_text(encoding='utf-8')
pattern = re.compile(r"(version:\s*)'" + re.escape(old) + r"'")
new_text, count = pattern.subn(lambda m: m.group(1) + f"'{new}'", text)
if count:
path.write_text(new_text, encoding='utf-8')
return bool(count)
def bump_app_py(path: Path, old: str, new: str) -> bool: def bump_app_py(path: Path, old: str, new: str) -> bool:
""" """
Update __app_version__ in app.py Update __app_version__ in app.py
@@ -120,7 +136,9 @@ def main():
(APPIMAGE_RECIPE, bump_appimage), (APPIMAGE_RECIPE, bump_appimage),
(ARCH_PKGBUILD, bump_arch), (ARCH_PKGBUILD, bump_arch),
(FEDORA_SPEC, bump_fedora), (FEDORA_SPEC, bump_fedora),
(FEDORA_GIT_SPEC, bump_fedora),
(PYPROJECT, bump_pyproject), (PYPROJECT, bump_pyproject),
(MESON_BUILD, bump_meson),
(APP_PY, bump_app_py), (APP_PY, bump_app_py),
(GITEA_WORKFLOW, bump_workflow), (GITEA_WORKFLOW, bump_workflow),
(CHANGELOG, bump_changelog) (CHANGELOG, bump_changelog)

View File

@@ -17,8 +17,9 @@ README_EN = GUIDE_DIR / "README.md"
README_RU = GUIDE_DIR / "README.ru.md" README_RU = GUIDE_DIR / "README.ru.md"
LOCALES_PATH = Path(__file__).parent.parent / "portprotonqt" / "locales" LOCALES_PATH = Path(__file__).parent.parent / "portprotonqt" / "locales"
THEMES_PATH = Path(__file__).parent.parent / "portprotonqt" / "themes" THEMES_PATH = Path(__file__).parent.parent / "portprotonqt" / "themes"
MESON_BUILD = Path(__file__).parent.parent / "portprotonqt" / "meson.build"
README_FILES = [README_EN, README_RU] README_FILES = [README_EN, README_RU]
POT_FILE = LOCALES_PATH / "messages.pot" POT_FILE = LOCALES_PATH / "portprotonqt.pot"
# ---------- Версия проекта ---------- # ---------- Версия проекта ----------
def _get_version() -> str: def _get_version() -> str:
@@ -27,16 +28,16 @@ def _get_version() -> str:
# ---------- Обновление README ---------- # ---------- Обновление README ----------
def _update_coverage(lines: list[str]) -> None: def _update_coverage(lines: list[str]) -> None:
# Парсим статистику из вывода pybabel --statistics # Парсим статистику из вывода pybabel --statistics
locales_stats = [line for line in lines if line.endswith(".po")] locales_stats = [line for line in lines if "portprotonqt.po" in line]
# Извлекаем (count, pct, locale) и сортируем # Извлекаем (count, pct, locale) и сортируем
rows = sorted( rows = sorted(
(m := re.search( row for stat in locales_stats
if (m := re.search(
r"""(\d+\ of\ \d+).* # message counts r"""(\d+\ of\ \d+).* # message counts
\((\d+\%)\).* # message percentage \((\d+\%)\).* # message percentage
locales\/(.*)\/LC_MESSAGES # locale name""", locales\/([^/]+)\/LC_MESSAGES # locale name""",
stat, re.VERBOSE stat, re.VERBOSE
)) and m.groups() )) and (row := m.groups())
for stat in locales_stats
) )
for md_file in README_FILES: for md_file in README_FILES:
@@ -59,14 +60,14 @@ def _update_coverage(lines: list[str]) -> None:
"| Локаль | Прогресс | Переведено |\n" "| Локаль | Прогресс | Переведено |\n"
"| :----- | -------: | ---------: |\n" "| :----- | -------: | ---------: |\n"
) )
fmt = lambda count, pct, loc: f"| [{loc}](./{loc}/LC_MESSAGES/messages.po) | {pct} | {count.replace(' of ', ' из ')} |" fmt = lambda count, pct, loc: f"| [{loc}](./{loc}/LC_MESSAGES/portprotonqt.po) | {pct} | {count.replace(' of ', ' из ')} |"
else: else:
table_header = ( table_header = (
"<!-- Auto-generated coverage table -->\n\n" "<!-- Auto-generated coverage table -->\n\n"
"| Locale | Progress | Translated |\n" "| Locale | Progress | Translated |\n"
"| :----- | -------: | ---------: |\n" "| :----- | -------: | ---------: |\n"
) )
fmt = lambda count, pct, loc: f"| [{loc}](./{loc}/LC_MESSAGES/messages.po) | {pct} | {count} |" fmt = lambda count, pct, loc: f"| [{loc}](./{loc}/LC_MESSAGES/portprotonqt.po) | {pct} | {count} |"
# Собираем строки и добавляем '---' в конце # Собираем строки и добавляем '---' в конце
coverage_table = ( coverage_table = (
@@ -100,7 +101,7 @@ def _update_coverage(lines: list[str]) -> None:
def compile_locales() -> None: def compile_locales() -> None:
CommandLineInterface().run([ CommandLineInterface().run([
"pybabel", "compile", "--use-fuzzy", "--directory", "pybabel", "compile", "--use-fuzzy", "--directory",
f"{LOCALES_PATH.resolve()}", "--statistics" f"{LOCALES_PATH.resolve()}", "--domain=portprotonqt", "--statistics"
]) ])
def extract_strings() -> None: def extract_strings() -> None:
@@ -121,10 +122,39 @@ def update_locales() -> None:
"pybabel", "update", "pybabel", "update",
f"--input-file={POT_FILE.resolve()}", f"--input-file={POT_FILE.resolve()}",
f"--output-dir={LOCALES_PATH.resolve()}", f"--output-dir={LOCALES_PATH.resolve()}",
"--domain=portprotonqt",
"--ignore-obsolete", "--ignore-obsolete",
"--update-header-comment", "--update-header-comment",
]) ])
def _update_meson_locales(new_locales: list[str]) -> None:
"""Обновляет список языков в meson.build."""
if not MESON_BUILD.exists():
return
text = MESON_BUILD.read_text(encoding="utf-8")
# Ищем foreach lang : ['de', 'es', 'pt', 'ru']
pattern = r"(foreach\s+lang\s*:\s*\[)([^\]]+)(\])"
match = re.search(pattern, text)
if not match:
return
# Парсим текущий список языков
current_langs_str = match.group(2)
current_langs = re.findall(r"'([^']+)'", current_langs_str)
# Добавляем новые языки и сортируем
all_langs = sorted(set(current_langs) | set(new_locales))
# Формируем новый список
new_langs_str = ", ".join(f"'{lang}'" for lang in all_langs)
new_text = text[:match.start()] + match.group(1) + new_langs_str + match.group(3) + text[match.end():]
if new_text != text:
MESON_BUILD.write_text(new_text, encoding="utf-8")
print(f"Updated meson.build with locales: {all_langs}")
def create_new(locales: list[str]) -> None: def create_new(locales: list[str]) -> None:
if not POT_FILE.exists(): if not POT_FILE.exists():
extract_strings() extract_strings()
@@ -133,8 +163,11 @@ def create_new(locales: list[str]) -> None:
"pybabel", "init", "pybabel", "init",
f"--input-file={POT_FILE.resolve()}", f"--input-file={POT_FILE.resolve()}",
f"--output-dir={LOCALES_PATH.resolve()}", f"--output-dir={LOCALES_PATH.resolve()}",
"--domain=portprotonqt",
f"--locale={locale}" f"--locale={locale}"
]) ])
# Обновляем meson.build с новыми локалями
_update_meson_locales(locales)
# ---------- Игнорируемые префиксы для spellcheck ---------- # ---------- Игнорируемые префиксы для spellcheck ----------
IGNORED_PREFIXES = () IGNORED_PREFIXES = ()
@@ -148,6 +181,41 @@ def load_ignored_prefixes(ignore_file=".spellignore"):
IGNORED_PREFIXES = load_ignored_prefixes() + ("PortProton", "flatpak") IGNORED_PREFIXES = load_ignored_prefixes() + ("PortProton", "flatpak")
# ---------- Проверка fuzzy строк ----------
def find_fuzzy_entries(filepath: Path) -> list[tuple[int, str, str]]:
"""Находит fuzzy записи в .po файле. Возвращает список (номер_строки, msgid, флаги)."""
fuzzy_entries = []
lines = filepath.read_text(encoding='utf-8').splitlines()
i = 0
while i < len(lines):
line = lines[i].strip()
# Ищем комментарий с флагами, содержащий fuzzy
if line.startswith('#,') and 'fuzzy' in line:
flags = line[2:].strip()
line_num = i + 1
# Ищем следующий msgid
msgid = ""
i += 1
while i < len(lines):
next_line = lines[i].strip()
if next_line.startswith('msgid '):
match = re.match(r'^msgid\s+"(.*)"', next_line)
if match:
msgid = match.group(1)
i += 1
# Собираем многострочный msgid
while i < len(lines) and lines[i].strip().startswith('"'):
msgid += lines[i].strip()[1:-1]
i += 1
break
i += 1
# Пропускаем пустой msgid (заголовок PO файла)
if msgid:
fuzzy_entries.append((line_num, msgid, flags))
else:
i += 1
return fuzzy_entries
# ---------- Проверка орфографии с параллелизмом ---------- # ---------- Проверка орфографии с параллелизмом ----------
speller = YandexSpeller() speller = YandexSpeller()
MSGID_RE = re.compile(r'^msgid\s+"(.*)"') MSGID_RE = re.compile(r'^msgid\s+"(.*)"')
@@ -208,17 +276,36 @@ def main(args) -> int:
if args.create_new: if args.create_new:
create_new(args.create_new) create_new(args.create_new)
if args.spellcheck: if args.spellcheck:
files = list(LOCALES_PATH.glob("**/*.po")) + [POT_FILE] files = list(LOCALES_PATH.glob("**/portprotonqt.po")) + [POT_FILE]
seen = set(); has_err = False seen = set(); has_err = False
issues_summary = defaultdict(list) issues_summary = defaultdict(list)
fuzzy_summary = defaultdict(list)
for f in files: for f in files:
if not f.exists() or f in seen: continue if not f.exists() or f in seen: continue
seen.add(f) seen.add(f)
# Проверка fuzzy строк (только для .po файлов)
if f.suffix == '.po':
fuzzy_entries = find_fuzzy_entries(f)
if fuzzy_entries:
fuzzy_summary[f] = fuzzy_entries
has_err = True
if check_file(f, issues_summary): if check_file(f, issues_summary):
has_err = True has_err = True
else: else:
print(f"{f} — no errors found.") if f not in fuzzy_summary:
if has_err: print(f"{f} — no errors found.")
# Вывод fuzzy строк
if fuzzy_summary:
print("\n⚠️ Fuzzy Entries (require review before release):")
for file, entries in fuzzy_summary.items():
print(f"\n{file}")
print("-----")
for idx, (line_num, msgid, flags) in enumerate(entries, 1):
print(f"{idx}. Line {line_num}: [{flags}]")
print(f" msgid: \"{msgid[:80]}{'...' if len(msgid) > 80 else ''}\"")
print("-----")
# Вывод орфографических ошибок
if issues_summary:
print("\n📋 Summary of Spelling Errors:") print("\n📋 Summary of Spelling Errors:")
for file, errs in issues_summary.items(): for file, errs in issues_summary.items():
print(f"\n{file}") print(f"\n{file}")

View File

@@ -13,7 +13,7 @@
## 📖 Overview ## 📖 Overview
Localization in `PortProtonQT` is powered by `Babel` using `.po/.mo` files stored under `LC_MESSAGES/messages.po` for each language. Localization in `PortProtonQT` is powered by `Babel` using `.po/.mo` files stored under `LC_MESSAGES/portprotonqt.po` for each language.
Current translation status: Current translation status:
@@ -21,9 +21,10 @@ Current translation status:
| Locale | Progress | Translated | | Locale | Progress | Translated |
| :----- | -------: | ---------: | | :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 of 376 | | [de](./de/LC_MESSAGES/portprotonqt.po) | 0% | 0 of 376 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 of 376 | | [es](./es/LC_MESSAGES/portprotonqt.po) | 0% | 0 of 376 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 376 of 376 | | [pt](./pt/LC_MESSAGES/portprotonqt.po) | 0% | 0 of 376 |
| [ru](./ru/LC_MESSAGES/portprotonqt.po) | 100% | 376 of 376 |
--- ---
@@ -39,7 +40,7 @@ source .venv/bin/activate
python dev-scripts/l10n.py --create-new <locale_code> python dev-scripts/l10n.py --create-new <locale_code>
``` ```
2. Edit the file `portprotonqt/locales/<locale>/LC_MESSAGES/messages.po` in Poedit or any text editor. 2. Edit the file `portprotonqt/locales/<locale>/LC_MESSAGES/portprotonqt.po` in Poedit or any text editor.
--- ---

View File

@@ -13,7 +13,7 @@
## 📖 Обзор ## 📖 Обзор
Локализация в `PortProtonQT` осуществляется через систему `.po/.mo` файлов и управляется утилитой `Babel`. Все переводы находятся в подкаталогах вида `LC_MESSAGES/messages.po` для каждой поддерживаемой локали. Локализация в `PortProtonQT` осуществляется через систему `.po/.mo` файлов и управляется утилитой `Babel`. Все переводы находятся в подкаталогах вида `LC_MESSAGES/portprotonqt.po` для каждой поддерживаемой локали.
Текущий статус перевода: Текущий статус перевода:
@@ -21,9 +21,10 @@
| Локаль | Прогресс | Переведено | | Локаль | Прогресс | Переведено |
| :----- | -------: | ---------: | | :----- | -------: | ---------: |
| [de_DE](./de_DE/LC_MESSAGES/messages.po) | 0% | 0 из 376 | | [de](./de/LC_MESSAGES/portprotonqt.po) | 0% | 0 из 376 |
| [es_ES](./es_ES/LC_MESSAGES/messages.po) | 0% | 0 из 376 | | [es](./es/LC_MESSAGES/portprotonqt.po) | 0% | 0 из 376 |
| [ru_RU](./ru_RU/LC_MESSAGES/messages.po) | 100% | 376 из 376 | | [pt](./pt/LC_MESSAGES/portprotonqt.po) | 0% | 0 из 376 |
| [ru](./ru/LC_MESSAGES/portprotonqt.po) | 100% | 376 из 376 |
--- ---
@@ -39,7 +40,7 @@ source .venv/bin/activate
python dev-scripts/l10n.py --create-new <код_локали> python dev-scripts/l10n.py --create-new <код_локали>
``` ```
2. Отредактируйте файл `portprotonqt/locales/<локаль>/LC_MESSAGES/messages.po` в Poedit или любом текстовом редакторе. 2. Отредактируйте файл `portprotonqt/locales/<локаль>/LC_MESSAGES/portprotonqt.po` в Poedit или любом текстовом редакторе.
--- ---

83
meson.build Normal file
View File

@@ -0,0 +1,83 @@
project('portprotonqt',
version: '0.1.10',
meson_version: '>= 0.61.2',
license: 'GPL-3.0',
)
# Project directories
prefix = get_option('prefix')
datadir = prefix / get_option('datadir')
bindir = prefix / get_option('bindir')
# Python module for site-packages path
python = import('python')
python3 = python.find_installation('python3')
pythondir = join_paths(prefix, python3.get_path('purelib'))
python_libdir = get_option('python_libdir')
if python_libdir == ''
python_installdir = pythondir
else
python_installdir = python_libdir
endif
pkgdatadir = python_installdir / meson.project_name()
conf = configuration_data()
conf.set('PYTHON', python.find_installation('python3').full_path())
# Install Python package
subdir('portprotonqt')
# Install entry point script with proper name to avoid conflict in build directory
configured_portprotonqt = configure_file(
input: 'build-aux/portprotonqt',
output: 'portprotonqt-script',
configuration: conf,
)
# Install the configured script with the correct name
install_data(configured_portprotonqt,
install_dir: bindir,
install_mode: 'rwxr-xr-x',
rename: 'portprotonqt')
# Install desktop file
install_data(
'build-aux/share/applications/ru.linux_gaming.PortProtonQt.desktop',
install_dir: datadir / 'applications',
)
# Install icon
install_data(
'build-aux/share/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg',
install_dir: datadir / 'icons/hicolor/scalable/apps',
)
# Install metainfo
install_data(
'build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml',
install_dir: datadir / 'metainfo',
)
# Install bash completion
install_data(
'build-aux/share/bash-completion/completions/portprotonqt',
install_dir: datadir / 'bash-completion/completions',
)
# Install udev rules
udevdir = get_option('udevdir')
if udevdir == ''
udev = dependency('udev', required: false)
if udev.found()
udevdir = udev.get_variable(pkgconfig: 'udevdir') / 'rules.d'
else
udevdir = prefix / get_option('libdir') / 'udev/rules.d'
endif
endif
install_data(
'build-aux/lib/udev/rules.d/60-portprotonqt.rules',
install_dir: udevdir,
)

11
meson_options.txt Normal file
View File

@@ -0,0 +1,11 @@
option('udevdir',
type: 'string',
value: '',
description: 'Directory for udev rules (auto-detected if empty)',
)
option('python_libdir',
type: 'string',
value: '',
description: 'Python site-packages directory (auto-detected if empty)',
)

View File

@@ -28,7 +28,7 @@ class DetailPageManager:
self._animations = {} self._animations = {}
self.portproton_api = PortProtonAPI(Downloader(max_workers=4)) self.portproton_api = PortProtonAPI(Downloader(max_workers=4))
def openGameDetailPage(self, name, description, cover_path=None, appid="", exec_line="", controller_support="", def openGameDetailPage(self, name, description, cover_path=None, appid="", controller_support="", exec_line="",
last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""): last_launch="", formatted_playtime="", protondb_tier="", game_source="", anticheat_status=""):
"""Open detailed game information page showing all game stats, playtime and settings.""" """Open detailed game information page showing all game stats, playtime and settings."""
detailPage = QWidget() detailPage = QWidget()
@@ -210,7 +210,7 @@ class DetailPageManager:
portprotonLabel.setStyleSheet(self.main_window.theme.STEAM_BADGE_STYLE) portprotonLabel.setStyleSheet(self.main_window.theme.STEAM_BADGE_STYLE)
portprotonLabel.setFixedWidth(badge_width) portprotonLabel.setFixedWidth(badge_width)
portprotonLabel.setVisible(portproton_visible) portprotonLabel.setVisible(portproton_visible)
portprotonLabel.clicked.connect(lambda: self.open_portproton_forum_topic(name)) portprotonLabel.clicked.connect(lambda: self.portproton_api.open_ppdb_page(name, exec_line))
# WeAntiCheatYet badge # WeAntiCheatYet badge
anticheat_text = GameCard.getAntiCheatText(anticheat_status) anticheat_text = GameCard.getAntiCheatText(anticheat_status)
@@ -869,12 +869,3 @@ class DetailPageManager:
except Exception as e: except Exception as e:
logger.error(f"Error starting exit animation: {e}", exc_info=True) logger.error(f"Error starting exit animation: {e}", exc_info=True)
self._exit_animation_in_progress = False self._exit_animation_in_progress = False
def open_portproton_forum_topic(self, name):
result = self.portproton_api.get_forum_topic_slug(name)
base_url = "https://linux-gaming.ru/"
if result.startswith("search?q="):
url = QUrl(f"{base_url}{result}")
else:
url = QUrl(f"{base_url}t/{result}")
QDesktopServices.openUrl(url)

View File

@@ -152,7 +152,7 @@ class GameCard(QFrame):
self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE) self.portprotonLabel.setStyleSheet(self.theme.STEAM_BADGE_STYLE)
self.portprotonLabel.setCardWidth(card_width) self.portprotonLabel.setCardWidth(card_width)
self.portprotonLabel.setVisible(self.portproton_visible) self.portprotonLabel.setVisible(self.portproton_visible)
self.portprotonLabel.clicked.connect(self.open_portproton_forum_topic) self.portprotonLabel.clicked.connect(self.open_ppdb_page)
anticheat_text = self.getAntiCheatText(anticheat_status) anticheat_text = self.getAntiCheatText(anticheat_status)
if anticheat_text: if anticheat_text:
@@ -426,14 +426,8 @@ class GameCard(QFrame):
return "broken" return "broken"
return "" return ""
def open_portproton_forum_topic(self): def open_ppdb_page(self):
result = self.portproton_api.get_forum_topic_slug(self.name) self.portproton_api.open_ppdb_page(self.name, self.exec_line)
base_url = "https://linux-gaming.ru/"
if result.startswith("search?q="):
url = QUrl(f"{base_url}{result}")
else:
url = QUrl(f"{base_url}t/{result}")
QDesktopServices.openUrl(url)
def open_protondb_report(self): def open_protondb_report(self):
url = QUrl(f"https://www.protondb.com/app/{self.appid}") url = QUrl(f"https://www.protondb.com/app/{self.appid}")

View File

@@ -1099,7 +1099,8 @@ class InputManager(QObject):
return return
# Handle common UI elements like QMessageBox, QMenu, etc. # Handle common UI elements like QMessageBox, QMenu, etc.
if self._handle_common_ui_elements(button_code): # Only handle press events (value != 0), ignore release events
if value != 0 and self._handle_common_ui_elements(button_code):
return return
# Handle other QDialogs # Handle other QDialogs
@@ -1550,7 +1551,8 @@ class InputManager(QObject):
def handle_button_slot(self, button_code: int, value: int) -> None: def handle_button_slot(self, button_code: int, value: int) -> None:
# Handle common UI elements like QMessageBox, QMenu, etc. FIRST # Handle common UI elements like QMessageBox, QMenu, etc. FIRST
# This ensures that any active dialogs are handled before main window logic # This ensures that any active dialogs are handled before main window logic
if self._handle_common_ui_elements(button_code): # Only handle press events (value=1), ignore release events (value=0)
if value == 1 and self._handle_common_ui_elements(button_code):
return return
active_window = QApplication.activeWindow() active_window = QApplication.activeWindow()
@@ -1591,16 +1593,10 @@ class InputManager(QObject):
current_tab_index = self._parent.stackedWidget.currentIndex() current_tab_index = self._parent.stackedWidget.currentIndex()
if button_code in BUTTONS['confirm'] and isinstance(focused, QLineEdit): if button_code in BUTTONS['confirm'] and isinstance(focused, QLineEdit):
search_edit = None keyboard = getattr(self._parent, 'keyboard', None)
if current_tab_index == 0: if keyboard:
search_edit = getattr(self._parent, 'searchEdit', None) keyboard.show_for_widget(focused)
elif current_tab_index == 1: return
search_edit = getattr(self._parent, 'autoInstallSearchLineEdit', None)
if focused == search_edit:
keyboard = getattr(self._parent, 'keyboard', None)
if keyboard:
keyboard.show_for_widget(focused)
return
# Handle Y button to focus search # Handle Y button to focus search
if button_code in BUTTONS['prev_dir']: # Y button if button_code in BUTTONS['prev_dir']: # Y button

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n" "Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-01-12 20:59+0500\n" "POT-Creation-Date: 2026-01-18 11:43+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"

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -33,11 +33,21 @@ LOCALE_MAP = {
'el': 'greek', 'el': 'greek',
} }
translate = gettext.translation( # Try system locale directory first, fallback to local for development
domain="messages", _system_localedir = Path("/usr/share/locale")
localedir = Path(__file__).parent / "locales", _local_localedir = Path(__file__).parent / "locales"
fallback=True,
) try:
translate = gettext.translation(
domain="portprotonqt",
localedir=_system_localedir,
)
except FileNotFoundError:
translate = gettext.translation(
domain="portprotonqt",
localedir=_local_localedir,
fallback=True,
)
_ = translate.gettext _ = translate.gettext
def get_system_locale(): def get_system_locale():

View File

@@ -804,16 +804,6 @@ class MainWindow(QMainWindow):
self.refreshButton.setText(_("Refresh Grid")) self.refreshButton.setText(_("Refresh Grid"))
self.update_status_message.emit(_("Game library refreshed"), 3000) self.update_status_message.emit(_("Game library refreshed"), 3000)
def open_portproton_forum_topic(self, topic_name: str):
"""Open the PortProton forum topic or search page for this game."""
result = self.portproton_api.get_forum_topic_slug(topic_name)
base_url = "https://linux-gaming.ru/"
if result.startswith("search?q="):
url = QUrl(f"{base_url}{result}")
else:
url = QUrl(f"{base_url}t/{result}")
QDesktopServices.openUrl(url)
def loadGames(self): def loadGames(self):
display_filter = read_display_filter() display_filter = read_display_filter()
favorites = read_favorites() favorites = read_favorites()
@@ -971,8 +961,8 @@ class MainWindow(QMainWindow):
info.get('description', ''), info.get('description', ''),
info.get('cover', ''), info.get('cover', ''),
appid, appid,
f"steam://rungameid/{appid}",
info.get('controller_support', ''), info.get('controller_support', ''),
f"steam://rungameid/{appid}",
last_launch, last_launch,
format_playtime(playtime_seconds), format_playtime(playtime_seconds),
info.get('protondb_tier', ''), info.get('protondb_tier', ''),
@@ -1121,8 +1111,8 @@ class MainWindow(QMainWindow):
final_desc, final_desc,
final_cover, final_cover,
steam_info.get("appid", ""), steam_info.get("appid", ""),
exec_line,
steam_info.get("controller_support", ""), steam_info.get("controller_support", ""),
exec_line,
get_last_launch(exe_name) if exe_name else _("Never"), get_last_launch(exe_name) if exe_name else _("Never"),
formatted_playtime, formatted_playtime,
steam_info.get("protondb_tier", ""), steam_info.get("protondb_tier", ""),
@@ -1440,8 +1430,8 @@ class MainWindow(QMainWindow):
final_desc, final_desc,
final_cover, final_cover,
steam_info.get("appid", ""), steam_info.get("appid", ""),
exec_line,
steam_info.get("controller_support", ""), steam_info.get("controller_support", ""),
exec_line,
last_launch, last_launch,
formatted_playtime, formatted_playtime,
steam_info.get("protondb_tier", ""), steam_info.get("protondb_tier", ""),
@@ -2433,6 +2423,9 @@ class MainWindow(QMainWindow):
else: else:
self.input_manager.gamepad_type = GamepadType.UNKNOWN self.input_manager.gamepad_type = GamepadType.UNKNOWN
self.updateControlHints() self.updateControlHints()
# Update virtual keyboard icons
if hasattr(self, 'keyboard'):
self.keyboard.update_keyboard()
for card in self.game_library_manager.game_card_cache.values(): for card in self.game_library_manager.game_card_cache.values():
card.update_badge_visibility(filter_key) card.update_badge_visibility(filter_key)

54
portprotonqt/meson.build Normal file
View File

@@ -0,0 +1,54 @@
# Install Python source files
install_data(
'__init__.py',
'animations.py',
'app.py',
'cli.py',
'config_utils.py',
'context_menu_manager.py',
'custom_widgets.py',
'detail_pages.py',
'dialogs.py',
'downloader.py',
'egs_api.py',
'game_card.py',
'game_library_manager.py',
'get_wine_module.py',
'howlongtobeat_api.py',
'image_utils.py',
'input_manager.py',
'keyboard_layouts.py',
'localization.py',
'logger.py',
'main_window.py',
'portproton_api.py',
'preloader.py',
'search_utils.py',
'settings_manager.py',
'steam_api.py',
'system_overlay.py',
'theme_manager.py',
'theme_security.py',
'time_utils.py',
'tray_manager.py',
'version_utils.py',
'virtual_keyboard.py',
install_dir: pkgdatadir,
)
# Install themes
install_subdir(
'themes',
install_dir: pkgdatadir,
exclude_directories: ['__pycache__'],
exclude_files: ['*.pyc'],
)
# Install locales - only .mo files (compiled translations)
# exclude_files doesn't work recursively, so we install each language manually
foreach lang : ['de', 'es', 'pt', 'ru']
install_data(
'locales' / lang / 'LC_MESSAGES' / 'portprotonqt.mo',
install_dir: get_option('localedir') / lang / 'LC_MESSAGES',
)
endforeach

View File

@@ -1,5 +1,4 @@
import os import os
import tarfile
import orjson import orjson
import requests import requests
import urllib.parse import urllib.parse
@@ -8,13 +7,13 @@ import glob
import re import re
import hashlib import hashlib
from collections.abc import Callable from collections.abc import Callable
from PySide6.QtCore import QThread, Signal from PySide6.QtCore import QThread, Signal, QUrl
from PySide6.QtGui import QDesktopServices
from portprotonqt.downloader import Downloader from portprotonqt.downloader import Downloader
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.config_utils import get_portproton_location from portprotonqt.config_utils import get_portproton_location
logger = get_logger(__name__) logger = get_logger(__name__)
CACHE_DURATION = 30 * 24 * 60 * 60 # 30 days in seconds
AUTOINSTALL_CACHE_DURATION = 3600 # 1 hour for autoinstall cache AUTOINSTALL_CACHE_DURATION = 3600 # 1 hour for autoinstall cache
def normalize_name(s): def normalize_name(s):
@@ -42,6 +41,41 @@ def normalize_name(s):
filtered_words = [word for word in words if word not in keywords_to_remove] filtered_words = [word for word in words if word not in keywords_to_remove]
return " ".join(filtered_words) return " ".join(filtered_words)
def extract_exe_name(exec_line: str) -> str:
"""Extract executable name from exec_line.
Handles various exec_line formats:
- Full command: 'env VAR=val /path/to/script /path/to/game.exe' -> 'game.exe'
- Autoinstall: 'autoinstall:script_name' -> ''
- Simple path: '/path/to/game.exe' -> 'game.exe'
Returns:
Executable name with .exe extension, or empty string if not found
"""
import shlex
if not exec_line:
return ""
# Handle autoinstall scripts - they don't have a direct exe
if exec_line.startswith("autoinstall:"):
return ""
try:
parts = shlex.split(exec_line)
# Search for the last part ending with .exe
# In PortProton format, the game exe is always the last argument
for part in reversed(parts):
if part.lower().endswith(".exe"):
game_exe = os.path.expanduser(part)
return os.path.basename(game_exe)
return ""
except (ValueError, IndexError):
return ""
def get_cache_dir(): def get_cache_dir():
"""Return the cache directory path, creating it if necessary.""" """Return the cache directory path, creating it if necessary."""
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"))
@@ -53,7 +87,6 @@ class PortProtonAPI:
"""API to fetch game assets (cover, metadata) and forum topics from the PortProtonQt repository.""" """API to fetch game assets (cover, metadata) and forum topics from the PortProtonQt repository."""
def __init__(self, downloader: Downloader | None = None): def __init__(self, downloader: Downloader | None = None):
self.base_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/custom_data" self.base_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/portprotonqt/custom_data"
self.topics_url = "https://git.linux-gaming.ru/Boria138/PortProtonQt/raw/branch/main/data/linux_gaming_topics.tar.xz"
self.downloader = downloader or Downloader(max_workers=4) self.downloader = downloader or Downloader(max_workers=4)
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")
@@ -61,8 +94,7 @@ class PortProtonAPI:
self.portproton_location = get_portproton_location() self.portproton_location = get_portproton_location()
self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) self.repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data") self.builtin_custom_folder = os.path.join(self.repo_root, "custom_data")
self._topics_data = None self._autoinstall_cache = None # In-memory cache
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)
@@ -503,65 +535,64 @@ class PortProtonAPI:
logger.info("Started background load of autoinstall games") logger.info("Started background load of autoinstall games")
return worker return worker
def _load_topics_data(self): def get_ppdb_url(self, game_name: str, exe_name: str) -> str:
"""Load and cache linux_gaming_topics_min.json from the archive.""" """Get the PPDB URL for a given game.
if self._topics_data is not None:
return self._topics_data
cache_dir = get_cache_dir() Makes an API call to ppdb.linux-gaming.ru to look up the game by exe name.
cache_tar = os.path.join(cache_dir, "linux_gaming_topics.tar.xz") If the returned name matches the game name, returns the direct URL.
cache_json = os.path.join(cache_dir, "linux_gaming_topics_min.json") Otherwise returns a search URL to avoid false positives (e.g., launcher.exe matches many games).
if os.path.exists(cache_json) and (time.time() - os.path.getmtime(cache_json) < CACHE_DURATION): Args:
logger.info("Using cached topics JSON: %s", cache_json) game_name: Display name of the game
try: exe_name: Executable name (with or without .exe extension)
with open(cache_json, "rb") as f:
self._topics_data = orjson.loads(f.read())
logger.debug("Loaded %d topics from cache", len(self._topics_data))
return self._topics_data
except Exception as e:
logger.error("Error reading cached topics JSON: %s", e)
self._topics_data = []
def process_tar(result: str | None): Returns:
if not result or not os.path.exists(result): Full URL to the PPDB page or search page
logger.error("Failed to download topics archive") """
self._topics_data = [] base_url = "https://ppdb.linux-gaming.ru"
return
try:
with tarfile.open(result, mode="r:xz") as tar:
member = next((m for m in tar.getmembers() if m.name == "linux_gaming_topics_min.json"), None)
if member is None:
raise RuntimeError("linux_gaming_topics_min.json not found in archive")
fobj = tar.extractfile(member)
if fobj is None:
raise RuntimeError("Failed to extract linux_gaming_topics_min.json from archive")
raw = fobj.read()
fobj.close()
self._topics_data = orjson.loads(raw)
with open(cache_json, "wb") as f:
f.write(orjson.dumps(self._topics_data))
if os.path.exists(cache_tar):
os.remove(cache_tar)
logger.info("Archive %s deleted after extraction", cache_tar)
logger.info("Loaded %d topics from archive", len(self._topics_data))
except Exception as e:
logger.error("Error processing topics archive: %s", e)
self._topics_data = []
self.downloader.download_async(self.topics_url, cache_tar, timeout=5, callback=process_tar) # Ensure exe_name has .exe extension
# Wait for async download to complete if called synchronously if not exe_name.lower().endswith(".exe"):
while self._topics_data is None: exe_name = f"{exe_name}.exe"
time.sleep(0.1)
return self._topics_data
def get_forum_topic_slug(self, game_name: str) -> str: api_url = f"{base_url}/api/lookup/exe/{urllib.parse.quote(exe_name)}"
"""Get the forum topic slug or search URL for a given game name."""
topics = self._load_topics_data() try:
normalized_name = normalize_name(game_name) response = requests.get(api_url, timeout=5)
for topic in topics: if response.status_code == 200:
if topic["normalized_title"] == normalized_name: data = response.json()
return topic["slug"] api_name = data.get("name", "")
logger.debug("No forum topic found for game: %s, redirecting to search", game_name) api_url_result = data.get("url", "")
encoded_name = urllib.parse.quote(f"#ppdb {game_name}")
return f"search?q={encoded_name}" # Compare normalized names to avoid false positives
if api_name and api_url_result:
normalized_game = normalize_name(game_name)
normalized_api = normalize_name(api_name)
if normalized_game == normalized_api:
logger.debug("PPDB exact match for %s: %s", game_name, api_url_result)
return api_url_result
logger.debug(
"PPDB name mismatch for %s (exe: %s): API returned '%s', redirecting to search",
game_name, exe_name, api_name
)
except requests.RequestException as e:
logger.debug("PPDB API request failed for %s: %s", exe_name, e)
except (ValueError, KeyError) as e:
logger.debug("PPDB API response parsing failed for %s: %s", exe_name, e)
# Fallback to search URL
encoded_name = urllib.parse.quote(game_name)
return f"{base_url}/browse?search={encoded_name}"
def open_ppdb_page(self, game_name: str, exec_line: str) -> None:
"""Open the PPDB page for a game in the default browser.
Args:
game_name: Display name of the game
exec_line: Exec line from which to extract the exe name
"""
exe_name = extract_exe_name(exec_line)
url = self.get_ppdb_url(game_name, exe_name)
QDesktopServices.openUrl(QUrl(url))