16 Commits

Author SHA1 Message Date
b8773f3b68 chore(build): added Debian
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-12-03 21:26:55 +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
38 changed files with 12803 additions and 1002 deletions

View File

@@ -8,6 +8,36 @@ env:
PACKAGE: "portprotonqt"
jobs:
build-deb:
name: Build Debian Package
runs-on: ubuntu-22.04
steps:
- name: Install build dependencies
run: |
apt-get update
apt-get install -y build-essential git python3-dev python3-pip python3-wheel debhelper dh-python \
devscripts python3-all bash-completion python3-setuptools
- name: Checkout repo
uses: https://gitea.com/actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Build Debian package
run: |
# Copy the debian directory to the root for proper packaging
cp -r build-aux/debian .
# Build the package
dpkg-buildpackage -us -uc -b
mv ../*.deb .
- name: Upload Debian package
uses: https://gitea.com/actions/gitea-upload-artifact@v4
with:
name: PortProtonQt-Debian
path: "*.deb"
build-appimage:
name: Build AppImage
runs-on: ubuntu-22.04

View File

@@ -8,7 +8,7 @@ on:
env:
# Common version, will be used for tagging the release
VERSION: 0.1.8
VERSION: 0.1.9
PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt"
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
@@ -147,9 +147,37 @@ jobs:
name: PortProtonQt-RPM-Fedora-${{ matrix.fedora_version }}
path: /home/rpmbuild/RPMS/**/*.rpm
build-deb:
name: Build Debian Package
runs-on: ubuntu-22.04
steps:
- name: Install build dependencies
run: |
apt-get update
apt-get install -y build-essential git python3-dev python3-pip python3-wheel debhelper dh-python \
devscripts python3-all bash-completion python3-setuptools
- name: Checkout repo
uses: https://gitea.com/actions/checkout@v4
- name: Build Debian package
run: |
# Copy the debian directory to the root for proper packaging
cp -r build-aux/debian .
# Build the package
dpkg-buildpackage -us -uc -b
- name: Upload Debian package
uses: https://gitea.com/actions/gitea-upload-artifact@v4
with:
name: PortProtonQt-Debian
path: "*.deb"
release:
name: Create and Publish Release
needs: [build-appimage, build-arch, build-fedora]
needs: [build-appimage, build-arch, build-fedora, build-deb]
runs-on: ubuntu-latest
steps:
- uses: https://gitea.com/actions/checkout@v4

View File

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

View File

@@ -16,7 +16,7 @@ source .venv/bin/activate
### Установка (release)
Выберите подходящий пакет для вашей системы или AppImage.
Выберите подходящий пакет для вашей системы (deb, rpm, arch) или AppImage.
Запуск производится по команде portprotonqt или по ярлыку в меню

View File

@@ -37,7 +37,7 @@ AppDir:
id: ru.linux_gaming.PortProtonQt
name: PortProtonQt
icon: ru.linux_gaming.PortProtonQt
version: 0.1.8
version: 0.1.9
exec: usr/bin/python3
exec_args: "-m portprotonqt.app $@"
apt:

View File

@@ -1,5 +1,5 @@
pkgname=portprotonqt
pkgver=0.1.8
pkgver=0.1.9
pkgrel=1
pkgdesc="Modern GUI for managing and launching games from PortProton, Steam, and Epic Games Store"
arch=('any')

View File

@@ -0,0 +1,5 @@
portprotonqt (0.1.9-1) unstable; urgency=medium
* Initial release for Debian packaging
-- Boris Yumankulov <boria138@altlinux.org> Wed, 03 Dec 2025 12:00:00 +0000

44
build-aux/debian/control Normal file
View File

@@ -0,0 +1,44 @@
Source: portprotonqt
Section: python
Priority: optional
Maintainer: Boris Yumankulov <boria138@altlinux.org>
Build-Depends: debhelper-compat (= 13),
dh-python,
python3-all,
python3-setuptools,
python3-pip
Standards-Version: 4.6.0
Homepage: https://git.linux-gaming.ru/Boria138/PortProtonQt
Rules-Requires-Root: no
X-Python3-Version: >= 3.10
Package: 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,
curl,
gzip,
unzip,
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.

View File

@@ -0,0 +1,24 @@
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: 2023-2025 Boria138, BlackSnaker, Mikhail Tergoev(Castro-Fidel)
License: GPL-3.0+
License: GPL-3.0+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
.
On Debian systems, the complete text of the GPL-3.0+ license
can be found in /usr/share/common-licenses/GPL-3.

25
build-aux/debian/rules Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/make -f
%:
dh $@ --with python3
override_dh_auto_build:
python3 -m pip install --user --no-deps --ignore-installed .
override_dh_auto_install:
# Install Python modules
python3 -m pip install --no-deps --ignore-installed --prefix=$(CURDIR)/debian/portprotonqt/usr .
# Install desktop files and icons
install -d $(CURDIR)/debian/portprotonqt/usr/share/applications
install -m 644 $(CURDIR)/../build-aux/share/applications/ru.linux_gaming.PortProtonQt.desktop \
$(CURDIR)/debian/portprotonqt/usr/share/applications/
install -d $(CURDIR)/debian/portprotonqt/usr/share/icons/hicolor/scalable/apps
install -m 644 $(CURDIR)/../build-aux/share/icons/hicolor/scalable/apps/ru.linux_gaming.PortProtonQt.svg \
$(CURDIR)/debian/portprotonqt/usr/share/icons/hicolor/scalable/apps/
# Install metainfo file
install -d $(CURDIR)/debian/portprotonqt/usr/share/metainfo
install -m 644 $(CURDIR)/../build-aux/share/metainfo/ru.linux_gaming.PortProtonQt.metainfo.xml \
$(CURDIR)/debian/portprotonqt/usr/share/metainfo/

View File

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

View File

@@ -0,0 +1,7 @@
extend-diff-ignore = "^[^/]*[.]egg-info/"
compression = "gzip"
tar-ignore = "dev-scripts"
tar-ignore = ".*"
tar-ignore = "__pycache__"
tar-ignore = "build-aux"
tar-ignore = "data"

View File

@@ -1,5 +1,5 @@
%global pypi_name portprotonqt
%global pypi_version 0.1.8
%global pypi_version 0.1.9
%global oname PortProtonQt
%global _python_no_extras_requires 1

View File

@@ -1373,7 +1373,7 @@
},
{
"normalized_name": "arena breakout infinite",
"status": "Broken"
"status": "Denied"
},
{
"normalized_name": "pixel gun 3d pc",
@@ -4316,7 +4316,7 @@
"status": "Broken"
},
{
"normalized_name": "solo leveling arise",
"normalized_name": "solo leveling arise overdrive",
"status": "Running"
},
{
@@ -4527,10 +4527,6 @@
"normalized_name": "project wraith",
"status": "Broken"
},
{
"normalized_name": "solo leveling arise",
"status": "Broken"
},
{
"normalized_name": "freedom wars",
"status": "Running"
@@ -4542,5 +4538,9 @@
{
"normalized_name": "no more room in hell 2",
"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",
"slug": "split-second"
@@ -11,10 +135,6 @@
"normalized_title": "foundation",
"slug": "foundation"
},
{
"normalized_title": "земский собор [демо]",
"slug": "zemskij-sobor-demo"
},
{
"normalized_title": "crusader kings 3",
"slug": "crusader-kings-3"
@@ -1411,10 +1531,6 @@
"normalized_title": "world of sea battle",
"slug": "world-of-sea-battle"
},
{
"normalized_title": "escape from tarkov",
"slug": "escape-from-tarkov"
},
{
"normalized_title": "bayonetta",
"slug": "bayonetta"
@@ -1539,10 +1655,6 @@
"normalized_title": "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",
"slug": "call-of-duty-world-at-war"
@@ -1735,10 +1847,6 @@
"normalized_title": "elden ring",
"slug": "elden-ring"
},
{
"normalized_title": "starcraft",
"slug": "starcraft-remastered"
},
{
"normalized_title": "cataclismo",
"slug": "cataclismo"

Binary file not shown.

View File

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

View File

@@ -0,0 +1,2 @@
"""PortProtonQt - A project to rewrite PortProton (PortWINE) using PySide."""
__version__ = "0.1.9"

View File

@@ -17,7 +17,7 @@ from portprotonqt.cli import parse_args
__app_id__ = "ru.linux_gaming.PortProtonQt"
__app_name__ = "PortProtonQt"
__app_version__ = "0.1.8"
__app_version__ = "0.1.9"
def get_version():
try:

View File

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

View File

@@ -1035,7 +1035,15 @@ Icon={icon_path}
)
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]
xdg_data_home = os.getenv(
"XDG_DATA_HOME",
@@ -1043,16 +1051,25 @@ Icon={icon_path}
)
custom_folder = os.path.join(xdg_data_home, "PortProtonQt", "custom_data", exe_name)
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"]:
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:
self.signals.show_warning_dialog.emit(
_("Error"),
_("Failed to copy cover image: {error}").format(error=str(e))
)
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):
"""

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 icoextract import IconExtractor, IconExtractorError
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.logger import get_logger
from portprotonqt.theme_manager import ThemeManager
@@ -906,6 +906,7 @@ class AddGameDialog(QDialog):
self.coverEdit = CustomLineEdit(self, theme=self.theme)
self.coverEdit.setStyleSheet(self.theme.ADDGAME_INPUT_STYLE)
self.coverEdit.setPlaceholderText(_("Enter local path or URL for cover image"))
if cover_path:
self.coverEdit.setText(cover_path)
@@ -949,7 +950,12 @@ class AddGameDialog(QDialog):
# Подключение сигналов
self.select_button.clicked.connect(self.accept)
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)
# Установка одинаковой ширины для кнопок и полей ввода
@@ -1094,22 +1100,32 @@ class AddGameDialog(QDialog):
self.coverPreview.setText(_("Failed to download cover"))
logger.warning(f"Failed to download cover to {file_path}")
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):
"""Update the cover preview image."""
cover_path = self.coverEdit.text().strip()
exe_path = self.exeEdit.text().strip()
# Check if cover_path is a URL
url_pattern = r'^https?://[^\s/$.?#].[^\s]*$'
if re.match(url_pattern, cover_path):
# Check if cover_path is a URL by checking for common image extensions
image_extensions = ('.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp')
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
fd, local_path = tempfile.mkstemp(suffix=".png")
os.close(fd)
os.unlink(local_path)
# 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(
url=cover_path,
url=download_url,
local_path=local_path,
timeout=10,
callback=self.handleDownloadedCover
@@ -1701,8 +1717,10 @@ class ExeSettingsDialog(QDialog):
if self.portproton_path is None:
logger.error("PortProton location not found")
return
base_path = os.path.join(self.portproton_path, "data")
self.start_sh = [os.path.join(base_path, "scripts", "start.sh")]
self.start_sh = get_portproton_start_command()
if self.start_sh is None:
logger.error("PortProton start command not found")
return
self.dist_options = []
self.prefix_options = []
@@ -1776,9 +1794,9 @@ class ExeSettingsDialog(QDialog):
self.load_current_settings()
def _get_process_args(self, subcommand_args):
"""Get the full arguments for QProcess.start, handling flatpak separator."""
if self.start_sh[0] == "flatpak":
return self.start_sh[1:] + ["--"] + subcommand_args
"""Get the full arguments for QProcess.start, handling flatpak format."""
if self.start_sh and self.start_sh[0] == "flatpak":
return self.start_sh + subcommand_args
else:
return self.start_sh + subcommand_args
@@ -1814,7 +1832,7 @@ class ExeSettingsDialog(QDialog):
# Connect tab change to update description hint
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.setAlternatingRowColors(True)
self.settings_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
@@ -1829,11 +1847,30 @@ class ExeSettingsDialog(QDialog):
self.settings_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.settings_table.setTextElideMode(Qt.TextElideMode.ElideNone)
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
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.setAlternatingRowColors(True)
self.advanced_table.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
@@ -1850,7 +1887,26 @@ class ExeSettingsDialog(QDialog):
self.advanced_table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.advanced_table.setTextElideMode(Qt.TextElideMode.ElideNone)
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
self.advanced_table.currentCellChanged.connect(self.on_table_selection_changed)
@@ -1888,9 +1944,14 @@ class ExeSettingsDialog(QDialog):
def load_current_settings(self):
"""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.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):
"""Handle --list-db output and extract available keys and system info."""
@@ -1937,7 +1998,8 @@ class ExeSettingsDialog(QDialog):
# Load current settings
process = QProcess(self)
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):
"""Handle --show-ppdb output."""
@@ -1977,6 +2039,10 @@ class ExeSettingsDialog(QDialog):
self.populate_table()
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):
"""Populate the table with settings that are available in both lists."""
self.settings_table.setRowCount(0)
@@ -2285,8 +2351,9 @@ class ExeSettingsDialog(QDialog):
process = QProcess(self)
process.finished.connect(self.on_edit_db_finished)
args = ["cli", "--edit-db", self.exe_path] + changes
process.start(self.start_sh[0], args)
process_args = ["cli", "--edit-db", self.exe_path] + changes
args = self._get_process_args(process_args)
process.start(args[0], args[1:])
self.apply_button.setEnabled(False)
def on_edit_db_finished(self, exit_code, exit_status):

View File

@@ -505,17 +505,25 @@ class GameLibraryManager:
"""Clears all widgets from the layout."""
if layout is None:
return
# Remove all widgets from the layout and clean up caches
while layout.count():
child = layout.takeAt(0)
if child.widget():
widget = child.widget()
# Clean up cache if widget exists in it
for key, card in list(self.game_card_cache.items()):
if card == widget:
del self.game_card_cache[key]
if key in self.pending_images:
del self.pending_images[key]
break
# Always schedule widget for deletion regardless of cache state
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]):
"""Sets the games list and updates the filtered games."""
self.games = games

View File

@@ -1939,8 +1939,10 @@ class InputManager(QObject):
active_win.show_next()
return True # Consume event to prevent tab switching
# Handle tab switching with Left/Right arrow keys when not in GameCard focus or QLineEdit
if key in (Qt.Key.Key_Left, Qt.Key.Key_Right) and not isinstance(focused, GameCard | QLineEdit) and not self.file_explorer:
# 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 | QTableWidget | AutoSizeButton) and
not self.file_explorer):
idx = self._parent.stackedWidget.currentIndex()
total = len(self._parent.tabButtons)
if key == Qt.Key.Key_Left:
@@ -1976,12 +1978,6 @@ class InputManager(QObject):
self.dpad_moved.emit(dpad_code, dpad_value, now)
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
if isinstance(focused, GameCard):
if key == Qt.Key.Key_F10 and modifiers & Qt.KeyboardModifier.ShiftModifier:
@@ -1991,6 +1987,18 @@ class InputManager(QObject):
# General actions: Activate, Back, Add
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()
return True
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)
# 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
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:
if event.code in {ecodes.ABS_Z, ecodes.ABS_RZ}:
# Trigger handling for UI

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de_DE\n"
@@ -217,6 +217,10 @@ msgstr ""
msgid "Failed to copy cover image: {error}"
msgstr ""
#, python-brace-format
msgid "Unsupported image format: {extension}"
msgstr ""
#, python-brace-format
msgid "Failed to add '{game_name}' to Steam: {error}"
msgstr ""
@@ -320,6 +324,9 @@ msgstr ""
msgid "Custom Cover:"
msgstr ""
msgid "Enter local path or URL for cover image"
msgstr ""
msgid "Cover Preview:"
msgstr ""
@@ -452,6 +459,9 @@ msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Starting PortProton..."
msgstr ""
msgid "Library"
msgstr ""
@@ -473,6 +483,9 @@ msgstr ""
msgid "Fullscreen"
msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Installation already in progress."
msgstr ""
@@ -492,9 +505,6 @@ msgstr ""
msgid "Installation error."
msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Game library refreshed"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es_ES\n"
@@ -217,6 +217,10 @@ msgstr ""
msgid "Failed to copy cover image: {error}"
msgstr ""
#, python-brace-format
msgid "Unsupported image format: {extension}"
msgstr ""
#, python-brace-format
msgid "Failed to add '{game_name}' to Steam: {error}"
msgstr ""
@@ -320,6 +324,9 @@ msgstr ""
msgid "Custom Cover:"
msgstr ""
msgid "Enter local path or URL for cover image"
msgstr ""
msgid "Cover Preview:"
msgstr ""
@@ -452,6 +459,9 @@ msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Starting PortProton..."
msgstr ""
msgid "Library"
msgstr ""
@@ -473,6 +483,9 @@ msgstr ""
msgid "Fullscreen"
msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Installation already in progress."
msgstr ""
@@ -492,9 +505,6 @@ msgstr ""
msgid "Installation error."
msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Game library refreshed"
msgstr ""

View File

@@ -9,7 +9,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PortProtonQt 0.1.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-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"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -215,6 +215,10 @@ msgstr ""
msgid "Failed to copy cover image: {error}"
msgstr ""
#, python-brace-format
msgid "Unsupported image format: {extension}"
msgstr ""
#, python-brace-format
msgid "Failed to add '{game_name}' to Steam: {error}"
msgstr ""
@@ -318,6 +322,9 @@ msgstr ""
msgid "Custom Cover:"
msgstr ""
msgid "Enter local path or URL for cover image"
msgstr ""
msgid "Cover Preview:"
msgstr ""
@@ -450,6 +457,9 @@ msgstr ""
msgid "Unknown Game"
msgstr ""
msgid "Starting PortProton..."
msgstr ""
msgid "Library"
msgstr ""
@@ -471,6 +481,9 @@ msgstr ""
msgid "Fullscreen"
msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Installation already in progress."
msgstr ""
@@ -490,9 +503,6 @@ msgstr ""
msgid "Installation error."
msgstr ""
msgid "Refresh Grid"
msgstr ""
msgid "Game library refreshed"
msgstr ""

View File

@@ -9,8 +9,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2025-11-24 23:48+0500\n"
"PO-Revision-Date: 2025-11-24 23:47+0500\n"
"POT-Creation-Date: 2025-11-30 13:20+0500\n"
"PO-Revision-Date: 2025-11-30 13:18+0500\n"
"Last-Translator: \n"
"Language: ru_RU\n"
"Language-Team: ru_RU <LL@li.org>\n"
@@ -222,6 +222,10 @@ msgstr "Не удалось сохранить файл .desktop: {error}"
msgid "Failed to copy cover image: {error}"
msgstr "Не удалось скопировать обложку: {error}"
#, python-brace-format
msgid "Unsupported image format: {extension}"
msgstr "Неподдерживаемый формат изображения: {extension}"
#, python-brace-format
msgid "Failed to add '{game_name}' to Steam: {error}"
msgstr "Не удалось добавить '{game_name}' в Steam: {error}"
@@ -327,6 +331,9 @@ msgstr "Обзор..."
msgid "Custom Cover:"
msgstr "Обложка:"
msgid "Enter local path or URL for cover image"
msgstr "Введите локальный путь или URL обложки"
msgid "Cover Preview:"
msgstr "Предпросмотр обложки:"
@@ -459,6 +466,9 @@ msgstr "В ожидании"
msgid "Unknown Game"
msgstr "Неизвестная игра"
msgid "Starting PortProton..."
msgstr "Инициализация PortProton"
msgid "Library"
msgstr "Библиотека"
@@ -480,6 +490,9 @@ msgstr "Назад"
msgid "Fullscreen"
msgstr "Полный экран"
msgid "Refresh Grid"
msgstr "Обновить"
msgid "Installation already in progress."
msgstr "Установка уже выполняется."
@@ -499,9 +512,6 @@ msgstr "Установка не удалась."
msgid "Installation error."
msgstr "Ошибка установки."
msgid "Refresh Grid"
msgstr "Обновить"
msgid "Game library refreshed"
msgstr "Игровая библиотека обновлена"

View File

@@ -780,11 +780,10 @@ class MainWindow(QMainWindow):
self.pending_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.setValue(0)
self.progress_bar.setVisible(True)
self.update_status_message.emit(_("Loading games..."), 0)
# Process events to keep UI responsive
QApplication.processEvents()
@@ -899,8 +898,8 @@ class MainWindow(QMainWindow):
self.update_status_message.emit
)
# Run loading with minimal delay to allow UI to be responsive
QTimer.singleShot(100, start_loading) # Reduced to 100ms
# Run loading immediately to show status without delay
start_loading()
def _load_steam_games_async(self, callback: Callable[[list[tuple]], None]):
steam_games = []
@@ -1200,8 +1199,32 @@ class MainWindow(QMainWindow):
self.progress_bar.setRange(0, 0) # Indeterminate
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
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):
"""Search text change handler with debounce."""

View File

@@ -1,10 +1,10 @@
[build-system]
requires = ["setuptools >= 77.0.3"]
requires = ["setuptools >= 77.0.3", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "portprotonqt"
version = "0.1.8"
version = "0.1.9"
description = "A project to rewrite PortProton (PortWINE) using PySide"
readme = "README.md"
license = { text = "GPL-3.0" }
@@ -46,12 +46,13 @@ dependencies = [
[project.scripts]
portprotonqt = "portprotonqt.app:main"
[tool.setuptools]
packages = ["portprotonqt"]
include-package-data = true
[tool.setuptools.package-data]
"portprotonqt" = ["themes/**/*", "locales/**/*"]
[tool.setuptools.packages.find]
exclude = ["build-aux", "dev-scripts", "documentation", "data"]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors

2
uv.lock generated
View File

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