10 Commits

Author SHA1 Message Date
Renovate Bot
92572bf5a1 chore(deps): update archlinux:base-devel docker digest to 87a967f 2025-10-19 00:01:20 +00:00
438e9737ea chore(release): drop sha256 sums
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 21:07:37 +05:00
2d39a4c740 fix: fix CloseEvent on native package
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 21:06:23 +05:00
567203b0b0 chore: bump to 0.1.8
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 18:22:32 +05:00
502cbc5030 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 18:20:50 +05:00
9b61215152 chore(theme): update screenshots
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 18:17:47 +05:00
10d3fe8ab4 chore(changelog): update
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 13:40:56 +05:00
a568ad9ef8 fix(add_game_dialog): prevent overwriting manually entered game name
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 13:09:58 +05:00
f074843fc8 fix: prevent udev monitor hang by using non-blocking poll with timeout
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 12:53:47 +05:00
4ab078b93e fix: sync card_width between GameLibraryManager and MainWindow to prevent config overwrite
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-10-18 12:17:17 +05:00
27 changed files with 131 additions and 86 deletions

View File

@@ -94,7 +94,7 @@ jobs:
name: Build Arch Package
runs-on: ubuntu-22.04
container:
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
image: archlinux:base-devel@sha256:87a967f07ba6319fc35c8c4e6ce6acdb4343b57aa817398a5d2db57bd8edc731
volumes:
- /usr:/usr-host
- /opt:/opt-host

View File

@@ -8,7 +8,7 @@ on:
env:
# Common version, will be used for tagging the release
VERSION: 0.1.7
VERSION: 0.1.8
PKGDEST: "/tmp/portprotonqt"
PACKAGE: "portprotonqt"
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
@@ -188,4 +188,4 @@ jobs:
tag_name: v${{ env.VERSION }}
prerelease: true
files: release/**/*
sha256sum: true
sha256sum: false

View File

@@ -138,7 +138,7 @@ jobs:
needs: changes
if: needs.changes.outputs.arch == 'true' || github.event_name == 'workflow_dispatch'
container:
image: archlinux:base-devel@sha256:06ab929f935145dd65994a89dd06651669ea28d43c812f3e24de990978511821
image: archlinux:base-devel@sha256:87a967f07ba6319fc35c8c4e6ce6acdb4343b57aa817398a5d2db57bd8edc731
volumes:
- /usr:/usr-host
- /opt:/opt-host

View File

@@ -3,16 +3,24 @@
Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [0.1.8] - 2025-10-18
### Added
- В настройки добавлен пункт для выбора типа геймпада для подсказок по управлению
- В настройки добавлен пункт для выбора сворачивать ли приложение в трей или нет
- К диалогу добавления игры, Winetricks, диалогу выбора файлов и виртуальной клавиатуре добавлены подсказки по управлению с геймпада
- Во вкладку автоустановок добавлен слайдер изменения размера карточек (они со слайдером в библиотеке независимы)
### Changed
- При завершении автоустановки приложение больше не перезапускается
- Выбор exe в диалоге добавления игры больше не перезаписывает введенное в поле название
- Обновлены и дополнены скриншоты темы
### Fixed
- Исправлено наложение карточек при смене фильтра игр
- Исправлена невозможность запуска приложения без подключёного геймпада
- Исправлена невозможность установки компонентов Winetricks через геймпад
- Ресиверы и виртуальные устройства больше не считаются за геймпад
### Contributors

View File

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

View File

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

View File

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

View File

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

View File

@@ -1034,8 +1034,8 @@ class AddGameDialog(QDialog):
"""Обработчик выбора файла в FileExplorer"""
self.exeEdit.setText(file_path)
self.last_exe_path = file_path # Update last selected exe path
if not self.edit_mode:
# Автоматически заполняем имя игры, если не в режиме редактирования
if not self.edit_mode and not self.nameEdit.text().strip():
# Автоматически заполняем имя игры, если не в режиме редактирования или если оно не введено вручную
game_name = os.path.splitext(os.path.basename(file_path))[0]
self.nameEdit.setText(game_name)

View File

@@ -33,6 +33,7 @@ class MainWindowProtocol(Protocol):
# Required attributes
searchEdit: CustomLineEdit
_last_card_width: int
card_width: int
current_hovered_card: GameCard | None
current_focused_card: GameCard | None
gamesListWidget: QWidget | None
@@ -128,6 +129,8 @@ class GameLibraryManager:
self.card_width = self.sizeSlider.value()
self.sizeSlider.setToolTip(f"{self.card_width} px")
save_card_size(self.card_width)
self.main_window.card_width = self.card_width
self.main_window._last_card_width = self.card_width
for card in self.game_card_cache.values():
card.update_card_size(self.card_width)
self.update_game_grid()

View File

@@ -1456,52 +1456,69 @@ class InputManager(QObject):
threading.Thread(target=self.run_udev_monitor, daemon=True).start()
logger.info("Gamepad support initialized with hotplug (evdev + pyudev)")
def run_udev_monitor(self) -> None:
"""
Неблокирующий опрос udev событий без MonitorObserver.
Использует monitor.poll() с таймаутом для корректного завершения.
Безопасный неблокирующий udev monitor для геймпадов.
Использует select.poll() вместо блокирующего monitor.poll().
"""
try:
logger.info("Starting udev monitor...")
monitor = Monitor.from_netlink(self.udev_context)
monitor.filter_by(subsystem='input')
monitor.start()
logger.info("Monitor started, draining initial events...")
# КРИТИЧНО: При старте udev отправляет события о ВСЕХ существующих устройствах
# Это может быть 10-50+ событий, которые блокируют инициализацию
# Решение: дренируем (игнорируем) все события за первые 500ms
try:
monitor.start()
except Exception as e:
logger.error(f"Failed to start udev monitor: {e}")
return
import select
fd = monitor.fileno()
poller = select.poll()
poller.register(fd, select.POLLIN)
# Короткий дренаж событий при запуске (0.5 сек)
drain_start = time.time()
drained_count = 0
while time.time() - drain_start < 0.5:
device = monitor.poll(timeout=0.1)
if device is not None:
events = poller.poll(100)
if not events:
continue
try:
_ = monitor.poll(timeout=0) # просто читаем, не обрабатываем
drained_count += 1
except Exception:
break
self.monitor_ready = True
logger.info(f"Drained {drained_count} initial events, now monitoring hotplug...")
# Основной цикл опроса с таймаутом 1 секунда
# Основной цикл
while self.running:
# poll() возвращает None при таймауте - не блокирует навсегда
device = monitor.poll(timeout=1.0)
events = poller.poll(1000) # 1 сек таймаут
if not events:
continue # просто ждём, не блокируем
if device is not None:
action = device.action
try:
device = monitor.poll(timeout=0)
except Exception as e:
logger.debug(f"Monitor poll failed: {e}")
continue
# Фильтруем только джойстики на уровне callback
# Это предотвращает обработку мышей/клавиатур/и т.д.
if action and self._is_joystick_device(device):
logger.info(f"Joystick hotplug event: {action} for {device.sys_name}")
self.handle_udev_event(action, device)
if not device:
continue
action = device.action
if action and self._is_joystick_device(device):
logger.info(f"Joystick hotplug event: {action} for {device.sys_name}")
# отправляем сигнал в Qt-поток
self.handle_udev_event(action, device)
logger.info("udev monitor stopped gracefully")
except Exception as e:
logger.error(f"Error in udev monitor: {e}", exc_info=True)
def _is_joystick_device(self, device: Device) -> bool:
"""
Быстрая проверка: является ли устройство джойстиком.

View File

@@ -3056,56 +3056,73 @@ class MainWindow(QMainWindow):
"""Обработчик закрытия окна: проверяет настройку minimize_to_tray.
Если True — сворачиваем в трей (по умолчанию). Иначе — полностью закрываем.
"""
minimize_to_tray = read_minimize_to_tray() # Импорт read_minimize_to_tray из config_utils
if hasattr(self, 'is_exiting') and self.is_exiting or not minimize_to_tray:
# Принудительное закрытие: завершаем процессы и приложение
for proc in self.game_processes:
try:
parent = psutil.Process(proc.pid)
children = parent.children(recursive=True)
for child in children:
try:
logger.debug(f"Terminating child process {child.pid}")
child.terminate()
except psutil.NoSuchProcess:
logger.debug(f"Child process {child.pid} already terminated")
psutil.wait_procs(children, timeout=5)
for child in children:
if child.is_running():
logger.debug(f"Killing child process {child.pid}")
child.kill()
logger.debug(f"Terminating process group {proc.pid}")
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except (psutil.NoSuchProcess, ProcessLookupError) as e:
logger.debug(f"Process {proc.pid} already terminated: {e}")
minimize_to_tray = read_minimize_to_tray()
self.game_processes = [] # Очищаем список процессов
# Очищаем таймеры
if hasattr(self, 'games_load_timer') and self.games_load_timer is not None and self.games_load_timer.isActive():
self.games_load_timer.stop()
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer is not None and self.settingsDebounceTimer.isActive():
self.settingsDebounceTimer.stop()
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer is not None and self.searchDebounceTimer.isActive():
self.searchDebounceTimer.stop()
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None and self.checkProcessTimer.isActive():
self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None
if hasattr(self, 'wine_monitor_timer') and self.wine_monitor_timer is not None:
self.wine_monitor_timer.stop()
self.wine_monitor_timer.deleteLater()
self.wine_monitor_timer = None
# Сохраняем настройки окна
if not read_fullscreen_config():
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
save_window_geometry(self.width(), self.height())
save_card_size(self.card_width)
save_auto_card_size(self.auto_card_width)
event.accept()
else:
# Сворачиваем в трей вместо закрытия
self.hide()
if minimize_to_tray:
# Просто сворачиваем в трей
event.ignore()
self.hide()
return
# Полное закрытие приложения
self.is_exiting = True
event.accept()
# Скрываем и удаляем иконку трея
if hasattr(self, "tray_manager") and self.tray_manager.tray_icon:
self.tray_manager.tray_icon.hide()
self.tray_manager.tray_icon.deleteLater()
# Сохраняем размеры карточек
save_card_size(self.card_width)
save_auto_card_size(self.auto_card_width)
# Сохраняем размеры окна (если не в полноэкранном режиме)
if not read_fullscreen_config():
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
save_window_geometry(self.width(), self.height())
# Завершаем все игровые процессы
for proc in getattr(self, "game_processes", []):
try:
parent = psutil.Process(proc.pid)
children = parent.children(recursive=True)
for child in children:
try:
logger.debug(f"Terminating child process {child.pid}")
child.terminate()
except psutil.NoSuchProcess:
logger.debug(f"Child process {child.pid} already terminated")
psutil.wait_procs(children, timeout=5)
for child in children:
if child.is_running():
logger.debug(f"Killing child process {child.pid}")
child.kill()
logger.debug(f"Terminating process group {proc.pid}")
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except (psutil.NoSuchProcess, ProcessLookupError) as e:
logger.debug(f"Process {getattr(proc, 'pid', '?')} already terminated: {e}")
except Exception as e:
logger.warning(f"Failed to terminate process {getattr(proc, 'pid', '?')}: {e}")
self.game_processes = []
# Универсальная остановка и удаление таймеров
timers = [
"games_load_timer",
"settingsDebounceTimer",
"searchDebounceTimer",
"checkProcessTimer",
"wine_monitor_timer",
]
for tname in timers:
timer = getattr(self, tname, None)
if timer and timer.isActive():
timer.stop()
if timer:
timer.deleteLater()
setattr(self, tname, None)

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "portprotonqt"
version = "0.1.7"
version = "0.1.8"
description = "A project to rewrite PortProton (PortWINE) using PySide"
readme = "README.md"
license = { text = "GPL-3.0" }

2
uv.lock generated
View File

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