Compare commits

..

4 Commits

Author SHA1 Message Date
ec10d184ae feat: added import to context menu
All checks were successful
Check Translations / check-translations (pull_request) Successful in 17s
Code and build check / Check code (pull_request) Successful in 1m44s
Code and build check / Build with uv (pull_request) Successful in 58s
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 20:47:09 +05:00
e29ca92a13 feat: replace steam placeholder icon to real egs icon
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 20:14:51 +05:00
457cdf2963 feat: added handle egs games to toggleGame
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 20:14:36 +05:00
3137dedcff Revert "feat: hide the games from EGS until after the workout"
This reverts commit a21705da15.

Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
2025-06-15 20:13:08 +05:00
10 changed files with 91 additions and 305 deletions

View File

@ -40,7 +40,7 @@ jobs:
strategy: strategy:
matrix: matrix:
fedora_version: [41, 42, rawhide] fedora_version: [40, 41, 42, rawhide]
container: container:
image: fedora:${{ matrix.fedora_version }} image: fedora:${{ matrix.fedora_version }}

View File

@ -97,7 +97,7 @@ jobs:
strategy: strategy:
matrix: matrix:
fedora_version: [41, 42, rawhide] fedora_version: [40, 41, 42, rawhide]
container: container:
image: fedora:${{ matrix.fedora_version }} image: fedora:${{ matrix.fedora_version }}
@ -145,20 +145,13 @@ jobs:
- name: Install required dependencies - name: Install required dependencies
run: | run: |
sudo apt update sudo apt update
sudo apt install -y original-awk unzip sudo apt install -y original-awk
- name: Download all artifacts - name: Download all artifacts
uses: https://gitea.com/actions/download-artifact@v3 uses: https://gitea.com/actions/download-artifact@v3
with: with:
path: release/ path: release/
- name: Extract downloaded artifacts
run: |
mkdir -p extracted
find release/ -name '*.zip' -exec unzip -o {} -d extracted/ \;
find extracted/ -type f -exec mv {} release/ \;
rm -rf extracted/
- name: Extract changelog for version - name: Extract changelog for version
id: changelog id: changelog
run: | run: |
@ -170,7 +163,7 @@ jobs:
with: with:
body_path: changelog.txt body_path: changelog.txt
token: ${{ env.GITEA_TOKEN }} token: ${{ env.GITEA_TOKEN }}
tag_name: v${{ env.VERSION }} tag_name: ${{ env.VERSION }}
prerelease: true prerelease: true
files: release/**/* files: release/**/*
sha256sum: true sha256sum: true

View File

@ -3,18 +3,6 @@
Все заметные изменения в этом проекте фиксируются в этом файле. Все заметные изменения в этом проекте фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/). Формат основан на [Keep a Changelog](https://keepachangelog.com/) и придерживается принципов [Semantic Versioning](https://semver.org/).
## [Unreleased]
### Added
### Changed
- Удалены сборки для Fedora 40
### Fixed
- Дублирование обводки выделения карточек при быстром перемешении мыши
---
## [0.1.2] - 2025-06-15 ## [0.1.2] - 2025-06-15
### Added ### Added
@ -74,7 +62,6 @@
- Исправлены ошибки при подключении геймпада - Исправлены ошибки при подключении геймпада
- Предотвращено многократное открытие диалога добавления игры через геймпад - Предотвращено многократное открытие диалога добавления игры через геймпад
- Корректная обработка событий геймпада во время игры - Корректная обработка событий геймпада во время игры
- Убийсво всех процессов "зомби" при закрытии программы
--- ---

View File

@ -28,19 +28,19 @@ BuildRequires: git
%package -n python3-%{pypi_name}-git %package -n python3-%{pypi_name}-git
Summary: %{summary} Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}} %{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3-babel Requires: python3dist(babel)
Requires: python3-evdev Requires: python3dist(evdev)
Requires: python3-icoextract Requires: python3dist(icoextract)
Requires: python3-numpy Requires: python3dist(numpy)
Requires: python3-orjson Requires: python3dist(orjson)
Requires: python3-psutil Requires: python3dist(psutil)
Requires: python3-pyside6 Requires: python3dist(pyside6)
Requires: python3-pyudev Requires: python3dist(pyudev)
Requires: python3-requests Requires: python3dist(requests)
Requires: python3-tqdm Requires: python3dist(tqdm)
Requires: python3-vdf Requires: python3dist(vdf)
Requires: python3-pefile Requires: python3dist(pefile)
Requires: python3-pillow Requires: python3dist(pillow)
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils

View File

@ -25,19 +25,19 @@ BuildRequires: git
%package -n python3-%{pypi_name} %package -n python3-%{pypi_name}
Summary: %{summary} Summary: %{summary}
%{?python_provide:%python_provide python3-%{pypi_name}} %{?python_provide:%python_provide python3-%{pypi_name}}
Requires: python3-babel Requires: python3dist(babel)
Requires: python3-evdev Requires: python3dist(evdev)
Requires: python3-icoextract Requires: python3dist(icoextract)
Requires: python3-numpy Requires: python3dist(numpy)
Requires: python3-orjson Requires: python3dist(orjson)
Requires: python3-psutil Requires: python3dist(psutil)
Requires: python3-pyside6 Requires: python3dist(pyside6)
Requires: python3-pyudev Requires: python3dist(pyudev)
Requires: python3-requests Requires: python3dist(requests)
Requires: python3-tqdm Requires: python3dist(tqdm)
Requires: python3-vdf Requires: python3dist(vdf)
Requires: python3-pefile Requires: python3dist(pefile)
Requires: python3-pillow Requires: python3dist(pillow)
Requires: perl-Image-ExifTool Requires: perl-Image-ExifTool
Requires: xdg-utils Requires: xdg-utils

View File

@ -29,12 +29,14 @@ def main():
else: else:
logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}") logger.error(f"Qt translations for {system_locale.name()} not found in {translations_path}")
# Парсинг аргументов командной строки
args = parse_args() args = parse_args()
window = MainWindow() window = MainWindow()
# Обработка флага --fullscreen
if args.fullscreen: if args.fullscreen:
logger.info("Launching in fullscreen mode due to --fullscreen flag") logger.info("Запуск в полноэкранном режиме по флагу --fullscreen")
save_fullscreen_config(True) save_fullscreen_config(True)
window.showFullScreen() window.showFullScreen()
@ -45,29 +47,13 @@ def main():
def recreate_tray(): def recreate_tray():
nonlocal tray nonlocal tray
if tray: tray.hide_tray()
logger.debug("Recreating system tray")
tray.cleanup()
tray = None
current_theme = read_theme_from_config() current_theme = read_theme_from_config()
tray = SystemTray(app, current_theme) tray = SystemTray(app, current_theme)
# Ensure window is not None before connecting signals tray.show_action.triggered.connect(window.show)
if window: tray.hide_action.triggered.connect(window.hide)
tray.show_action.triggered.connect(window.show)
tray.hide_action.triggered.connect(window.hide)
def cleanup_on_exit():
nonlocal tray, window
app.aboutToQuit.disconnect()
if tray:
tray.cleanup()
tray = None
if window:
window.close()
app.quit()
window.settings_saved.connect(recreate_tray) window.settings_saved.connect(recreate_tray)
app.aboutToQuit.connect(cleanup_on_exit)
window.show() window.show()

View File

@ -12,36 +12,10 @@ from collections.abc import Callable
from portprotonqt.localization import get_egs_language, _ from portprotonqt.localization import get_egs_language, _
from portprotonqt.logger import get_logger from portprotonqt.logger import get_logger
from portprotonqt.image_utils import load_pixmap_async from portprotonqt.image_utils import load_pixmap_async
from portprotonqt.time_utils import parse_playtime_file, format_playtime, get_last_launch, get_last_launch_timestamp
from portprotonqt.config_utils import get_portproton_location
from portprotonqt.steam_api import get_weanticheatyet_status_async
from PySide6.QtGui import QPixmap from PySide6.QtGui import QPixmap
logger = get_logger(__name__) logger = get_logger(__name__)
def get_egs_executable(app_name: str, legendary_config_path: str) -> str | None:
"""Получает путь к исполняемому файлу EGS-игры из installed.json с использованием orjson."""
installed_json_path = os.path.join(legendary_config_path, "installed.json")
try:
with open(installed_json_path, "rb") as f:
installed_data = orjson.loads(f.read())
if app_name in installed_data:
install_path = installed_data[app_name].get("install_path", "").decode('utf-8') if isinstance(installed_data[app_name].get("install_path"), bytes) else installed_data[app_name].get("install_path", "")
executable = installed_data[app_name].get("executable", "").decode('utf-8') if isinstance(installed_data[app_name].get("executable"), bytes) else installed_data[app_name].get("executable", "")
if install_path and executable:
return os.path.join(install_path, executable)
return None
except FileNotFoundError:
logger.error(f"installed.json not found at {installed_json_path}")
return None
except orjson.JSONDecodeError:
logger.error(f"Invalid JSON in {installed_json_path}")
return None
except Exception as e:
logger.error(f"Error reading installed.json: {e}")
return None
def get_cache_dir() -> Path: def get_cache_dir() -> Path:
"""Returns the path to the cache directory, creating it if necessary.""" """Returns the path to the cache directory, creating it if necessary."""
xdg_cache_home = os.getenv( xdg_cache_home = os.getenv(
@ -307,7 +281,6 @@ def get_egs_game_description_async(
thread = threading.Thread(target=fetch_description, daemon=True) thread = threading.Thread(target=fetch_description, daemon=True)
thread.start() thread.start()
def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]): def run_legendary_list_async(legendary_path: str, callback: Callable[[list | None], None]):
""" """
Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback. Асинхронно выполняет команду 'legendary list --json' и возвращает результат через callback.
@ -353,7 +326,6 @@ def run_legendary_list_async(legendary_path: str, callback: Callable[[list | Non
def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]): def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]], None], downloader, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
""" """
Асинхронно загружает Epic Games Store игры с использованием legendary CLI. Асинхронно загружает Epic Games Store игры с использованием legendary CLI.
Читает статистику времени игры и последнего запуска из файла statistics.
""" """
logger.debug("Starting to load Epic Games Store games") logger.debug("Starting to load Epic Games Store games")
games: list[tuple] = [] games: list[tuple] = []
@ -362,14 +334,6 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
cache_file = cache_dir / "legendary_games.json" cache_file = cache_dir / "legendary_games.json"
cache_ttl = 3600 # Cache TTL in seconds (1 hour) cache_ttl = 3600 # Cache TTL in seconds (1 hour)
# Путь к файлу statistics
portproton_location = get_portproton_location()
if portproton_location is None:
logger.error("PortProton location is not set, cannot locate statistics file")
statistics_file = ""
else:
statistics_file = os.path.join(portproton_location, "data", "tmp", "statistics")
if not os.path.exists(legendary_path): if not os.path.exists(legendary_path):
logger.info("Legendary binary not found, downloading...") logger.info("Legendary binary not found, downloading...")
def on_legendary_downloaded(result): def on_legendary_downloaded(result):
@ -381,7 +345,7 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
logger.error(f"Failed to make legendary binary executable: {e}") logger.error(f"Failed to make legendary binary executable: {e}")
callback(games) # Return empty games list on failure callback(games) # Return empty games list on failure
return return
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file) _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message)
else: else:
logger.error("Failed to download legendary binary") logger.error("Failed to download legendary binary")
callback(games) # Return empty games list on failure callback(games) # Return empty games list on failure
@ -392,9 +356,9 @@ def load_egs_games_async(legendary_path: str, callback: Callable[[list[tuple]],
callback(games) callback(games)
return return
else: else:
_continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message, statistics_file) _continue_loading_egs_games(legendary_path, callback, metadata_dir, cache_dir, cache_file, cache_ttl, update_progress, update_status_message)
def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None], statistics_file: str): def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tuple]], None], metadata_dir: Path, cache_dir: Path, cache_file: Path, cache_ttl: int, update_progress: Callable[[int], None], update_status_message: Callable[[str, int], None]):
""" """
Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI. Продолжает процесс загрузки EGS игр, либо из кэша, либо через legendary CLI.
""" """
@ -446,33 +410,6 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
callback(final_games) callback(final_games)
return return
# Получаем путь к .exe для извлечения имени
game_exe = get_egs_executable(app_name, os.path.dirname(legendary_path))
exe_name = ""
if game_exe:
exe_name = os.path.splitext(os.path.basename(game_exe))[0]
# Читаем статистику из файла statistics
playtime_seconds = 0
formatted_playtime = ""
last_launch = _("Never")
last_launch_timestamp = 0
if exe_name and os.path.exists(statistics_file):
try:
playtime_data = parse_playtime_file(statistics_file)
matching_key = next(
(key for key in playtime_data if os.path.basename(key).split('.')[0] == exe_name),
None
)
if matching_key:
playtime_seconds = playtime_data[matching_key]
formatted_playtime = format_playtime(playtime_seconds)
except Exception as e:
logger.error(f"Failed to parse playtime data for {app_name}: {e}")
if exe_name:
last_launch = get_last_launch(exe_name) or _("Never")
last_launch_timestamp = get_last_launch_timestamp(exe_name)
metadata_file = metadata_dir / f"{app_name}.json" metadata_file = metadata_dir / f"{app_name}.json"
cover_url = "" cover_url = ""
try: try:
@ -493,6 +430,7 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
final_description = api_description or _("No description available") final_description = api_description or _("No description available")
def on_cover_loaded(pixmap: QPixmap): def on_cover_loaded(pixmap: QPixmap):
from portprotonqt.steam_api import get_weanticheatyet_status_async
def on_anticheat_status(status: str): def on_anticheat_status(status: str):
nonlocal pending_images nonlocal pending_images
with results_lock: with results_lock:
@ -503,12 +441,12 @@ def _continue_loading_egs_games(legendary_path: str, callback: Callable[[list[tu
app_name, app_name,
f"legendary:launch:{app_name}", f"legendary:launch:{app_name}",
"", "",
last_launch, # Время последнего запуска _("Never"),
formatted_playtime, # Форматированное время игры "",
"", "",
status or "", status or "",
last_launch_timestamp, # Временная метка последнего запуска 0,
playtime_seconds, # Время игры в секундах 0,
"epic" "epic"
) )
pending_images -= 1 pending_images -= 1

View File

@ -25,7 +25,6 @@ class GameCard(QFrame):
addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path addToSteamRequested = Signal(str, str, str) # name, exec_line, cover_path
removeFromSteamRequested = Signal(str, str) # name, exec_line removeFromSteamRequested = Signal(str, str) # name, exec_line
openGameFolderRequested = Signal(str, str) # name, exec_line openGameFolderRequested = Signal(str, str) # name, exec_line
hoverChanged = Signal(str, bool)
def __init__(self, name, description, cover_path, appid, controller_support, exec_line, def __init__(self, name, description, cover_path, appid, controller_support, exec_line,
last_launch, formatted_playtime, protondb_tier, anticheat_status, last_launch_ts, playtime_seconds, game_source, last_launch, formatted_playtime, protondb_tier, anticheat_status, last_launch_ts, playtime_seconds, game_source,
@ -477,7 +476,6 @@ class GameCard(QFrame):
def enterEvent(self, event): def enterEvent(self, event):
self._hovered = True self._hovered = True
self.hoverChanged.emit(self.name, True)
self.thickness_anim.stop() self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
@ -502,21 +500,22 @@ class GameCard(QFrame):
def leaveEvent(self, event): def leaveEvent(self, event):
self._hovered = False self._hovered = False
self.hoverChanged.emit(self.name, False) if not self._focused: # Сохраняем анимацию, если есть фокус
if not self._focused:
if self.gradient_anim: if self.gradient_anim:
self.gradient_anim.stop() self.gradient_anim.stop()
self.gradient_anim = None self.gradient_anim = None
if self.pulse_anim: self.thickness_anim.stop()
self.pulse_anim.stop()
self.pulse_anim = None
if self.thickness_anim:
self.thickness_anim.stop()
if self._isPulseAnimationConnected: if self._isPulseAnimationConnected:
self.thickness_anim.finished.disconnect(self.startPulseAnimation) self.thickness_anim.finished.disconnect(self.startPulseAnimation)
self._isPulseAnimationConnected = False self._isPulseAnimationConnected = False
self.setBorderWidth(2) if self.pulse_anim:
self.update() self.pulse_anim.stop()
self.pulse_anim = None
self.thickness_anim.setEasingCurve(QEasingCurve(QEasingCurve.Type.InBack))
self.thickness_anim.setStartValue(self._borderWidth)
self.thickness_anim.setEndValue(2)
self.thickness_anim.start()
super().leaveEvent(event) super().leaveEvent(event)
def focusInEvent(self, event): def focusInEvent(self, event):

View File

@ -17,7 +17,7 @@ from portprotonqt.system_overlay import SystemOverlay
from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel from portprotonqt.image_utils import load_pixmap_async, round_corners, ImageCarousel
from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games from portprotonqt.steam_api import get_steam_game_info_async, get_full_steam_game_info_async, get_steam_installed_games
from portprotonqt.egs_api import load_egs_games_async, get_egs_executable from portprotonqt.egs_api import load_egs_games_async
from portprotonqt.theme_manager import ThemeManager, load_theme_screenshots, load_logo from portprotonqt.theme_manager import ThemeManager, load_theme_screenshots, load_logo
from portprotonqt.time_utils import save_last_launch, get_last_launch, parse_playtime_file, format_playtime, get_last_launch_timestamp, format_last_launch from portprotonqt.time_utils import save_last_launch, get_last_launch, parse_playtime_file, format_playtime, get_last_launch_timestamp, format_last_launch
from portprotonqt.config_utils import ( from portprotonqt.config_utils import (
@ -65,7 +65,6 @@ class MainWindow(QMainWindow):
self.games_load_timer.timeout.connect(self.finalize_game_loading) self.games_load_timer.timeout.connect(self.finalize_game_loading)
self.games_loaded.connect(self.on_games_loaded) self.games_loaded.connect(self.on_games_loaded)
self.current_add_game_dialog = None self.current_add_game_dialog = None
self.current_hovered_card = None
# Добавляем таймер для дебаунсинга сохранения настроек # Добавляем таймер для дебаунсинга сохранения настроек
self.settingsDebounceTimer = QTimer(self) self.settingsDebounceTimer = QTimer(self)
@ -242,32 +241,6 @@ class MainWindow(QMainWindow):
self.updateGameGrid() self.updateGameGrid()
self.progress_bar.setVisible(False) self.progress_bar.setVisible(False)
def _on_card_hovered(self, game_name: str, is_hovered: bool):
"""Обработчик сигнала hoverChanged от GameCard."""
card_key = None
# Находим ключ карточки по имени игры
for key, card in self.game_card_cache.items():
if card.name == game_name:
card_key = key
break
if not card_key:
return
card = self.game_card_cache[card_key]
if is_hovered:
# Если мышь наведена на карточку
if self.current_hovered_card and self.current_hovered_card != card:
# Сбрасываем предыдущую выделенную карточку
self.current_hovered_card._hovered = False
self.current_hovered_card.leaveEvent(None) # Принудительно вызываем leaveEvent
self.current_hovered_card = card
else:
# Если мышь покинула карточку
if self.current_hovered_card == card:
self.current_hovered_card = None
def loadGames(self): def loadGames(self):
display_filter = read_display_filter() display_filter = read_display_filter()
favorites = read_favorites() favorites = read_favorites()
@ -720,7 +693,6 @@ class MainWindow(QMainWindow):
card_width=self.card_width, card_width=self.card_width,
context_menu_manager=self.context_menu_manager context_menu_manager=self.context_menu_manager
) )
card.hoverChanged.connect(self._on_card_hovered)
# Подключаем сигналы контекстного меню # Подключаем сигналы контекстного меню
card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut) card.editShortcutRequested.connect(self.context_menu_manager.edit_game_shortcut)
card.deleteGameRequested.connect(self.context_menu_manager.delete_game) card.deleteGameRequested.connect(self.context_menu_manager.delete_game)
@ -1863,15 +1835,26 @@ class MainWindow(QMainWindow):
# Обработка EGS-игр # Обработка EGS-игр
if exec_line.startswith("legendary:launch:"): if exec_line.startswith("legendary:launch:"):
# Извлекаем app_name из exec_line
app_name = exec_line.split("legendary:launch:")[1] app_name = exec_line.split("legendary:launch:")[1]
legendary_path = self.legendary_path # Путь к legendary
# Получаем путь к .exe из installed.json # Формируем переменные окружения
game_exe = get_egs_executable(app_name, self.legendary_config_path) env_vars = os.environ.copy()
if not game_exe or not os.path.exists(game_exe): env_vars['START_FROM_STEAM'] = '1'
QMessageBox.warning(self, _("Error"), _("Executable not found for EGS game: {0}").format(app_name)) env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
return
current_exe = os.path.basename(game_exe) wrapper = "flatpak run ru.linux_gaming.PortProton"
if self.portproton_location is not None and ".var" not in self.portproton_location:
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh")
wrapper = start_sh
# Формируем команду
cmd = [
legendary_path, "launch", app_name, "--no-wine", "--wrapper", wrapper
]
current_exe = os.path.basename(legendary_path)
if self.game_processes and self.target_exe is not None and self.target_exe != current_exe: if self.game_processes and self.target_exe is not None and self.target_exe != current_exe:
QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running")) QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running"))
return return
@ -1880,82 +1863,28 @@ class MainWindow(QMainWindow):
update_button = button if button is not None else self.current_play_button update_button = button if button is not None else self.current_play_button
self.current_running_button = update_button self.current_running_button = update_button
self.target_exe = current_exe self.target_exe = current_exe
exe_name = os.path.splitext(current_exe)[0] exe_name = app_name # Используем app_name для EGS-игр
# Проверяем, запущена ли игра # Запускаем процесс
if self.game_processes and self.target_exe == current_exe: try:
# Останавливаем игру process = subprocess.Popen(cmd, env=env_vars, shell=False, preexec_fn=os.setsid)
if hasattr(self, 'input_manager'): self.game_processes.append(process)
self.input_manager.enable_gamepad_handling() save_last_launch(exe_name, datetime.now())
for proc in self.game_processes:
try:
parent = psutil.Process(proc.pid)
children = parent.children(recursive=True)
for child in children:
try:
child.terminate()
except psutil.NoSuchProcess:
pass
psutil.wait_procs(children, timeout=5)
for child in children:
if child.is_running():
child.kill()
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except psutil.NoSuchProcess:
pass
self.game_processes = []
if update_button: if update_button:
update_button.setText(_("Play")) update_button.setText(_("Launching"))
icon = self.theme_manager.get_icon("play") icon = self.theme_manager.get_icon("stop")
if isinstance(icon, str): if isinstance(icon, str):
icon = QIcon(icon) icon = QIcon(icon)
elif icon is None: elif icon is None:
icon = QIcon() icon = QIcon()
update_button.setIcon(icon) update_button.setIcon(icon)
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer is not None:
self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None
self.current_running_button = None
self.target_exe = None
self._gameLaunched = False
else:
# Запускаем игру через PortProton
env_vars = os.environ.copy()
env_vars['START_FROM_STEAM'] = '1'
env_vars['LEGENDARY_CONFIG_PATH'] = self.legendary_config_path
wrapper = "flatpak run ru.linux_gaming.PortProton" self.checkProcessTimer = QTimer(self)
if self.portproton_location is not None and ".var" not in self.portproton_location: self.checkProcessTimer.timeout.connect(self.checkTargetExe)
start_sh = os.path.join(self.portproton_location, "data", "scripts", "start.sh") self.checkProcessTimer.start(500)
wrapper = start_sh except Exception as e:
logger.error(f"Failed to launch EGS game {app_name}: {e}")
cmd = [wrapper, game_exe] QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
try:
process = subprocess.Popen(cmd, env=env_vars, shell=False, preexec_fn=os.setsid)
self.game_processes.append(process)
save_last_launch(exe_name, datetime.now())
if update_button:
update_button.setText(_("Launching"))
icon = self.theme_manager.get_icon("stop")
if isinstance(icon, str):
icon = QIcon(icon)
elif icon is None:
icon = QIcon()
update_button.setIcon(icon)
# Delay disabling gamepad handling
if hasattr(self, 'input_manager'):
QTimer.singleShot(200, self.input_manager.disable_gamepad_handling)
self.checkProcessTimer = QTimer(self)
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
self.checkProcessTimer.start(500)
except Exception as e:
logger.error(f"Failed to launch EGS game {app_name}: {e}")
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
return return
# Обработка PortProton-игр # Обработка PortProton-игр
@ -2067,46 +1996,14 @@ class MainWindow(QMainWindow):
QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e))) QMessageBox.warning(self, _("Error"), _("Failed to launch game: {0}").format(str(e)))
def closeEvent(self, event): def closeEvent(self, event):
"""Завершает все дочерние процессы и сохраняет настройки при закрытии окна."""
# Завершаем все игровые процессы
for proc in self.game_processes: for proc in self.game_processes:
try: 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) os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except (psutil.NoSuchProcess, ProcessLookupError) as e: except ProcessLookupError:
logger.debug(f"Process {proc.pid} already terminated: {e}") pass # процесс уже завершился
self.game_processes = [] # Очищаем список процессов
# Сохраняем настройки окна
if not read_fullscreen_config(): if not read_fullscreen_config():
logger.debug(f"Saving window geometry: {self.width()}x{self.height()}")
save_window_geometry(self.width(), self.height()) save_window_geometry(self.width(), self.height())
save_card_size(self.card_width) save_card_size(self.card_width)
# Очищаем таймеры и другие ресурсы
if hasattr(self, 'games_load_timer') and self.games_load_timer.isActive():
self.games_load_timer.stop()
if hasattr(self, 'settingsDebounceTimer') and self.settingsDebounceTimer.isActive():
self.settingsDebounceTimer.stop()
if hasattr(self, 'searchDebounceTimer') and self.searchDebounceTimer.isActive():
self.searchDebounceTimer.stop()
if hasattr(self, 'checkProcessTimer') and self.checkProcessTimer and self.checkProcessTimer.isActive():
self.checkProcessTimer.stop()
self.checkProcessTimer.deleteLater()
self.checkProcessTimer = None
event.accept() event.accept()

View File

@ -7,13 +7,12 @@ from portprotonqt.config_utils import read_theme_from_config
class SystemTray: class SystemTray:
def __init__(self, app, theme=None): def __init__(self, app, theme=None):
self.app = app
self.theme_manager = ThemeManager() self.theme_manager = ThemeManager()
self.theme = theme if theme is not None else default_styles self.theme = theme if theme is not None else default_styles
self.current_theme_name = read_theme_from_config() self.current_theme_name = read_theme_from_config()
self.tray = QSystemTrayIcon() self.tray = QSystemTrayIcon()
self.tray.setIcon(cast(QIcon, self.theme_manager.get_icon("ppqt-tray", self.current_theme_name))) self.tray.setIcon(cast(QIcon, self.theme_manager.get_icon("ppqt-tray", self.current_theme_name)))
self.tray.setToolTip("PortProtonQt") self.tray.setToolTip("PortProton QT")
self.tray.setVisible(True) self.tray.setVisible(True)
# Создаём меню # Создаём меню
@ -33,17 +32,4 @@ class SystemTray:
def hide_tray(self): def hide_tray(self):
"""Скрыть иконку трея""" """Скрыть иконку трея"""
if self.tray: self.tray.hide()
self.tray.setVisible(False)
if self.menu:
self.menu.deleteLater()
self.menu = None
def cleanup(self):
"""Очистка ресурсов трея"""
if self.tray:
self.tray.setVisible(False)
self.tray = None
if self.menu:
self.menu.deleteLater()
self.menu = None