25 Commits

Author SHA1 Message Date
de229401a7 chore(build): added deb package
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-12 00:22:11 +05:00
6f82068864 chore: bump to 0.1.9
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-08 11:47:25 +05:00
d4672ecb0e chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-08 11:47:19 +05:00
Renovate Bot
087ac8eda2 chore(deps): update https://gitea.com/actions/setup-node digest to 395ad32 2025-12-07 10:48:27 +00:00
Renovate Bot
0a9acaf5da chore(deps): update https://gitea.com/actions/checkout digest to 8e8c483 2025-12-07 10:48:16 +00:00
d0fad6a3c9 fix: added correct parent to GameCard
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-07 15:46:27 +05:00
468887110c fix(qt): prevent RuntimeError from accessing deleted Qt C++ objects
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-07 12:45:37 +05:00
32e4950a00 Revert "chore: bump ver to 0.1.9"
This reverts commit 29d25cec01.
2025-12-06 14:26:04 +05:00
b16074fa5c fix: Add protection against accessing deleted Qt objects in async callbacks
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-06 14:22:41 +05:00
1bd7c23419 fix(settings): Remove surrounding quotes from the value if present
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-04 11:53:54 +05:00
f4275dd465 fix(get_portproton_start_command): Check if flatpak command exists before trying to run it
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 18:44:47 +05:00
c8b91c4687 fix(settings): update keyboard navigation
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 18:40:27 +05:00
4aaeb2e809 fix: dont start game by Enter
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 18:23:49 +05:00
b6ea9350fa fix: fix gamecard refrefresh regression after 0889aa8
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-02 17:52:19 +05:00
29d25cec01 chore: bump ver to 0.1.9
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-01 20:29:45 +05:00
a634de5462 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-01 20:27:12 +05:00
1ba1781994 feat(settings): added preloader because flatpak is too slow
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-01 17:06:20 +05:00
0aae292f61 fix(settings): fix work on Flatpak
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-01 16:59:43 +05:00
3ef433af0c fix: Only handle menu button if our main window is currently active
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-01 12:08:55 +05:00
Gitea Actions
9fe33e02d8 chore: update steam apps list 2025-12-01T00:01:44Z 2025-12-01 00:01:44 +00:00
2ac91a759d chore(localization): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-30 13:20:33 +05:00
2c82bff204 fix(main_window): remove redundant loading status and improve loading flow
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-30 13:14:38 +05:00
0889aa883e fix: refresh button refresh custom data too now
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-30 12:59:32 +05:00
7780dcfc4d chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-29 23:12:31 +05:00
9ef39ae2b6 fix: save cover images from URL to custom_data folder
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-11-29 23:08:54 +05:00
56 changed files with 13390 additions and 1179 deletions

View File

@@ -1,4 +1,4 @@
name: Nightly Build - AppImage, Arch, Fedora name: Nightly Build - AppImage, Debian, Arch, Fedora
on: on:
workflow_dispatch: workflow_dispatch:
@@ -8,11 +8,37 @@ 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Install required dependencies - name: Install required dependencies
run: | run: |
@@ -73,7 +99,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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Copy fedora.spec - name: Copy fedora.spec
run: | run: |
@@ -134,7 +160,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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 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

View File

@@ -1,4 +1,4 @@
name: Build AppImage, Arch and Fedora Packages name: Build AppImage, Debian, Arch and Fedora Packages
on: on:
workflow_dispatch: workflow_dispatch:
@@ -8,12 +8,38 @@ on:
env: env:
# Common version, will be used for tagging the release # Common version, will be used for tagging the release
VERSION: 0.1.8 VERSION: 0.1.9
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
@@ -149,7 +175,7 @@ jobs:
release: release:
name: Create and Publish Release name: Create and Publish Release
needs: [build-appimage, build-arch, build-fedora] needs: [build-debian, 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

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6

View File

@@ -1,10 +1,11 @@
name: Build Check - AppImage, Arch, Fedora name: Build Check - AppImage, Debian, 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"
@@ -15,10 +16,11 @@ 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -43,6 +45,13 @@ 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
@@ -57,13 +66,38 @@ 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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 - uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Install required dependencies - name: Install required dependencies
run: | run: |
@@ -115,7 +149,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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 uses: https://gitea.com/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Copy fedora-git.spec - name: Copy fedora-git.spec
run: | run: |
@@ -178,7 +212,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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 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

View File

@@ -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@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 uses: https://gitea.com/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with: with:
node-version: 20 node-version: 20

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6

View File

@@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6 container: ghcr.io/renovatebot/renovate:latest@sha256:17c8966ef38fc361e108a550ffe2dcedf73e846f9975a974aea3d48c66b107a6
steps: steps:
- uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 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@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6 uses: https://gitea.com/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with: with:
node-version: 20 node-version: 20

View File

@@ -3,7 +3,7 @@
Все заметные изменения в этом проекте фиксируются в этом файле. Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/). Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased] ## [0.1.9] - 2025-12-08
### Added ### Added
- Добавлены основные и расширенные настройки для `.exe`-файлов - Добавлены основные и расширенные настройки для `.exe`-файлов
@@ -18,11 +18,16 @@
- Ускорено чтение конфигов за счёт уменьшения количества обращений к файловой системе. - Ускорено чтение конфигов за счёт уменьшения количества обращений к файловой системе.
- Из стандартной темы удалены неиспользуемые шрифты - Из стандартной темы удалены неиспользуемые шрифты
- Улучшена совместимость с Qt 6.10 - Улучшена совместимость с Qt 6.10
- Ускорен запуск программы
- В диалог редактирования ярылыка добавлен placeholder с уточнением того что в качевстве обложки можно использовать и ссылку, а не только файл
- Ссылку на обложку в диалоге редактирования ярлыка теперь можно указывать без протокола вроде http или https
### Fixed ### Fixed
- Добавлено больше проверок на None для избежания вылетов - Добавлено больше проверок на None для избежания вылетов
- Улучшена работа с потоками для избежания вылетов - Улучшена работа с потоками для избежания вылетов
- Исправлен запуск PortProton из Flatpak: теперь используется `flatpak run`, а не `start.sh` - Исправлен запуск PortProton из Flatpak: теперь используется `flatpak run`, а не `start.sh`
- Исправлено применение обложки по ссылке например со steamgriddb.com/
- Исправлено множественное открытие окон в X11
### Contributors ### Contributors
- @Vector_null - @Vector_null

2
MANIFEST.in Normal file
View File

@@ -0,0 +1,2 @@
recursive-include portprotonqt/themes *
recursive-include portprotonqt/locales *

View File

@@ -5,8 +5,7 @@ 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 share AppDir/usr - cp -r usr AppDir/
- cp -r lib AppDir/usr
- rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/ - rm -rf AppDir/usr/local/lib/python3.10/dist-packages/PySide6/Qt/qml/
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate} - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{assistant,designer,linguist,lrelease,lupdate}
- rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*} - rm -f AppDir/usr/local/lib/python3.10/dist-packages/PySide6/{Qt3DAnimation*,Qt3DCore*,Qt3DExtras*,Qt3DInput*,Qt3DLogic*,Qt3DRender*,QtBluetooth*,QtCharts*,QtConcurrent*,QtDataVisualization*,QtDesigner*,QtExampleIcons*,QtGraphs*,QtGraphsWidgets*,QtHelp*,QtHttpServer*,QtLocation*,QtMultimedia*,QtMultimediaWidgets*,QtNetworkAuth*,QtNfc*,QtOpenGL*,QtOpenGLWidgets*,QtPdf*,QtPdfWidgets*,QtPositioning*,QtPrintSupport*,QtQml*,QtQuick*,QtQuick3D*,QtQuickControls2*,QtQuickTest*,QtQuickWidgets*,QtRemoteObjects*,QtScxml*,QtSensors*,QtSerialBus*,QtSerialPort*,QtSpatialAudio*,QtSql*,QtStateMachine*,QtSvgWidgets*,QtTest*,QtTextToSpeech*,QtUiTools*,QtWebChannel*,QtWebEngineCore*,QtWebEngineQuick*,QtWebEngineWidgets*,QtWebSockets*,QtWebView*,QtXml*}
@@ -37,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.8 version: 0.1.9
exec: usr/bin/python3 exec: usr/bin/python3
exec_args: "-m portprotonqt.app $@" exec_args: "-m portprotonqt.app $@"
apt: apt:

View File

@@ -1,5 +1,5 @@
pkgname=portprotonqt pkgname=portprotonqt
pkgver=0.1.8 pkgver=0.1.9
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')
@@ -19,6 +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/share "$pkgdir/usr/" cp -r build-aux/usr "$pkgdir/"
cp -r build-aux/lib "$pkgdir/usr/"
} }

View File

@@ -24,6 +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/share "$pkgdir/usr/" cp -r build-aux/usr "$pkgdir/"
cp -r build-aux/lib "$pkgdir/usr/"
} }

View File

@@ -70,8 +70,7 @@ cd %{oname}
cd %{oname} cd %{oname}
%pyproject_install %pyproject_install
%pyproject_save_files %{pypi_name} %pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/ cp -r build-aux/usr %{buildroot}/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name}-git -f %{pyproject_files} %files -n python3-%{pypi_name}-git -f %{pyproject_files}
%{_bindir}/%{pypi_name} %{_bindir}/%{pypi_name}

View File

@@ -1,5 +1,5 @@
%global pypi_name portprotonqt %global pypi_name portprotonqt
%global pypi_version 0.1.8 %global pypi_version 0.1.9
%global oname PortProtonQt %global oname PortProtonQt
%global _python_no_extras_requires 1 %global _python_no_extras_requires 1
@@ -69,8 +69,7 @@ cd %{oname}
cd %{oname} cd %{oname}
%pyproject_install %pyproject_install
%pyproject_save_files %{pypi_name} %pyproject_save_files %{pypi_name}
cp -r build-aux/share %{buildroot}/usr/ cp -r build-aux/usr %{buildroot}/
cp -r build-aux/lib %{buildroot}/usr/
%files -n python3-%{pypi_name} -f %{pyproject_files} %files -n python3-%{pypi_name} -f %{pyproject_files}
%{_bindir}/%{pypi_name} %{_bindir}/%{pypi_name}

View File

@@ -1373,7 +1373,7 @@
}, },
{ {
"normalized_name": "arena breakout infinite", "normalized_name": "arena breakout infinite",
"status": "Broken" "status": "Denied"
}, },
{ {
"normalized_name": "pixel gun 3d pc", "normalized_name": "pixel gun 3d pc",
@@ -4316,7 +4316,7 @@
"status": "Broken" "status": "Broken"
}, },
{ {
"normalized_name": "solo leveling arise", "normalized_name": "solo leveling arise overdrive",
"status": "Running" "status": "Running"
}, },
{ {
@@ -4527,10 +4527,6 @@
"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"
@@ -4542,5 +4538,9 @@
{ {
"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"
} }
] ]

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,128 @@
[ [
{
"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", "normalized_title": "split/second",
"slug": "split-second" "slug": "split-second"
@@ -11,10 +135,6 @@
"normalized_title": "foundation", "normalized_title": "foundation",
"slug": "foundation" "slug": "foundation"
}, },
{
"normalized_title": "земский собор [демо]",
"slug": "zemskij-sobor-demo"
},
{ {
"normalized_title": "crusader kings 3", "normalized_title": "crusader kings 3",
"slug": "crusader-kings-3" "slug": "crusader-kings-3"
@@ -1411,10 +1531,6 @@
"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"
@@ -1539,10 +1655,6 @@
"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"
@@ -1735,10 +1847,6 @@
"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"

Binary file not shown.

8
debian/README.Debian vendored Normal file
View File

@@ -0,0 +1,8 @@
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 Normal file
View File

@@ -0,0 +1,22 @@
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 Normal file
View File

@@ -0,0 +1,5 @@
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 Normal file
View File

@@ -0,0 +1 @@
13

47
debian/control vendored Normal file
View File

@@ -0,0 +1,47 @@
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 Normal file
View File

@@ -0,0 +1,7 @@
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 Executable file
View File

@@ -0,0 +1,33 @@
#!/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 Normal file
View File

@@ -0,0 +1 @@
3.0 (native)

7
debian/source/options vendored Normal file
View File

@@ -0,0 +1,7 @@
# Configuration for Debian source package
compression = "gzip"
# Files and directories to exclude from source package
tar-ignore = "dev-scripts"
tar-ignore = ".*"
tar-ignore = "__pycache__"

View File

@@ -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 from datetime import date, datetime
# Base directory of the project # Base directory of the project
BASE_DIR = Path(__file__).parent.parent BASE_DIR = Path(__file__).parent.parent
@@ -16,6 +16,7 @@ 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:
""" """
@@ -109,6 +110,138 @@ 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')
@@ -123,7 +256,8 @@ 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 = []

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ 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.8" __app_version__ = "0.1.9"
def get_version(): def get_version():
try: try:

View File

@@ -183,23 +183,39 @@ def get_portproton_start_command():
if not portproton_path: if not portproton_path:
return None return None
# Check if flatpak command exists before trying to run it
try: try:
result = subprocess.run( subprocess.run(
["flatpak", "list"], ["flatpak", "--version"],
capture_output=True, capture_output=True,
text=True, text=True,
check=False, check=False,
timeout=10 timeout=5
) )
if "ru.linux_gaming.PortProton" in result.stdout: flatpak_available = True
logger.info("Detected Flatpak installation") except FileNotFoundError:
return ["flatpak", "run", "ru.linux_gaming.PortProton"] flatpak_available = False
except subprocess.TimeoutExpired: except Exception:
logger.warning("Flatpak list command timed out") flatpak_available = False
return None
except Exception as e: if flatpak_available:
logger.warning(f"Error checking flatpak list: {e}") try:
pass 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") start_sh_path = os.path.join(portproton_path, "data", "scripts", "start.sh")
if os.path.exists(start_sh_path): if os.path.exists(start_sh_path):

View File

@@ -1035,7 +1035,15 @@ Icon={icon_path}
) )
return return
if os.path.isfile(new_cover_path): # Check if new_cover_path is a URL by checking for common image extensions
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",
@@ -1043,16 +1051,25 @@ 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(new_cover_path, os.path.join(custom_folder, f"cover{ext}")) shutil.copyfile(cover_to_copy, 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):
""" """

View File

@@ -10,7 +10,7 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment from PySide6.QtCore import Qt, QObject, Signal, QMimeDatabase, QTimer, QThreadPool, QRunnable, Slot, QProcess, QProcessEnvironment
from icoextract import IconExtractor, IconExtractorError from icoextract import IconExtractor, IconExtractorError
from PIL import Image from PIL import Image
from portprotonqt.config_utils import get_portproton_location, read_favorite_folders, read_theme_from_config from portprotonqt.config_utils import get_portproton_location, get_portproton_start_command, read_favorite_folders, read_theme_from_config
from portprotonqt.localization import _ from portprotonqt.localization import _
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.theme_manager import ThemeManager from portprotonqt.theme_manager import ThemeManager
@@ -906,6 +906,7 @@ class AddGameDialog(QDialog):
self.coverEdit = CustomLineEdit(self, theme=self.theme) self.coverEdit = CustomLineEdit(self, theme=self.theme)
self.coverEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE) self.coverEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
self.coverEdit.setPlaceholderText(_("Enter local path or URL for cover image"))
if cover_path: if cover_path:
self.coverEdit.setText(cover_path) self.coverEdit.setText(cover_path)
@@ -949,7 +950,12 @@ class AddGameDialog(QDialog):
# Подключение сигналов # Подключение сигналов
self.select_button.clicked.connect(self.accept) self.select_button.clicked.connect(self.accept)
self.cancel_button.clicked.connect(self.reject) self.cancel_button.clicked.connect(self.reject)
self.coverEdit.textChanged.connect(self.updatePreview) # Set up a timer for debounced cover preview updates
self.cover_preview_timer = QTimer(self)
self.cover_preview_timer.setSingleShot(True)
self.cover_preview_timer.timeout.connect(self.updatePreview)
self.coverEdit.textChanged.connect(self.onCoverTextChanged)
self.exeEdit.textChanged.connect(self.updatePreview) self.exeEdit.textChanged.connect(self.updatePreview)
# Установка одинаковой ширины для кнопок и полей ввода # Установка одинаковой ширины для кнопок и полей ввода
@@ -1083,33 +1089,51 @@ class AddGameDialog(QDialog):
def handleDownloadedCover(self, file_path): def handleDownloadedCover(self, file_path):
"""Handle the downloaded cover image and update the preview.""" """Handle the downloaded cover image and update the preview."""
if file_path and os.path.isfile(file_path): # Check if the dialog or widget has been destroyed before updating
self.last_cover_path = file_path if not hasattr(self, 'coverPreview') or self.coverPreview is None:
pixmap = QPixmap(file_path) return
if not pixmap.isNull():
self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio)) try:
if file_path and os.path.isfile(file_path):
self.last_cover_path = file_path
pixmap = QPixmap(file_path)
if not pixmap.isNull():
self.coverPreview.setPixmap(pixmap.scaled(250, 250, Qt.AspectRatioMode.KeepAspectRatio))
else:
self.coverPreview.setText(_("Invalid image"))
else: else:
self.coverPreview.setText(_("Invalid image")) self.coverPreview.setText(_("Failed to download cover"))
else: logger.warning(f"Failed to download cover to {file_path}")
self.coverPreview.setText(_("Failed to download cover")) except RuntimeError:
logger.warning(f"Failed to download cover to {file_path}") # Handle the case where the Qt object was deleted
pass
def onCoverTextChanged(self):
"""Handle cover text changes with debounce."""
# Restart the timer to delay the preview update
self.cover_preview_timer.start(500) # 500ms delay
def updatePreview(self): def updatePreview(self):
"""Update the cover preview image.""" """Update the cover preview image."""
cover_path = self.coverEdit.text().strip() cover_path = self.coverEdit.text().strip()
exe_path = self.exeEdit.text().strip() exe_path = self.exeEdit.text().strip()
# Check if cover_path is a URL # Check if cover_path is a URL by checking for common image extensions
url_pattern = r'^https?://[^\s/$.?#].[^\s]*$' image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp')
if re.match(url_pattern, cover_path): has_image_extension = any(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
if has_image_extension and not os.path.isfile(cover_path):
# Create a temporary file for the downloaded image # Create a temporary file for the downloaded image
fd, local_path = tempfile.mkstemp(suffix=".png") fd, local_path = tempfile.mkstemp(suffix=".png")
os.close(fd) os.close(fd)
os.unlink(local_path) os.unlink(local_path)
# Start asynchronous download # Start asynchronous download
# Add protocol if not present
download_url = cover_path if cover_path.startswith(('http://', 'https://')) else f'https://{cover_path}'
self.downloader.download_async( self.downloader.download_async(
url=cover_path, url=download_url,
local_path=local_path, local_path=local_path,
timeout=10, timeout=10,
callback=self.handleDownloadedCover callback=self.handleDownloadedCover
@@ -1701,8 +1725,10 @@ class ExeSettingsDialog(QDialog):
if self.portproton_path is None: if self.portproton_path is None:
logger.error("PortProton location not found") logger.error("PortProton location not found")
return return
base_path = os.path.join(self.portproton_path, "data") self.start_sh = get_portproton_start_command()
self.start_sh = [os.path.join(base_path, "scripts", "start.sh")] if self.start_sh is None:
logger.error("PortProton start command not found")
return
self.dist_options = [] self.dist_options = []
self.prefix_options = [] self.prefix_options = []
@@ -1776,9 +1802,9 @@ class ExeSettingsDialog(QDialog):
self.load_current_settings() self.load_current_settings()
def _get_process_args(self, subcommand_args): def _get_process_args(self, subcommand_args):
"""Get the full arguments for QProcess.start, handling flatpak separator.""" """Get the full arguments for QProcess.start, handling flatpak format."""
if self.start_sh[0] == "flatpak": if self.start_sh and self.start_sh[0] == "flatpak":
return self.start_sh[1:] + ["--"] + subcommand_args return self.start_sh + subcommand_args
else: else:
return self.start_sh + subcommand_args return self.start_sh + subcommand_args
@@ -1814,7 +1840,7 @@ class ExeSettingsDialog(QDialog):
# Connect tab change to update description hint # Connect tab change to update description hint
self.tab_widget.currentChanged.connect(self.on_table_selection_changed) self.tab_widget.currentChanged.connect(self.on_table_selection_changed)
# Main settings table # Main settings table with preloader
self.settings_table = QTableWidget() self.settings_table = QTableWidget()
self.settings_table.setAlternatingRowColors(True) self.settings_table.setAlternatingRowColors(True)
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
@@ -1829,11 +1855,30 @@ class ExeSettingsDialog(QDialog):
self.settings_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.settings_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.settings_table.setTextElideMode(Qt.TextElideMode.ElideNone) self.settings_table.setTextElideMode(Qt.TextElideMode.ElideNone)
self.settings_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE) self.settings_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
self.main_tab_layout.addWidget(self.settings_table)
# Create preloader for main settings table
self.settings_preloader = Preloader()
settings_preloader_container = QWidget()
settings_preloader_layout = QVBoxLayout(settings_preloader_container)
settings_preloader_layout.addStretch()
settings_preloader_hlayout = QHBoxLayout()
settings_preloader_hlayout.addStretch()
settings_preloader_hlayout.addWidget(self.settings_preloader)
settings_preloader_hlayout.addStretch()
settings_preloader_layout.addLayout(settings_preloader_hlayout)
settings_preloader_layout.addStretch()
settings_preloader_layout.setContentsMargins(0, 0, 0, 0)
settings_preloader_layout.setSpacing(0)
# Create stacked widget for main settings
self.settings_container = QStackedWidget()
self.settings_container.addWidget(settings_preloader_container) # Index 0: preloader
self.settings_container.addWidget(self.settings_table) # Index 1: table
self.main_tab_layout.addWidget(self.settings_container)
# Connect selection changed signal for the main table # Connect selection changed signal for the main table
self.settings_table.currentCellChanged.connect(self.on_table_selection_changed) self.settings_table.currentCellChanged.connect(self.on_table_selection_changed)
# Advanced settings table # Advanced settings table with preloader
self.advanced_table = QTableWidget() self.advanced_table = QTableWidget()
self.advanced_table.setAlternatingRowColors(True) self.advanced_table.setAlternatingRowColors(True)
self.advanced_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.advanced_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
@@ -1850,7 +1895,26 @@ class ExeSettingsDialog(QDialog):
self.advanced_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) self.advanced_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.advanced_table.setTextElideMode(Qt.TextElideMode.ElideNone) self.advanced_table.setTextElideMode(Qt.TextElideMode.ElideNone)
self.advanced_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE) self.advanced_table.setStyleSheet(self.theme.WINETRICKS_TABBLE_STYLE)
self.advanced_tab_layout.addWidget(self.advanced_table)
# Create preloader for advanced settings table
self.advanced_preloader = Preloader()
advanced_preloader_container = QWidget()
advanced_preloader_layout = QVBoxLayout(advanced_preloader_container)
advanced_preloader_layout.addStretch()
advanced_preloader_hlayout = QHBoxLayout()
advanced_preloader_hlayout.addStretch()
advanced_preloader_hlayout.addWidget(self.advanced_preloader)
advanced_preloader_hlayout.addStretch()
advanced_preloader_layout.addLayout(advanced_preloader_hlayout)
advanced_preloader_layout.addStretch()
advanced_preloader_layout.setContentsMargins(0, 0, 0, 0)
advanced_preloader_layout.setSpacing(0)
# Create stacked widget for advanced settings
self.advanced_container = QStackedWidget()
self.advanced_container.addWidget(advanced_preloader_container) # Index 0: preloader
self.advanced_container.addWidget(self.advanced_table) # Index 1: table
self.advanced_tab_layout.addWidget(self.advanced_container)
# Connect selection changed signal for the advanced table # Connect selection changed signal for the advanced table
self.advanced_table.currentCellChanged.connect(self.on_table_selection_changed) self.advanced_table.currentCellChanged.connect(self.on_table_selection_changed)
@@ -1888,9 +1952,14 @@ class ExeSettingsDialog(QDialog):
def load_current_settings(self): def load_current_settings(self):
"""Load available toggles first, then current settings.""" """Load available toggles first, then current settings."""
# Show preloaders initially
self.settings_container.setCurrentIndex(0) # Show preloader for main settings
self.advanced_container.setCurrentIndex(0) # Show preloader for advanced settings
process = QProcess(self) process = QProcess(self)
process.finished.connect(self.on_list_db_finished) process.finished.connect(self.on_list_db_finished)
process.start(self.start_sh[0], ["cli", "--list-db"]) args = self._get_process_args(["cli", "--list-db"])
process.start(args[0], args[1:])
def on_list_db_finished(self, exit_code, exit_status): def on_list_db_finished(self, exit_code, exit_status):
"""Handle --list-db output and extract available keys and system info.""" """Handle --list-db output and extract available keys and system info."""
@@ -1912,6 +1981,9 @@ class ExeSettingsDialog(QDialog):
if re.match(r'^[A-Z_0-9]+=[^=]+$', line_stripped) and not line_stripped.startswith('PW_'): if re.match(r'^[A-Z_0-9]+=[^=]+$', line_stripped) and not line_stripped.startswith('PW_'):
# System info # System info
k, v = line_stripped.split('=', 1) k, v = line_stripped.split('=', 1)
# Remove surrounding quotes from the value if present
if v.startswith('"') and v.endswith('"') and len(v) >= 2:
v = v[1:-1]
if k.startswith('NUMA_NODE_'): if k.startswith('NUMA_NODE_'):
node_id = k[10:] node_id = k[10:]
self.numa_nodes[node_id] = v self.numa_nodes[node_id] = v
@@ -1937,7 +2009,8 @@ class ExeSettingsDialog(QDialog):
# Load current settings # Load current settings
process = QProcess(self) process = QProcess(self)
process.finished.connect(self.on_show_ppdb_finished) process.finished.connect(self.on_show_ppdb_finished)
process.start(self.start_sh[0], ["cli", "--show-ppdb", f"{self.exe_path}"]) args = self._get_process_args(["cli", "--show-ppdb", f"{self.exe_path}"])
process.start(args[0], args[1:])
def on_show_ppdb_finished(self, exit_code, exit_status): def on_show_ppdb_finished(self, exit_code, exit_status):
"""Handle --show-ppdb output.""" """Handle --show-ppdb output."""
@@ -1957,6 +2030,9 @@ class ExeSettingsDialog(QDialog):
try: try:
key, val = line_stripped.split('=', 1) key, val = line_stripped.split('=', 1)
if key in self.toggle_settings or key in ADVANCED_SETTING_KEYS: if key in self.toggle_settings or key in ADVANCED_SETTING_KEYS:
# Remove surrounding quotes from the value if present
if val.startswith('"') and val.endswith('"') and len(val) >= 2:
val = val[1:-1]
self.current_settings[key] = val self.current_settings[key] = val
except ValueError: except ValueError:
continue continue
@@ -1977,6 +2053,10 @@ class ExeSettingsDialog(QDialog):
self.populate_table() self.populate_table()
self.populate_advanced() self.populate_advanced()
# Show the loaded content and hide preloaders
self.settings_container.setCurrentIndex(1) # Show main settings table
self.advanced_container.setCurrentIndex(1) # Show advanced settings table
def populate_table(self): def populate_table(self):
"""Populate the table with settings that are available in both lists.""" """Populate the table with settings that are available in both lists."""
self.settings_table.setRowCount(0) self.settings_table.setRowCount(0)
@@ -2285,8 +2365,9 @@ class ExeSettingsDialog(QDialog):
process = QProcess(self) process = QProcess(self)
process.finished.connect(self.on_edit_db_finished) process.finished.connect(self.on_edit_db_finished)
args = ["cli", "--edit-db", self.exe_path] + changes process_args = ["cli", "--edit-db", self.exe_path] + changes
process.start(self.start_sh[0], args) args = self._get_process_args(process_args)
process.start(args[0], args[1:])
self.apply_button.setEnabled(False) self.apply_button.setEnabled(False)
def on_edit_db_finished(self, exit_code, exit_status): def on_edit_db_finished(self, exit_code, exit_status):

View File

@@ -200,13 +200,27 @@ 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
# 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(): 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))
self.coverLabel.setPixmap(rounded_pixmap) try:
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)
@@ -225,16 +239,28 @@ 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
badge.move(int(badge_x), int(badge_y)) try:
badge_y_positions.append(badge_y + badge.height()) badge.move(int(badge_x), int(badge_y))
badge_y_positions.append(badge_y + badge.height())
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
self.anticheatLabel.raise_() try:
self.protondbLabel.raise_() self.anticheatLabel.raise_()
self.portprotonLabel.raise_() self.protondbLabel.raise_()
self.egsLabel.raise_() self.portprotonLabel.raise_()
self.steamLabel.raise_() self.egsLabel.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)
@@ -255,33 +281,53 @@ 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:
label.setFixedWidth(badge_width) try:
label.setIconSize(icon_size, icon_space) label.setFixedWidth(badge_width)
label.setCardWidth(scaled_width) label.setIconSize(icon_size, icon_space)
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:
font = self.nameLabel.font() try:
new_font_size = self.base_font_size * self._scale font = self.nameLabel.font()
if new_font_size > 0: new_font_size = self.base_font_size * self._scale
font.setPointSizeF(new_font_size) if new_font_size > 0:
self.nameLabel.setFont(font) font.setPointSizeF(new_font_size)
self.nameLabel.setFont(font)
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
self.shadow.setBlurRadius(int(20 * self._scale)) try:
self.shadow.setBlurRadius(int(20 * self._scale))
except RuntimeError:
# Handle the case where the Qt object was deleted
pass
self.updateGeometry() try:
self.update() self.updateGeometry()
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
parent = self.parentWidget() try:
if parent: parent = self.parentWidget()
layout = parent.layout() if parent:
if layout: layout = parent.layout()
layout.invalidate() if layout:
layout.activate() layout.invalidate()
layout.update() layout.activate()
parent.updateGeometry() layout.update()
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
@@ -289,6 +335,10 @@ 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"))
@@ -296,11 +346,15 @@ 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))
self.steamLabel.setVisible(self.steam_visible) try:
self.egsLabel.setVisible(self.egs_visible) self.steamLabel.setVisible(self.steam_visible)
self.portprotonLabel.setVisible(self.portproton_visible) self.egsLabel.setVisible(self.egs_visible)
self.protondbLabel.setVisible(protondb_visible) self.portprotonLabel.setVisible(self.portproton_visible)
self.anticheatLabel.setVisible(anticheat_visible) self.protondbLabel.setVisible(protondb_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)
@@ -395,21 +449,33 @@ class GameCard(QFrame):
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
def update_favorite_icon(self): def update_favorite_icon(self):
if self.is_favorite: # Check if the card has been destroyed before updating
self.favoriteLabel.setText("") if not hasattr(self, 'coverLabel') or self.coverLabel is None:
else: return
self.favoriteLabel.setText("")
self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
parent = self.parent() try:
while parent: if self.is_favorite:
if hasattr(parent, 'game_library_manager'): self.favoriteLabel.setText("")
# Access using getattr with default to avoid Ruff B009 warning else:
manager = getattr(parent, 'game_library_manager', None) self.favoriteLabel.setText("")
if manager is not None: self.favoriteLabel.setStyleSheet(self.theme.FAVORITE_LABEL_STYLE)
QTimer.singleShot(0, manager.update_game_grid) except RuntimeError:
break # Handle the case where the Qt object was deleted
parent = parent.parent() 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()

View File

@@ -167,12 +167,18 @@ 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:
self.main_window.current_hovered_card._hovered = False try:
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 = 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:
self.main_window.current_focused_card._focused = False try:
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
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:
@@ -193,11 +199,19 @@ 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:
self.main_window.current_focused_card._focused = False try:
self.main_window.current_focused_card.clearFocus() if self.main_window.current_focused_card:
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:
self.main_window.current_hovered_card._hovered = False try:
self.main_window.current_hovered_card.leaveEvent(None) if self.main_window.current_hovered_card:
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:
@@ -476,6 +490,7 @@ 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
) )
@@ -498,6 +513,11 @@ 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)
@@ -505,17 +525,25 @@ 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

View File

@@ -1939,8 +1939,10 @@ class InputManager(QObject):
active_win.show_next() active_win.show_next()
return True # Consume event to prevent tab switching return True # Consume event to prevent tab switching
# Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit # Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit or QTableWidget or AutoSizeButton
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and not isinstance(focused, GameCard | QLineEdit) and not self.file_explorer: if (key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and
not isinstance(focused, GameCard | QLineEdit | QTableWidget | AutoSizeButton) and
not self.file_explorer):
idx = self._parent.stackedWidget.currentIndex() idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons) total = len(self._parent.tabButtons)
if key == Qt.Key.Key_Left: if key == Qt.Key.Key_Left:
@@ -1976,12 +1978,6 @@ class InputManager(QObject):
self.dpad_moved.emit(dpad_code, dpad_value, now) self.dpad_moved.emit(dpad_code, dpad_value, now)
return True return True
# Launch/stop game on detail page
if self._parent.currentDetailPage and key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if self._parent.current_exec_line:
self._parent.toggleGame(self._parent.current_exec_line, None)
return True
# Context menu for GameCard # Context menu for GameCard
if isinstance(focused, GameCard): if isinstance(focused, GameCard):
if key == Qt.Key.Key_F10 and modifiers & Qt.KeyboardModifier.ShiftModifier: if key == Qt.Key.Key_F10 and modifiers & Qt.KeyboardModifier.ShiftModifier:
@@ -1991,6 +1987,18 @@ class InputManager(QObject):
# General actions: Activate, Back, Add # General actions: Activate, Back, Add
if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter): if key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
# Special handling for table widgets with checkboxes
if isinstance(focused, QTableWidget):
current_row = focused.currentRow()
current_col = focused.currentColumn()
if current_row >= 0 and current_col >= 0:
# Check if the cell contains a checkbox
item = focused.item(current_row, current_col)
if item and (item.flags() & Qt.ItemFlag.ItemIsUserCheckable):
# Toggle the checkbox state
new_state = Qt.CheckState.Checked if item.checkState() == Qt.CheckState.Unchecked else Qt.CheckState.Unchecked
item.setCheckState(new_state)
return True
self._parent.activateFocusedWidget() self._parent.activateFocusedWidget()
return True return True
elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace): elif key in (Qt.Key.Key_Escape, Qt.Key.Key_Backspace):
@@ -2345,9 +2353,12 @@ class InputManager(QObject):
self.button_event.emit(event.code, event.value) self.button_event.emit(event.code, event.value)
# Special handling for menu on press only # Special handling for menu on press only
# Only handle menu button if our main window is currently active
if (event.value == 1 and event.code in BUTTONS['menu'] and if (event.value == 1 and event.code in BUTTONS['menu'] and
not self._is_gamescope_session and not self.in_guide_combination_attempt): not self._is_gamescope_session and not self.in_guide_combination_attempt):
self.toggle_fullscreen.emit(not self._is_fullscreen) # Check if our main window is the currently active window
if self._parent.isActiveWindow():
self.toggle_fullscreen.emit(not self._is_fullscreen)
elif event.type == ecodes.EV_ABS: elif event.type == ecodes.EV_ABS:
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}: if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
# Trigger handling for UI # Trigger handling for UI

View File

@@ -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-24 23:48+0500\n" "POT-Creation-Date: 2025-11-30 13:20+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"
@@ -217,6 +217,10 @@ 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 ""
@@ -320,6 +324,9 @@ 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 ""
@@ -452,6 +459,9 @@ msgstr ""
msgid "Unknown Game" msgid "Unknown Game"
msgstr "" msgstr ""
msgid "Starting PortProton..."
msgstr ""
msgid "Library" msgid "Library"
msgstr "" msgstr ""
@@ -473,6 +483,9 @@ msgstr ""
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Installation already in progress." msgid "Installation already in progress."
msgstr "" msgstr ""
@@ -492,9 +505,6 @@ msgstr ""
msgid "Installation error." msgid "Installation error."
msgstr "" msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Game library refreshed" msgid "Game library refreshed"
msgstr "" msgstr ""

View File

@@ -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-24 23:48+0500\n" "POT-Creation-Date: 2025-11-30 13:20+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"
@@ -217,6 +217,10 @@ 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 ""
@@ -320,6 +324,9 @@ 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 ""
@@ -452,6 +459,9 @@ msgstr ""
msgid "Unknown Game" msgid "Unknown Game"
msgstr "" msgstr ""
msgid "Starting PortProton..."
msgstr ""
msgid "Library" msgid "Library"
msgstr "" msgstr ""
@@ -473,6 +483,9 @@ msgstr ""
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Installation already in progress." msgid "Installation already in progress."
msgstr "" msgstr ""
@@ -492,9 +505,6 @@ msgstr ""
msgid "Installation error." msgid "Installation error."
msgstr "" msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Game library refreshed" msgid "Game library refreshed"
msgstr "" msgstr ""

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: 2025-11-24 23:48+0500\n" "POT-Creation-Date: 2025-11-30 13:20+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"
@@ -215,6 +215,10 @@ 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 ""
@@ -318,6 +322,9 @@ 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 ""
@@ -450,6 +457,9 @@ msgstr ""
msgid "Unknown Game" msgid "Unknown Game"
msgstr "" msgstr ""
msgid "Starting PortProton..."
msgstr ""
msgid "Library" msgid "Library"
msgstr "" msgstr ""
@@ -471,6 +481,9 @@ msgstr ""
msgid "Fullscreen" msgid "Fullscreen"
msgstr "" msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Installation already in progress." msgid "Installation already in progress."
msgstr "" msgstr ""
@@ -490,9 +503,6 @@ msgstr ""
msgid "Installation error." msgid "Installation error."
msgstr "" msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Game library refreshed" msgid "Game library refreshed"
msgstr "" msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-11-24 23:48+0500\n" "POT-Creation-Date: 2025-11-30 13:20+0500\n"
"PO-Revision-Date: 2025-11-24 23:47+0500\n" "PO-Revision-Date: 2025-11-30 13:18+0500\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language: ru_RU\n" "Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n" "Language-Team: ru_RU <LL@li.org>\n"
@@ -222,6 +222,10 @@ 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}"
@@ -327,6 +331,9 @@ 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 "Предпросмотр обложки:"
@@ -459,6 +466,9 @@ msgstr "В ожидании"
msgid "Unknown Game" msgid "Unknown Game"
msgstr "Неизвестная игра" msgstr "Неизвестная игра"
msgid "Starting PortProton..."
msgstr "Инициализация PortProton"
msgid "Library" msgid "Library"
msgstr "Библиотека" msgstr "Библиотека"
@@ -480,6 +490,9 @@ msgstr "Назад"
msgid "Fullscreen" msgid "Fullscreen"
msgstr "Полный экран" msgstr "Полный экран"
msgid "Refresh Grid"
msgstr "Обновить"
msgid "Installation already in progress." msgid "Installation already in progress."
msgstr "Установка уже выполняется." msgstr "Установка уже выполняется."
@@ -499,9 +512,6 @@ msgstr "Установка не удалась."
msgid "Installation error." msgid "Installation error."
msgstr "Ошибка установки." msgstr "Ошибка установки."
msgid "Refresh Grid"
msgstr "Обновить"
msgid "Game library refreshed" msgid "Game library refreshed"
msgstr "Игровая библиотека обновлена" msgstr "Игровая библиотека обновлена"

View File

@@ -780,11 +780,10 @@ class MainWindow(QMainWindow):
self.pending_games = [] self.pending_games = []
self.games = [] self.games = []
# Show initial progress bar and status message immediately # Show initial progress bar immediately
self.progress_bar.setRange(0, 100) # Set to determinate range self.progress_bar.setRange(0, 100) # Set to determinate range
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
self.progress_bar.setVisible(True) self.progress_bar.setVisible(True)
self.update_status_message.emit(_("Loading games..."), 0)
# Process events to keep UI responsive # Process events to keep UI responsive
QApplication.processEvents() QApplication.processEvents()
@@ -899,8 +898,8 @@ class MainWindow(QMainWindow):
self.update_status_message.emit self.update_status_message.emit
) )
# Run loading with minimal delay to allow UI to be responsive # Run loading immediately to show status without delay
QTimer.singleShot(100, start_loading) # Reduced to 100ms start_loading()
def _load_steam_games_async(self, callback: Callable[[list[tuple]], None]): def _load_steam_games_async(self, callback: Callable[[list[tuple]], None]):
steam_games = [] steam_games = []
@@ -1200,8 +1199,32 @@ class MainWindow(QMainWindow):
self.progress_bar.setRange(0, 0) # Indeterminate self.progress_bar.setRange(0, 0) # Indeterminate
self.update_status_message.emit(_("Refreshing game library..."), 0) self.update_status_message.emit(_("Refreshing game library..."), 0)
# Clear the game card cache and layout to force reload of custom data
if hasattr(self, 'game_library_manager') and self.game_library_manager:
# Clear the cache to ensure custom data is reloaded
self.game_library_manager.game_card_cache.clear()
self.game_library_manager.pending_images.clear()
# Clear search indices to rebuild with fresh data
if hasattr(self.game_library_manager, '_build_search_indices'):
# Mark for full rebuild of search indices
self.game_library_manager.dirty = True # Force full update
# Also clear the layout to ensure old widgets are removed
if (hasattr(self.game_library_manager, 'gamesListLayout') and
self.game_library_manager.gamesListLayout and
hasattr(self.game_library_manager, 'gamesListWidget') and
self.game_library_manager.gamesListWidget):
# Remove all widgets from the layout
self.game_library_manager.clear_layout(self.game_library_manager.gamesListLayout)
# Force layout update to ensure UI changes are visible
self.game_library_manager.gamesListWidget.updateGeometry()
if hasattr(self.game_library_manager, 'gamesListLayout'):
self.game_library_manager.gamesListLayout.update()
# Reload games using the existing loadGames functionality # Reload games using the existing loadGames functionality
QTimer.singleShot(0, self.loadGames) # Use a small delay to allow UI to update before starting the refresh
QTimer.singleShot(50, lambda: self.loadGames())
def on_search_text_changed(self, text: str): def on_search_text_changed(self, text: str):
"""Search text change handler with debounce.""" """Search text change handler with debounce."""
@@ -3206,7 +3229,10 @@ class MainWindow(QMainWindow):
# Игра стартовала устанавливаем флаг, обновляем кнопку на "Stop" # Игра стартовала устанавливаем флаг, обновляем кнопку на "Stop"
self._gameLaunched = True self._gameLaunched = True
if self.current_running_button is not None: if self.current_running_button is not None:
self.current_running_button.setText(_("Stop")) try:
self.current_running_button.setText(_("Stop"))
except RuntimeError:
self.current_running_button = None
#self._inhibit_screensaver() #self._inhibit_screensaver()
elif not child_running: elif not child_running:
# Игра завершилась сбрасываем флаг, сбрасываем кнопку и останавливаем таймер # Игра завершилась сбрасываем флаг, сбрасываем кнопку и останавливаем таймер
@@ -3225,13 +3251,16 @@ class MainWindow(QMainWindow):
Вызывается, когда игра завершилась (не по нажатию кнопки). Вызывается, когда игра завершилась (не по нажатию кнопки).
""" """
if self.current_running_button is not None: if self.current_running_button is not None:
self.current_running_button.setText(_("Play")) try:
icon = self.theme_manager.get_icon("play") self.current_running_button.setText(_("Play"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("play")
icon = QIcon(icon) # Convert path to QIcon if isinstance(icon, str):
elif icon is None: icon = QIcon(icon) # Convert path to QIcon
icon = QIcon() # Use empty QIcon as fallback elif icon is None:
self.current_running_button.setIcon(icon) icon = QIcon() # Use empty QIcon as fallback
self.current_running_button.setIcon(icon)
except RuntimeError:
pass
self.current_running_button = None self.current_running_button = None
self.target_exe = None self.target_exe = None
@@ -3284,13 +3313,16 @@ class MainWindow(QMainWindow):
pass pass
self.game_processes = [] self.game_processes = []
if update_button: if update_button:
update_button.setText(_("Play")) try:
icon = self.theme_manager.get_icon("play") update_button.setText(_("Play"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("play")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None: if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
self.checkProcessTimer.stop() self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater() self.checkProcessTimer.deleteLater()
@@ -3312,13 +3344,16 @@ class MainWindow(QMainWindow):
self.game_processes.append(process) self.game_processes.append(process)
save_last_launch(exe_name, datetime.now()) save_last_launch(exe_name, datetime.now())
if update_button: if update_button:
update_button.setText(_("Launching")) try:
icon = self.theme_manager.get_icon("stop") update_button.setText(_("Launching"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("stop")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
self.checkProcessTimer = QTimer(self) self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe) self.checkProcessTimer.timeout.connect(self.checkTargetExe)
@@ -3375,13 +3410,16 @@ class MainWindow(QMainWindow):
pass pass
self.game_processes = [] self.game_processes = []
if update_button: if update_button:
update_button.setText(_("Play")) try:
icon = self.theme_manager.get_icon("play") update_button.setText(_("Play"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("play")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None: if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
self.checkProcessTimer.stop() self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater() self.checkProcessTimer.deleteLater()
@@ -3403,13 +3441,16 @@ class MainWindow(QMainWindow):
self.game_processes.append(process) self.game_processes.append(process)
save_last_launch(exe_name, datetime.now()) save_last_launch(exe_name, datetime.now())
if update_button: if update_button:
update_button.setText(_("Launching")) try:
icon = self.theme_manager.get_icon("stop") update_button.setText(_("Launching"))
if isinstance(icon, str): icon = self.theme_manager.get_icon("stop")
icon = QIcon(icon) if isinstance(icon, str):
elif icon is None: icon = QIcon(icon)
icon = QIcon() elif icon is None:
update_button.setIcon(icon) icon = QIcon()
update_button.setIcon(icon)
except RuntimeError:
pass
self.checkProcessTimer = QTimer(self) self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe) self.checkProcessTimer.timeout.connect(self.checkTargetExe)

View File

@@ -66,8 +66,11 @@ class VirtualKeyboard(QFrame):
if not self.current_input_widget or not isinstance(self.current_input_widget, QLineEdit): if not self.current_input_widget or not isinstance(self.current_input_widget, QLineEdit):
return return
# Просто устанавливаем курсор на нужную позицию без выделения try:
self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition()) # Просто устанавливаем курсор на нужную позицию без выделения
self.current_input_widget.setCursorPosition(self.current_input_widget.cursorPosition())
except RuntimeError:
self.current_input_widget = None
def initUI(self): def initUI(self):
layout = QVBoxLayout() layout = QVBoxLayout()
@@ -290,31 +293,43 @@ class VirtualKeyboard(QFrame):
def up_key(self): def up_key(self):
"""Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима""" """Перемещает курсор в QLineEdit вверх/в начало, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
self.current_input_widget.setCursorPosition(0) try:
self.current_input_widget.setFocus() self.current_input_widget.setCursorPosition(0)
self.current_input_widget.setFocus()
except RuntimeError:
self.current_input_widget = None
def down_key(self): def down_key(self):
"""Перемещает курсор в QLineEdit вниз/в конец, если клавиатура видима""" """Перемещает курсор в QLineEdit вниз/в конец, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
self.current_input_widget.setCursorPosition(len(self.current_input_widget.text())) try:
self.current_input_widget.setFocus() self.current_input_widget.setCursorPosition(len(self.current_input_widget.text()))
self.current_input_widget.setFocus()
except RuntimeError:
self.current_input_widget = None
def left_key(self): def left_key(self):
"""Перемещает курсор в QLineEdit влево, если клавиатура видима""" """Перемещает курсор в QLineEdit влево, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
pos = self.current_input_widget.cursorPosition() try:
if pos > 0: pos = self.current_input_widget.cursorPosition()
self.current_input_widget.setCursorPosition(pos - 1) if pos > 0:
self.current_input_widget.setFocus() self.current_input_widget.setCursorPosition(pos - 1)
self.current_input_widget.setFocus()
except RuntimeError:
self.current_input_widget = None
def right_key(self): def right_key(self):
"""Перемещает курсор в QLineEdit вправо, если клавиатура видима""" """Перемещает курсор в QLineEdit вправо, если клавиатура видима"""
if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit): if self.current_input_widget and isinstance(self.current_input_widget, QLineEdit):
pos = self.current_input_widget.cursorPosition() try:
text_len = len(self.current_input_widget.text()) pos = self.current_input_widget.cursorPosition()
if pos < text_len: text_len = len(self.current_input_widget.text())
self.current_input_widget.setCursorPosition(pos + 1) if pos < text_len:
self.current_input_widget.setFocus() self.current_input_widget.setCursorPosition(pos + 1)
self.current_input_widget.setFocus()
except RuntimeError:
self.current_input_widget = None
def move_focus_up(self): def move_focus_up(self):
"""Перемещает фокус по кнопкам клавиатуры вверх с фиксированной скоростью""" """Перемещает фокус по кнопкам клавиатуры вверх с фиксированной скоростью"""
@@ -370,35 +385,41 @@ class VirtualKeyboard(QFrame):
self.on_shift_click(not self.shift_pressed) self.on_shift_click(not self.shift_pressed)
self.highlight_cursor_position() self.highlight_cursor_position()
elif self.current_input_widget is not None: elif self.current_input_widget is not None:
# Сохраняем текущую кнопку с фокусом try:
focused_button = self.focusWidget() # Сохраняем текущую кнопку с фокусом
key_to_restore = None focused_button = self.focusWidget()
if isinstance(focused_button, QPushButton) and focused_button in self.buttons.values(): key_to_restore = None
key_to_restore = next((k for k, btn in self.buttons.items() if btn == focused_button), None) if isinstance(focused_button, QPushButton) and focused_button in self.buttons.values():
key_to_restore = next((k for k, btn in self.buttons.items() if btn == focused_button), None)
key = "&" if key == "&&" else key key = "&" if key == "&&" else key
cursor_pos = self.current_input_widget.cursorPosition() cursor_pos = self.current_input_widget.cursorPosition()
text = self.current_input_widget.text() text = self.current_input_widget.text()
new_text = text[:cursor_pos] + key + text[cursor_pos:] new_text = text[:cursor_pos] + key + text[cursor_pos:]
self.current_input_widget.setText(new_text) self.current_input_widget.setText(new_text)
self.current_input_widget.setCursorPosition(cursor_pos + len(key)) self.current_input_widget.setCursorPosition(cursor_pos + len(key))
self.keyPressed.emit(key) self.keyPressed.emit(key)
self.highlight_cursor_position() self.highlight_cursor_position()
# Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа # Если был нажат SHIFT, но не CapsLock, отключаем его после ввода символа
if self.shift_pressed and not self.caps_lock: if self.shift_pressed and not self.caps_lock:
self.shift_pressed = False self.shift_pressed = False
self.update_keyboard() self.update_keyboard()
if key_to_restore and key_to_restore in self.buttons: if key_to_restore and key_to_restore in self.buttons:
self.buttons[key_to_restore].setFocus() self.buttons[key_to_restore].setFocus()
except RuntimeError:
self.current_input_widget = None
def on_tab_click(self): def on_tab_click(self):
if self.current_input_widget is not None: if self.current_input_widget is not None:
self.current_input_widget.insert('\t') try:
self.keyPressed.emit('Tab') self.current_input_widget.insert('\t')
if self.current_input_widget: self.keyPressed.emit('Tab')
self.current_input_widget.setFocus() if self.current_input_widget:
self.highlight_cursor_position() self.current_input_widget.setFocus()
self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
def on_caps_click(self): def on_caps_click(self):
"""Включаем/выключаем CapsLock""" """Включаем/выключаем CapsLock"""
@@ -417,15 +438,18 @@ class VirtualKeyboard(QFrame):
def on_backspace_click(self): def on_backspace_click(self):
"""Обработка одного нажатия Backspace""" """Обработка одного нажатия Backspace"""
if self.current_input_widget is not None: if self.current_input_widget is not None:
cursor_pos = self.current_input_widget.cursorPosition() try:
text = self.current_input_widget.text() cursor_pos = self.current_input_widget.cursorPosition()
text = self.current_input_widget.text()
if cursor_pos > 0: if cursor_pos > 0:
new_text = text[:cursor_pos - 1] + text[cursor_pos:] new_text = text[:cursor_pos - 1] + text[cursor_pos:]
self.current_input_widget.setText(new_text) self.current_input_widget.setText(new_text)
self.current_input_widget.setCursorPosition(cursor_pos - 1) self.current_input_widget.setCursorPosition(cursor_pos - 1)
self.keyPressed.emit('Backspace') self.keyPressed.emit('Backspace')
self.highlight_cursor_position() self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
def on_backspace_pressed(self): def on_backspace_pressed(self):
"""Обработка зажатого Backspace""" """Обработка зажатого Backspace"""
@@ -449,15 +473,21 @@ class VirtualKeyboard(QFrame):
# TODO: тут подумать, как обрабатывать нажатие. # TODO: тут подумать, как обрабатывать нажатие.
# Пока болванка перехода на новую строку, в QlineEdit работает как нажатие пробела # Пока болванка перехода на новую строку, в QlineEdit работает как нажатие пробела
if self.current_input_widget is not None: if self.current_input_widget is not None:
self.current_input_widget.insert('\n') try:
self.keyPressed.emit('Enter') self.current_input_widget.insert('\n')
self.keyPressed.emit('Enter')
except RuntimeError:
self.current_input_widget = None
def on_clear_click(self): def on_clear_click(self):
"""Чистим строку от введённого текста""" """Чистим строку от введённого текста"""
if self.current_input_widget is not None: if self.current_input_widget is not None:
self.current_input_widget.clear() try:
self.keyPressed.emit('Clear') self.current_input_widget.clear()
self.highlight_cursor_position() self.keyPressed.emit('Clear')
self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
def on_lang_click(self): def on_lang_click(self):
"""Переключение раскладки""" """Переключение раскладки"""
@@ -483,8 +513,11 @@ class VirtualKeyboard(QFrame):
def show_for_widget(self, widget): def show_for_widget(self, widget):
self.current_input_widget = widget self.current_input_widget = widget
if widget: if widget:
widget.setFocus() try:
self.highlight_cursor_position() widget.setFocus()
self.highlight_cursor_position()
except RuntimeError:
self.current_input_widget = None
# Позиционирование клавиатуры внизу родительского виджета # Позиционирование клавиатуры внизу родительского виджета
if self._parent and isinstance(self._parent, QWidget): if self._parent and isinstance(self._parent, QWidget):

View File

@@ -1,10 +1,10 @@
[build-system] [build-system]
requires = ["setuptools >= 77.0.3"] requires = ["setuptools >= 75.0.0", "wheel"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "portprotonqt" name = "portprotonqt"
version = "0.1.8" version = "0.1.9"
description = "A project to rewrite PortProton (PortWINE) using PySide" description = "A project to rewrite PortProton (PortWINE) using PySide"
readme = "README.md" readme = "README.md"
license = { text = "GPL-3.0" } license = { text = "GPL-3.0" }

77
setup.py Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Setup script for PortProtonQt
Debian package build configuration
"""
from setuptools import setup, find_packages
from pathlib import Path
import re
# Читаем версию из pyproject.toml простым regex
pyproject_file = Path(__file__).parent / "pyproject.toml"
version_match = re.search(r'^version\s*=\s*"([^"]+)"', pyproject_file.read_text(), re.MULTILINE)
version = version_match.group(1) if version_match else "0.0.0"
# Читаем README для long_description
readme_file = Path(__file__).parent / "README.md"
long_description = readme_file.read_text(encoding="utf-8") if readme_file.exists() else ""
setup(
name="portprotonqt",
version=version,
description="A project to rewrite PortProton (PortWINE) using PySide",
long_description=long_description,
long_description_content_type="text/markdown",
author="Boria138, BlackSnaker, Mikhail Tergoev(Castro-Fidel)",
author_email="",
url="https://github.com/Castro-Fidel/PortProton",
license="GPL-3.0",
# Классификаторы PyPI
classifiers=[
"Development Status :: 3 - Alpha",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: POSIX :: Linux",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Intended Audience :: End Users/Desktop",
"Topic :: Games/Entertainment",
],
keywords=["portproton", "wine", "game", "steam", "proton", "linux"],
# Python версия
python_requires=">=3.10",
# Пакеты
packages=find_packages(exclude=["build-aux", "dev-scripts", "documentation", "data"]),
# Включаемые файлы пакета
package_data={
"portprotonqt": [
"themes/**/*",
"themes/**/fonts/*",
"themes/**/images/*",
"themes/**/images/icons/*",
"themes/**/images/screenshots/*",
"locales/**/*",
"locales/**/*.po",
"locales/**/*.mo",
],
},
# Точка входа - исполняемый скрипт
entry_points={
"console_scripts": [
"portprotonqt=portprotonqt.app:main",
],
},
# Дополнительные опции
include_package_data=True,
zip_safe=False,
)

2
uv.lock generated
View File

@@ -552,7 +552,7 @@ wheels = [
[[package]] [[package]]
name = "portprotonqt" name = "portprotonqt"
version = "0.1.8" version = "0.1.9"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "babel" }, { name = "babel" },