Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
62b8da2dc4
|
|||
b77609cb5f
|
|||
56b105d7b4
|
|||
14687d12ca
|
@ -145,13 +145,20 @@ jobs:
|
|||||||
- name: Install required dependencies
|
- name: Install required dependencies
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install -y original-awk
|
sudo apt install -y original-awk unzip
|
||||||
|
|
||||||
- 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: |
|
||||||
@ -163,7 +170,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
body_path: changelog.txt
|
body_path: changelog.txt
|
||||||
token: ${{ env.GITEA_TOKEN }}
|
token: ${{ env.GITEA_TOKEN }}
|
||||||
tag_name: ${{ env.VERSION }}
|
tag_name: v${{ env.VERSION }}
|
||||||
prerelease: true
|
prerelease: true
|
||||||
files: release/**/*
|
files: release/**/*
|
||||||
sha256sum: true
|
sha256sum: true
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
- Исправлены ошибки при подключении геймпада
|
- Исправлены ошибки при подключении геймпада
|
||||||
- Предотвращено многократное открытие диалога добавления игры через геймпад
|
- Предотвращено многократное открытие диалога добавления игры через геймпад
|
||||||
- Корректная обработка событий геймпада во время игры
|
- Корректная обработка событий геймпада во время игры
|
||||||
|
- Убийсво всех процессов "зомби" при закрытии программы
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -29,14 +29,12 @@ 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("Запуск в полноэкранном режиме по флагу --fullscreen")
|
logger.info("Launching in fullscreen mode due to --fullscreen flag")
|
||||||
save_fullscreen_config(True)
|
save_fullscreen_config(True)
|
||||||
window.showFullScreen()
|
window.showFullScreen()
|
||||||
|
|
||||||
@ -47,13 +45,29 @@ def main():
|
|||||||
|
|
||||||
def recreate_tray():
|
def recreate_tray():
|
||||||
nonlocal tray
|
nonlocal tray
|
||||||
tray.hide_tray()
|
if 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)
|
||||||
tray.show_action.triggered.connect(window.show)
|
# Ensure window is not None before connecting signals
|
||||||
tray.hide_action.triggered.connect(window.hide)
|
if window:
|
||||||
|
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()
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import shlex
|
|||||||
import glob
|
import glob
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog
|
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu
|
||||||
from PySide6.QtCore import QUrl, QPoint
|
from PySide6.QtCore import QUrl, QPoint
|
||||||
from PySide6.QtGui import QDesktopServices
|
from PySide6.QtGui import QDesktopServices
|
||||||
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
|
from portprotonqt.config_utils import parse_desktop_entry, read_favorites, save_favorites
|
||||||
@ -53,12 +53,6 @@ class ContextMenuManager:
|
|||||||
favorite_action = menu.addAction(_("Add to Favorites"))
|
favorite_action = menu.addAction(_("Add to Favorites"))
|
||||||
favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True))
|
favorite_action.triggered.connect(lambda: self.toggle_favorite(game_card, True))
|
||||||
|
|
||||||
if game_card.game_source == "epic":
|
|
||||||
import_action = menu.addAction(_("Import to Legendary"))
|
|
||||||
import_action.triggered.connect(
|
|
||||||
lambda: self.import_to_legendary(game_card.name, game_card.appid)
|
|
||||||
)
|
|
||||||
|
|
||||||
if game_card.game_source not in ("steam", "epic"):
|
if game_card.game_source not in ("steam", "epic"):
|
||||||
desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip()
|
desktop_dir = subprocess.check_output(['xdg-user-dir', 'DESKTOP']).decode('utf-8').strip()
|
||||||
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
desktop_path = os.path.join(desktop_dir, f"{game_card.name}.desktop")
|
||||||
@ -98,75 +92,6 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
menu.exec(game_card.mapToGlobal(pos))
|
menu.exec(game_card.mapToGlobal(pos))
|
||||||
|
|
||||||
def import_to_legendary(self, game_name, app_name):
|
|
||||||
"""
|
|
||||||
Imports an installed Epic Games Store game to Legendary using the provided app_name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
game_name: The display name of the game.
|
|
||||||
app_name: The Legendary app_name (unique identifier for the game).
|
|
||||||
"""
|
|
||||||
if not self._check_portproton():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Открываем диалог для выбора папки с установленной игрой
|
|
||||||
folder_path = QFileDialog.getExistingDirectory(
|
|
||||||
self.parent,
|
|
||||||
_("Select Game Installation Folder"),
|
|
||||||
os.path.expanduser("~")
|
|
||||||
)
|
|
||||||
if not folder_path:
|
|
||||||
self.parent.statusBar().showMessage(_("No folder selected"), 3000)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Путь к legendary
|
|
||||||
legendary_path = os.path.join(
|
|
||||||
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
|
||||||
"PortProtonQt", "legendary_cache", "legendary"
|
|
||||||
)
|
|
||||||
if not os.path.exists(legendary_path):
|
|
||||||
QMessageBox.warning(
|
|
||||||
self.parent,
|
|
||||||
_("Error"),
|
|
||||||
_("Legendary executable not found at {0}").format(legendary_path)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Формируем команду для импорта
|
|
||||||
cmd = [legendary_path, "import", app_name, folder_path]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Выполняем команду legendary import
|
|
||||||
subprocess.run(
|
|
||||||
cmd,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
self.parent.statusBar().showMessage(
|
|
||||||
_("Successfully imported '{0}' to Legendary").format(game_name), 3000
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self.parent,
|
|
||||||
_("Error"),
|
|
||||||
_("Failed to import '{0}' to Legendary: {1}").format(game_name, e.stderr)
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self.parent,
|
|
||||||
_("Error"),
|
|
||||||
_("Legendary executable not found")
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self.parent,
|
|
||||||
_("Error"),
|
|
||||||
_("Unexpected error during import: {0}").format(str(e))
|
|
||||||
)
|
|
||||||
|
|
||||||
def toggle_favorite(self, game_card, add: bool):
|
def toggle_favorite(self, game_card, add: bool):
|
||||||
"""
|
"""
|
||||||
Toggle the favorite status of a game and update its icon.
|
Toggle the favorite status of a game and update its icon.
|
||||||
|
@ -169,7 +169,7 @@ class GameCard(QFrame):
|
|||||||
self.steamLabel.setVisible(self.steam_visible)
|
self.steamLabel.setVisible(self.steam_visible)
|
||||||
|
|
||||||
# Epic Games Store бейдж
|
# Epic Games Store бейдж
|
||||||
egs_icon = self.theme_manager.get_icon("epic_games")
|
egs_icon = self.theme_manager.get_icon("steam")
|
||||||
self.egsLabel = ClickableLabel(
|
self.egsLabel = ClickableLabel(
|
||||||
"Epic Games",
|
"Epic Games",
|
||||||
icon=egs_icon,
|
icon=egs_icon,
|
||||||
|
@ -261,25 +261,19 @@ class MainWindow(QMainWindow):
|
|||||||
self.update_status_message.emit
|
self.update_status_message.emit
|
||||||
)
|
)
|
||||||
elif display_filter == "favorites":
|
elif display_filter == "favorites":
|
||||||
def on_all_games(portproton_games, steam_games, epic_games):
|
def on_all_games(portproton_games, steam_games):
|
||||||
games = [game for game in portproton_games + steam_games + epic_games if game[0] in favorites]
|
games = [game for game in portproton_games + steam_games if game[0] in favorites]
|
||||||
self.games_loaded.emit(games)
|
self.games_loaded.emit(games)
|
||||||
self._load_portproton_games_async(
|
self._load_portproton_games_async(
|
||||||
lambda pg: self._load_steam_games_async(
|
lambda pg: self._load_steam_games_async(
|
||||||
lambda sg: load_egs_games_async(
|
lambda sg: on_all_games(pg, sg)
|
||||||
self.legendary_path,
|
|
||||||
lambda eg: on_all_games(pg, sg, eg),
|
|
||||||
self.downloader,
|
|
||||||
self.update_progress.emit,
|
|
||||||
self.update_status_message.emit
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
def on_all_games(portproton_games, steam_games, epic_games):
|
def on_all_games(portproton_games, steam_games):
|
||||||
seen = set()
|
seen = set()
|
||||||
games = []
|
games = []
|
||||||
for game in portproton_games + steam_games + epic_games:
|
for game in portproton_games + steam_games:
|
||||||
# Уникальный ключ: имя + exec_line
|
# Уникальный ключ: имя + exec_line
|
||||||
key = (game[0], game[4])
|
key = (game[0], game[4])
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
@ -288,13 +282,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.games_loaded.emit(games)
|
self.games_loaded.emit(games)
|
||||||
self._load_portproton_games_async(
|
self._load_portproton_games_async(
|
||||||
lambda pg: self._load_steam_games_async(
|
lambda pg: self._load_steam_games_async(
|
||||||
lambda sg: load_egs_games_async(
|
lambda sg: on_all_games(pg, sg)
|
||||||
self.legendary_path,
|
|
||||||
lambda eg: on_all_games(pg, sg, eg),
|
|
||||||
self.downloader,
|
|
||||||
self.update_progress.emit,
|
|
||||||
self.update_status_message.emit
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
@ -941,7 +929,7 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# 3. Games display_filter
|
# 3. Games display_filter
|
||||||
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
|
self.filter_keys = ["all", "steam", "portproton", "favorites", "epic"]
|
||||||
self.filter_labels = [_("all"), "steam", "portproton", _("favorites"), "epic games store"]
|
self.filter_labels = [_("all"), "steam", "portproton", _("favorites")]
|
||||||
self.gamesDisplayCombo = QComboBox()
|
self.gamesDisplayCombo = QComboBox()
|
||||||
self.gamesDisplayCombo.addItems(self.filter_labels)
|
self.gamesDisplayCombo.addItems(self.filter_labels)
|
||||||
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
self.gamesDisplayCombo.setStyleSheet(self.theme.SETTINGS_COMBO_STYLE)
|
||||||
@ -1023,37 +1011,6 @@ class MainWindow(QMainWindow):
|
|||||||
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
self.gamepadRumbleCheckBox.setChecked(current_rumble_state)
|
||||||
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
formLayout.addRow(self.gamepadRumbleTitle, self.gamepadRumbleCheckBox)
|
||||||
|
|
||||||
# 8. Legendary Authentication
|
|
||||||
self.legendaryAuthButton = AutoSizeButton(
|
|
||||||
_("Open Legendary Login"),
|
|
||||||
icon=self.theme_manager.get_icon("login")
|
|
||||||
)
|
|
||||||
self.legendaryAuthButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
|
||||||
self.legendaryAuthButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
||||||
self.legendaryAuthButton.clicked.connect(self.openLegendaryLogin)
|
|
||||||
self.legendaryAuthTitle = QLabel(_("Legendary Authentication:"))
|
|
||||||
self.legendaryAuthTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
|
||||||
self.legendaryAuthTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
||||||
formLayout.addRow(self.legendaryAuthTitle, self.legendaryAuthButton)
|
|
||||||
|
|
||||||
self.legendaryCodeEdit = QLineEdit()
|
|
||||||
self.legendaryCodeEdit.setPlaceholderText(_("Enter Legendary Authorization Code"))
|
|
||||||
self.legendaryCodeEdit.setStyleSheet(self.theme.PROXY_INPUT_STYLE)
|
|
||||||
self.legendaryCodeEdit.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
||||||
self.legendaryCodeTitle = QLabel(_("Authorization Code:"))
|
|
||||||
self.legendaryCodeTitle.setStyleSheet(self.theme.PARAMS_TITLE_STYLE)
|
|
||||||
self.legendaryCodeTitle.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
||||||
formLayout.addRow(self.legendaryCodeTitle, self.legendaryCodeEdit)
|
|
||||||
|
|
||||||
self.submitCodeButton = AutoSizeButton(
|
|
||||||
_("Submit Code"),
|
|
||||||
icon=self.theme_manager.get_icon("save")
|
|
||||||
)
|
|
||||||
self.submitCodeButton.setStyleSheet(self.theme.ACTION_BUTTON_STYLE)
|
|
||||||
self.submitCodeButton.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
||||||
self.submitCodeButton.clicked.connect(self.submitLegendaryCode)
|
|
||||||
formLayout.addRow(QLabel(""), self.submitCodeButton)
|
|
||||||
|
|
||||||
layout.addLayout(formLayout)
|
layout.addLayout(formLayout)
|
||||||
|
|
||||||
# Кнопки
|
# Кнопки
|
||||||
@ -1104,37 +1061,6 @@ class MainWindow(QMainWindow):
|
|||||||
logger.error(f"Failed to open Legendary login page: {e}")
|
logger.error(f"Failed to open Legendary login page: {e}")
|
||||||
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
|
self.statusBar().showMessage(_("Failed to open Legendary login page"), 3000)
|
||||||
|
|
||||||
def submitLegendaryCode(self):
|
|
||||||
"""Submits the Legendary authorization code using the legendary CLI."""
|
|
||||||
auth_code = self.legendaryCodeEdit.text().strip()
|
|
||||||
if not auth_code:
|
|
||||||
QMessageBox.warning(self, _("Error"), _("Please enter an authorization code"))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Execute legendary auth command
|
|
||||||
result = subprocess.run(
|
|
||||||
[self.legendary_path, "auth", "--code", auth_code],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
logger.info("Legendary authentication successful: %s", result.stdout)
|
|
||||||
self.statusBar().showMessage(_("Successfully authenticated with Legendary"), 3000)
|
|
||||||
self.legendaryCodeEdit.clear()
|
|
||||||
# Reload Epic Games Store games after successful authentication
|
|
||||||
self.games = self.loadGames()
|
|
||||||
self.updateGameGrid()
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
logger.error("Legendary authentication failed: %s", e.stderr)
|
|
||||||
self.statusBar().showMessage(_("Legendary authentication failed: {0}").format(e.stderr), 5000)
|
|
||||||
except FileNotFoundError:
|
|
||||||
logger.error("Legendary executable not found at %s", self.legendary_path)
|
|
||||||
self.statusBar().showMessage(_("Legendary executable not found"), 5000)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Unexpected error during Legendary authentication: %s", str(e))
|
|
||||||
self.statusBar().showMessage(_("Unexpected error during authentication"), 5000)
|
|
||||||
|
|
||||||
def resetSettings(self):
|
def resetSettings(self):
|
||||||
"""Сбрасывает настройки и перезапускает приложение."""
|
"""Сбрасывает настройки и перезапускает приложение."""
|
||||||
reply = QMessageBox.question(
|
reply = QMessageBox.question(
|
||||||
@ -1530,7 +1456,7 @@ class MainWindow(QMainWindow):
|
|||||||
steamLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://steamcommunity.com/app/{appid}")))
|
steamLabel.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(f"https://steamcommunity.com/app/{appid}")))
|
||||||
|
|
||||||
# Epic Games Store бейдж
|
# Epic Games Store бейдж
|
||||||
egs_icon = self.theme_manager.get_icon("epic_games")
|
egs_icon = self.theme_manager.get_icon("steam")
|
||||||
egsLabel = ClickableLabel(
|
egsLabel = ClickableLabel(
|
||||||
"Epic Games",
|
"Epic Games",
|
||||||
icon=egs_icon,
|
icon=egs_icon,
|
||||||
@ -1827,67 +1753,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.target_exe = None
|
self.target_exe = None
|
||||||
|
|
||||||
def toggleGame(self, exec_line, button=None):
|
def toggleGame(self, exec_line, button=None):
|
||||||
# Обработка Steam-игр
|
|
||||||
if exec_line.startswith("steam://"):
|
if exec_line.startswith("steam://"):
|
||||||
url = QUrl(exec_line)
|
url = QUrl(exec_line)
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Обработка EGS-игр
|
|
||||||
if exec_line.startswith("legendary:launch:"):
|
|
||||||
# Извлекаем app_name из exec_line
|
|
||||||
app_name = exec_line.split("legendary:launch:")[1]
|
|
||||||
legendary_path = self.legendary_path # Путь к legendary
|
|
||||||
|
|
||||||
# Формируем переменные окружения
|
|
||||||
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"
|
|
||||||
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:
|
|
||||||
QMessageBox.warning(self, _("Error"), _("Cannot launch game while another game is running"))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Обновляем кнопку
|
|
||||||
update_button = button if button is not None else self.current_play_button
|
|
||||||
self.current_running_button = update_button
|
|
||||||
self.target_exe = current_exe
|
|
||||||
exe_name = app_name # Используем app_name для EGS-игр
|
|
||||||
|
|
||||||
# Запускаем процесс
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Обработка PortProton-игр
|
|
||||||
entry_exec_split = shlex.split(exec_line)
|
entry_exec_split = shlex.split(exec_line)
|
||||||
if entry_exec_split[0] == "env":
|
if entry_exec_split[0] == "env":
|
||||||
if len(entry_exec_split) < 3:
|
if len(entry_exec_split) < 3:
|
||||||
@ -1901,20 +1771,18 @@ class MainWindow(QMainWindow):
|
|||||||
file_to_check = entry_exec_split[3]
|
file_to_check = entry_exec_split[3]
|
||||||
else:
|
else:
|
||||||
file_to_check = entry_exec_split[0]
|
file_to_check = entry_exec_split[0]
|
||||||
|
|
||||||
if not os.path.exists(file_to_check):
|
if not os.path.exists(file_to_check):
|
||||||
QMessageBox.warning(self, _("Error"), _("File not found: {0}").format(file_to_check))
|
QMessageBox.warning(self, _("Error"), _("File not found: {0}").format(file_to_check))
|
||||||
return
|
return
|
||||||
|
|
||||||
current_exe = os.path.basename(file_to_check)
|
current_exe = os.path.basename(file_to_check)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
# Обновляем кнопку
|
|
||||||
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
|
||||||
|
|
||||||
# Если игра уже запущена для этого exe – останавливаем её
|
# Если игра уже запущена для этого exe – останавливаем её по нажатию кнопки
|
||||||
if self.game_processes and self.target_exe == current_exe:
|
if self.game_processes and self.target_exe == current_exe:
|
||||||
if hasattr(self, 'input_manager'):
|
if hasattr(self, 'input_manager'):
|
||||||
self.input_manager.enable_gamepad_handling()
|
self.input_manager.enable_gamepad_handling()
|
||||||
@ -1967,15 +1835,6 @@ class MainWindow(QMainWindow):
|
|||||||
env_vars['START_FROM_STEAM'] = '1'
|
env_vars['START_FROM_STEAM'] = '1'
|
||||||
elif entry_exec_split[0] == "flatpak":
|
elif entry_exec_split[0] == "flatpak":
|
||||||
env_vars['START_FROM_STEAM'] = '1'
|
env_vars['START_FROM_STEAM'] = '1'
|
||||||
return
|
|
||||||
|
|
||||||
# Запускаем игру
|
|
||||||
self.current_running_button = update_button
|
|
||||||
self.target_exe = current_exe
|
|
||||||
exe_name = os.path.splitext(current_exe)[0]
|
|
||||||
env_vars = os.environ.copy()
|
|
||||||
env_vars['START_FROM_STEAM'] = '1'
|
|
||||||
try:
|
|
||||||
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
process = subprocess.Popen(entry_exec_split, env=env_vars, shell=False, preexec_fn=os.setsid)
|
||||||
self.game_processes.append(process)
|
self.game_processes.append(process)
|
||||||
save_last_launch(exe_name, datetime.now())
|
save_last_launch(exe_name, datetime.now())
|
||||||
@ -1991,19 +1850,48 @@ class MainWindow(QMainWindow):
|
|||||||
self.checkProcessTimer = QTimer(self)
|
self.checkProcessTimer = QTimer(self)
|
||||||
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
|
self.checkProcessTimer.timeout.connect(self.checkTargetExe)
|
||||||
self.checkProcessTimer.start(500)
|
self.checkProcessTimer.start(500)
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to launch game {exe_name}: {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 ProcessLookupError:
|
except (psutil.NoSuchProcess, ProcessLookupError) as e:
|
||||||
pass # процесс уже завершился
|
logger.debug(f"Process {proc.pid} already terminated: {e}")
|
||||||
|
|
||||||
|
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()
|
||||||
|
@ -7,12 +7,13 @@ 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("PortProton QT")
|
self.tray.setToolTip("PortProtonQt")
|
||||||
self.tray.setVisible(True)
|
self.tray.setVisible(True)
|
||||||
|
|
||||||
# Создаём меню
|
# Создаём меню
|
||||||
@ -32,4 +33,17 @@ class SystemTray:
|
|||||||
|
|
||||||
def hide_tray(self):
|
def hide_tray(self):
|
||||||
"""Скрыть иконку трея"""
|
"""Скрыть иконку трея"""
|
||||||
self.tray.hide()
|
if self.tray:
|
||||||
|
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
|
||||||
|
Reference in New Issue
Block a user