feat: update context menu for egs games
Signed-off-by: Boris Yumankulov <boria138@altlinux.org>
This commit is contained in:
@ -3,13 +3,26 @@ import shlex
|
|||||||
import glob
|
import glob
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import json
|
||||||
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog
|
from PySide6.QtWidgets import QMessageBox, QDialog, QMenu, QFileDialog
|
||||||
from PySide6.QtCore import QUrl, QPoint
|
from PySide6.QtCore import QUrl, QPoint, QObject, Signal, Qt
|
||||||
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
|
||||||
from portprotonqt.localization import _
|
from portprotonqt.localization import _
|
||||||
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
from portprotonqt.steam_api import is_game_in_steam, add_to_steam, remove_from_steam
|
||||||
from portprotonqt.dialogs import AddGameDialog
|
from portprotonqt.dialogs import AddGameDialog
|
||||||
|
from portprotonqt.egs_api import add_egs_to_steam, get_egs_executable
|
||||||
|
import vdf
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ContextMenuSignals(QObject):
|
||||||
|
"""Signals for thread-safe UI updates from worker threads."""
|
||||||
|
show_status_message = Signal(str, int)
|
||||||
|
show_warning_dialog = Signal(str, str)
|
||||||
|
|
||||||
class ContextMenuManager:
|
class ContextMenuManager:
|
||||||
"""Manages context menu actions for game management in PortProtonQt."""
|
"""Manages context menu actions for game management in PortProtonQt."""
|
||||||
@ -30,6 +43,56 @@ class ContextMenuManager:
|
|||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.load_games = load_games_callback
|
self.load_games = load_games_callback
|
||||||
self.update_game_grid = update_game_grid_callback
|
self.update_game_grid = update_game_grid_callback
|
||||||
|
self.legendary_path = os.path.join(
|
||||||
|
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
||||||
|
"PortProtonQt", "legendary_cache", "legendary"
|
||||||
|
)
|
||||||
|
self.legendary_config_path = os.path.join(
|
||||||
|
os.getenv("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
||||||
|
"PortProtonQt", "legendary_cache"
|
||||||
|
)
|
||||||
|
# Initialize signals for thread-safe UI updates
|
||||||
|
self.signals = ContextMenuSignals()
|
||||||
|
if self.parent.statusBar() is None:
|
||||||
|
logger.warning("Status bar is not initialized in MainWindow")
|
||||||
|
else:
|
||||||
|
self.signals.show_status_message.connect(
|
||||||
|
self.parent.statusBar().showMessage,
|
||||||
|
Qt.ConnectionType.QueuedConnection
|
||||||
|
)
|
||||||
|
logger.debug("Connected show_status_message signal to statusBar")
|
||||||
|
self.signals.show_warning_dialog.connect(
|
||||||
|
self._show_warning_dialog,
|
||||||
|
Qt.ConnectionType.QueuedConnection
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_warning_dialog(self, title: str, message: str):
|
||||||
|
"""Show a warning dialog in the main thread."""
|
||||||
|
logger.debug("Showing warning dialog: %s - %s", title, message)
|
||||||
|
QMessageBox.warning(self.parent, title, message)
|
||||||
|
|
||||||
|
def _is_egs_game_installed(self, app_name: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if an EGS game is installed by reading installed.json.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_name: The Legendary app_name (unique identifier for the game).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the game is installed, False otherwise.
|
||||||
|
"""
|
||||||
|
installed_json_path = os.path.join(self.legendary_config_path, "installed.json")
|
||||||
|
if not os.path.exists(installed_json_path):
|
||||||
|
logger.debug("installed.json not found at %s", installed_json_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(installed_json_path, encoding="utf-8") as f:
|
||||||
|
installed_games = json.load(f)
|
||||||
|
return app_name in installed_games
|
||||||
|
except (OSError, json.JSONDecodeError) as e:
|
||||||
|
logger.error("Failed to read installed.json: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
def show_context_menu(self, game_card, pos: QPoint):
|
def show_context_menu(self, game_card, pos: QPoint):
|
||||||
"""
|
"""
|
||||||
@ -39,7 +102,6 @@ class ContextMenuManager:
|
|||||||
game_card: The GameCard instance requesting the context menu.
|
game_card: The GameCard instance requesting the context menu.
|
||||||
pos: The position (in widget coordinates) where the menu should appear.
|
pos: The position (in widget coordinates) where the menu should appear.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
menu = QMenu(self.parent)
|
menu = QMenu(self.parent)
|
||||||
menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
|
menu.setStyleSheet(self.theme.CONTEXT_MENU_STYLE)
|
||||||
|
|
||||||
@ -54,9 +116,31 @@ class ContextMenuManager:
|
|||||||
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":
|
if game_card.game_source == "epic":
|
||||||
import_action = menu.addAction(_("Import to Legendary"))
|
# Always show Import to Legendary
|
||||||
import_action.triggered.connect(
|
import_action = menu.addAction(_("Import to Legendary"))
|
||||||
lambda: self.import_to_legendary(game_card.name, game_card.appid)
|
import_action.triggered.connect(
|
||||||
|
lambda: self.import_to_legendary(game_card.name, game_card.appid)
|
||||||
|
)
|
||||||
|
# Show other actions only if the game is installed
|
||||||
|
if self._is_egs_game_installed(game_card.appid):
|
||||||
|
uninstall_action = menu.addAction(_("Uninstall Game"))
|
||||||
|
uninstall_action.triggered.connect(
|
||||||
|
lambda: self.uninstall_egs_game(game_card.name, game_card.appid)
|
||||||
|
)
|
||||||
|
is_in_steam = is_game_in_steam(game_card.name)
|
||||||
|
if is_in_steam:
|
||||||
|
remove_steam_action = menu.addAction(_("Remove from Steam"))
|
||||||
|
remove_steam_action.triggered.connect(
|
||||||
|
lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
add_steam_action = menu.addAction(_("Add to Steam"))
|
||||||
|
add_steam_action.triggered.connect(
|
||||||
|
lambda: self.add_egs_to_steam(game_card.name, game_card.appid)
|
||||||
|
)
|
||||||
|
open_folder_action = menu.addAction(_("Open Game Folder"))
|
||||||
|
open_folder_action.triggered.connect(
|
||||||
|
lambda: self.open_egs_game_folder(game_card.appid)
|
||||||
)
|
)
|
||||||
|
|
||||||
if game_card.game_source not in ("steam", "epic"):
|
if game_card.game_source not in ("steam", "epic"):
|
||||||
@ -70,13 +154,17 @@ class ContextMenuManager:
|
|||||||
add_action.triggered.connect(lambda: self.add_to_desktop(game_card.name, game_card.exec_line))
|
add_action.triggered.connect(lambda: self.add_to_desktop(game_card.name, game_card.exec_line))
|
||||||
|
|
||||||
edit_action = menu.addAction(_("Edit Shortcut"))
|
edit_action = menu.addAction(_("Edit Shortcut"))
|
||||||
edit_action.triggered.connect(lambda: self.edit_game_shortcut(game_card.name, game_card.exec_line, game_card.cover_path))
|
edit_action.triggered.connect(
|
||||||
|
lambda: self.edit_game_shortcut(game_card.name, game_card.exec_line, game_card.cover_path)
|
||||||
|
)
|
||||||
|
|
||||||
delete_action = menu.addAction(_("Delete from PortProton"))
|
delete_action = menu.addAction(_("Delete from PortProton"))
|
||||||
delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line))
|
delete_action.triggered.connect(lambda: self.delete_game(game_card.name, game_card.exec_line))
|
||||||
|
|
||||||
open_folder_action = menu.addAction(_("Open Game Folder"))
|
open_folder_action = menu.addAction(_("Open Game Folder"))
|
||||||
open_folder_action.triggered.connect(lambda: self.open_game_folder(game_card.name, game_card.exec_line))
|
open_folder_action.triggered.connect(
|
||||||
|
lambda: self.open_game_folder(game_card.name, game_card.exec_line)
|
||||||
|
)
|
||||||
|
|
||||||
applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications")
|
applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications")
|
||||||
desktop_path = os.path.join(applications_dir, f"{game_card.name}.desktop")
|
desktop_path = os.path.join(applications_dir, f"{game_card.name}.desktop")
|
||||||
@ -87,20 +175,88 @@ class ContextMenuManager:
|
|||||||
add_action = menu.addAction(_("Add to Menu"))
|
add_action = menu.addAction(_("Add to Menu"))
|
||||||
add_action.triggered.connect(lambda: self.add_to_menu(game_card.name, game_card.exec_line))
|
add_action.triggered.connect(lambda: self.add_to_menu(game_card.name, game_card.exec_line))
|
||||||
|
|
||||||
# Add Steam-related actions
|
|
||||||
is_in_steam = is_game_in_steam(game_card.name)
|
is_in_steam = is_game_in_steam(game_card.name)
|
||||||
if is_in_steam:
|
if is_in_steam:
|
||||||
remove_steam_action = menu.addAction(_("Remove from Steam"))
|
remove_steam_action = menu.addAction(_("Remove from Steam"))
|
||||||
remove_steam_action.triggered.connect(lambda: self.remove_from_steam(game_card.name, game_card.exec_line))
|
remove_steam_action.triggered.connect(
|
||||||
|
lambda: self.remove_from_steam(game_card.name, game_card.exec_line, game_card.game_source)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
add_steam_action = menu.addAction(_("Add to Steam"))
|
add_steam_action = menu.addAction(_("Add to Steam"))
|
||||||
add_steam_action.triggered.connect(lambda: self.add_to_steam(game_card.name, game_card.exec_line, game_card.cover_path))
|
add_steam_action.triggered.connect(
|
||||||
|
lambda: self.add_to_steam(game_card.name, game_card.exec_line, game_card.cover_path)
|
||||||
|
)
|
||||||
|
|
||||||
menu.exec(game_card.mapToGlobal(pos))
|
menu.exec(game_card.mapToGlobal(pos))
|
||||||
|
|
||||||
def import_to_legendary(self, game_name, app_name):
|
def add_egs_to_steam(self, game_name: str, app_name: str):
|
||||||
"""
|
"""
|
||||||
Imports an installed Epic Games Store game to Legendary using the provided app_name.
|
Adds an EGS game to Steam using the egs_api.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if not os.path.exists(self.legendary_path):
|
||||||
|
self._show_warning_dialog(_("Error"), _("Legendary executable not found at {0}").format(self.legendary_path))
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_add_to_steam_result(result: tuple[bool, str]):
|
||||||
|
success, message = result
|
||||||
|
if success:
|
||||||
|
self.signals.show_status_message.emit(
|
||||||
|
_("The game was added successfully. Please restart Steam for changes to take effect."), 5000
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.signals.show_warning_dialog.emit(_("Error"), message)
|
||||||
|
|
||||||
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(_("Adding '{0}' to Steam...").format(game_name), 0)
|
||||||
|
logger.debug("Direct status message: Adding '%s' to Steam", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when adding '%s' to Steam", game_name)
|
||||||
|
add_egs_to_steam(app_name, game_name, self.legendary_path, on_add_to_steam_result)
|
||||||
|
|
||||||
|
def open_egs_game_folder(self, app_name: str):
|
||||||
|
"""
|
||||||
|
Opens the folder containing the EGS game's executable.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app_name: The Legendary app_name (unique identifier for the game).
|
||||||
|
"""
|
||||||
|
if not self._check_portproton():
|
||||||
|
return
|
||||||
|
|
||||||
|
exe_path = get_egs_executable(app_name, self.legendary_config_path)
|
||||||
|
if not exe_path or not os.path.exists(exe_path):
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Executable file not found for game: {0}").format(app_name)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
folder_path = os.path.dirname(os.path.abspath(exe_path))
|
||||||
|
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
|
||||||
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Opened folder for EGS game '{0}'").format(app_name), 3000
|
||||||
|
)
|
||||||
|
logger.debug("Direct status message: Opened folder for '%s'", app_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when opening folder for '%s'", app_name)
|
||||||
|
except Exception as e:
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to open game folder: {0}").format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
|
def import_to_legendary(self, game_name, app_name):
|
||||||
|
"""
|
||||||
|
Imports an installed Epic Games Store game to Legendary asynchronously.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
game_name: The display name of the game.
|
game_name: The display name of the game.
|
||||||
@ -109,63 +265,118 @@ class ContextMenuManager:
|
|||||||
if not self._check_portproton():
|
if not self._check_portproton():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Открываем диалог для выбора папки с установленной игрой
|
|
||||||
folder_path = QFileDialog.getExistingDirectory(
|
folder_path = QFileDialog.getExistingDirectory(
|
||||||
self.parent,
|
self.parent,
|
||||||
_("Select Game Installation Folder"),
|
_("Select Game Installation Folder"),
|
||||||
os.path.expanduser("~")
|
os.path.expanduser("~")
|
||||||
)
|
)
|
||||||
if not folder_path:
|
if not folder_path:
|
||||||
self.parent.statusBar().showMessage(_("No folder selected"), 3000)
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(_("No folder selected"), 3000)
|
||||||
|
logger.debug("Direct status message: No folder selected for '%s'", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when no folder selected for '%s'", game_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Путь к legendary
|
if not os.path.exists(self.legendary_path):
|
||||||
legendary_path = os.path.join(
|
self._show_warning_dialog(
|
||||||
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"),
|
_("Error"),
|
||||||
_("Legendary executable not found at {0}").format(legendary_path)
|
_("Legendary executable not found at {0}").format(self.legendary_path)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Формируем команду для импорта
|
def run_import():
|
||||||
cmd = [legendary_path, "import", app_name, folder_path]
|
cmd = [self.legendary_path, "import", app_name, folder_path]
|
||||||
|
|
||||||
try:
|
|
||||||
# Выполняем команду legendary import
|
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=True
|
check=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.parent.statusBar():
|
||||||
self.parent.statusBar().showMessage(
|
self.parent.statusBar().showMessage(
|
||||||
_("Successfully imported '{0}' to Legendary").format(game_name), 3000
|
_("Importing '{0}' to Legendary...").format(game_name), 0
|
||||||
)
|
)
|
||||||
|
logger.debug("Direct status message: Importing '%s' to Legendary", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when importing '%s'", game_name)
|
||||||
|
threading.Thread(target=run_import, daemon=True).start()
|
||||||
|
|
||||||
|
def uninstall_egs_game(self, game_name: str, app_name: str):
|
||||||
|
"""
|
||||||
|
Uninstalls an Epic Games Store game using Legendary asynchronously.
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
Args:
|
||||||
QMessageBox.warning(
|
game_name: The display name of the game.
|
||||||
self.parent,
|
app_name: The Legendary app_name (unique identifier for the game).
|
||||||
|
"""
|
||||||
|
if not self._check_portproton():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not os.path.exists(self.legendary_path):
|
||||||
|
self._show_warning_dialog(
|
||||||
_("Error"),
|
_("Error"),
|
||||||
_("Failed to import '{0}' to Legendary: {1}").format(game_name, e.stderr)
|
_("Legendary executable not found at {0}").format(self.legendary_path)
|
||||||
)
|
)
|
||||||
except FileNotFoundError:
|
return
|
||||||
QMessageBox.warning(
|
|
||||||
self.parent,
|
reply = QMessageBox.question(
|
||||||
_("Error"),
|
self.parent,
|
||||||
_("Legendary executable not found")
|
_("Confirm Uninstallation"),
|
||||||
)
|
_("Are you sure you want to uninstall '{0}'? This will remove the game files.").format(game_name),
|
||||||
except Exception as e:
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
QMessageBox.warning(
|
QMessageBox.StandardButton.No
|
||||||
self.parent,
|
)
|
||||||
_("Error"),
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
_("Unexpected error during import: {0}").format(str(e))
|
return
|
||||||
|
|
||||||
|
def run_uninstall():
|
||||||
|
cmd = [self.legendary_path, "uninstall", app_name, "--skip-uninstaller"]
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
|
env={"LEGENDARY_CONFIG_PATH": self.legendary_config_path}
|
||||||
|
)
|
||||||
|
self.signals.show_status_message.emit(
|
||||||
|
_("Successfully uninstalled '{0}'").format(game_name), 3000
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
self.signals.show_status_message.emit(
|
||||||
|
_("Failed to uninstall '{0}'").format(game_name), 3000
|
||||||
|
)
|
||||||
|
self.signals.show_warning_dialog.emit(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to uninstall '{0}': {1}").format(game_name, e.stderr)
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.signals.show_status_message.emit(
|
||||||
|
_("Legendary executable not found"), 3000
|
||||||
|
)
|
||||||
|
self.signals.show_warning_dialog.emit(
|
||||||
|
_("Error"),
|
||||||
|
_("Legendary executable not found")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.signals.show_status_message.emit(
|
||||||
|
_("Unexpected error during uninstall"), 3000
|
||||||
|
)
|
||||||
|
self.signals.show_warning_dialog.emit(
|
||||||
|
_("Error"),
|
||||||
|
_("Unexpected error during uninstall: {0}").format(str(e))
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Uninstalling '{0}'...").format(game_name), 0
|
||||||
)
|
)
|
||||||
|
logger.debug("Direct status message: Uninstalling '%s'", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when uninstalling '%s'", game_name)
|
||||||
|
threading.Thread(target=run_uninstall, daemon=True).start()
|
||||||
|
|
||||||
def toggle_favorite(self, game_card, add: bool):
|
def toggle_favorite(self, game_card, add: bool):
|
||||||
"""
|
"""
|
||||||
@ -179,18 +390,33 @@ class ContextMenuManager:
|
|||||||
if add and game_card.name not in favorites:
|
if add and game_card.name not in favorites:
|
||||||
favorites.append(game_card.name)
|
favorites.append(game_card.name)
|
||||||
game_card.is_favorite = True
|
game_card.is_favorite = True
|
||||||
self.parent.statusBar().showMessage(_("Added '{0}' to favorites").format(game_card.name), 3000)
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Added '{0}' to favorites").format(game_card.name), 3000
|
||||||
|
)
|
||||||
|
logger.debug("Direct status message: Added '%s' to favorites", game_card.name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when adding '%s' to favorites", game_card.name)
|
||||||
elif not add and game_card.name in favorites:
|
elif not add and game_card.name in favorites:
|
||||||
favorites.remove(game_card.name)
|
favorites.remove(game_card.name)
|
||||||
game_card.is_favorite = False
|
game_card.is_favorite = False
|
||||||
self.parent.statusBar().showMessage(_("Removed '{0}' from favorites").format(game_card.name), 3000)
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Removed '{0}' from favorites").format(game_card.name), 3000
|
||||||
|
)
|
||||||
|
logger.debug("Direct status message: Removed '%s' from favorites", game_card.name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when removing '%s' from favorites", game_card.name)
|
||||||
save_favorites(favorites)
|
save_favorites(favorites)
|
||||||
game_card.update_favorite_icon()
|
game_card.update_favorite_icon()
|
||||||
|
|
||||||
def _check_portproton(self):
|
def _check_portproton(self):
|
||||||
"""Check if PortProton is available."""
|
"""Check if PortProton is available."""
|
||||||
if self.portproton_location is None:
|
if self.portproton_location is None:
|
||||||
QMessageBox.warning(self.parent, _("Error"), _("PortProton is not found."))
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("PortProton is not found.")
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -214,33 +440,32 @@ class ContextMenuManager:
|
|||||||
if entry:
|
if entry:
|
||||||
exec_line = entry.get("Exec", entry.get("exec", "")).strip()
|
exec_line = entry.get("Exec", entry.get("exec", "")).strip()
|
||||||
if not exec_line:
|
if not exec_line:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("No executable command found in .desktop for game: {0}").format(game_name)
|
_("No executable command found in .desktop for game: {0}").format(game_name)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Failed to parse .desktop file for game: {0}").format(game_name)
|
_("Failed to parse .desktop file for game: {0}").format(game_name)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Error reading .desktop file: {0}").format(e)
|
_("Error reading .desktop file: {0}").format(e)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
# Fallback: Search all .desktop files
|
|
||||||
for file in glob.glob(os.path.join(self.portproton_location, "*.desktop")):
|
for file in glob.glob(os.path.join(self.portproton_location, "*.desktop")):
|
||||||
entry = parse_desktop_entry(file)
|
entry = parse_desktop_entry(file)
|
||||||
if entry:
|
if entry:
|
||||||
exec_line = entry.get("Exec", entry.get("exec", "")).strip()
|
exec_line = entry.get("Exec", entry.get("exec", "")).strip()
|
||||||
if exec_line:
|
if exec_line:
|
||||||
return exec_line
|
return exec_line
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_(".desktop file not found for game: {0}").format(game_name)
|
_(".desktop file not found for game: {0}").format(game_name)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
@ -251,8 +476,8 @@ class ContextMenuManager:
|
|||||||
try:
|
try:
|
||||||
entry_exec_split = shlex.split(exec_line)
|
entry_exec_split = shlex.split(exec_line)
|
||||||
if not entry_exec_split:
|
if not entry_exec_split:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Invalid executable command: {0}").format(exec_line)
|
_("Invalid executable command: {0}").format(exec_line)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
@ -263,15 +488,15 @@ class ContextMenuManager:
|
|||||||
else:
|
else:
|
||||||
exe_path = entry_exec_split[-1]
|
exe_path = entry_exec_split[-1]
|
||||||
if not exe_path or not os.path.exists(exe_path):
|
if not exe_path or not os.path.exists(exe_path):
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Executable file not found: {0}").format(exe_path or "None")
|
_("Executable file not found: {0}").format(exe_path or "None")
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
return exe_path
|
return exe_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Failed to parse executable command: {0}").format(e)
|
_("Failed to parse executable command: {0}").format(e)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
@ -280,10 +505,17 @@ class ContextMenuManager:
|
|||||||
"""Remove a file and handle errors."""
|
"""Remove a file and handle errors."""
|
||||||
try:
|
try:
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
self.parent.statusBar().showMessage(success_message.format(game_name), 3000)
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(success_message.format(game_name), 3000)
|
||||||
|
logger.debug("Direct status message: %s", success_message.format(game_name))
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when removing file for '%s'", game_name)
|
||||||
return True
|
return True
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
QMessageBox.warning(self.parent, _("Error"), error_message.format(e))
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
error_message.format(e)
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_game(self, game_name, exec_line):
|
def delete_game(self, game_name, exec_line):
|
||||||
@ -304,13 +536,12 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
desktop_path = self._get_desktop_path(game_name)
|
desktop_path = self._get_desktop_path(game_name)
|
||||||
if not os.path.exists(desktop_path):
|
if not os.path.exists(desktop_path):
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Could not locate .desktop file for '{0}'").format(game_name)
|
_("Could not locate .desktop file for '{0}'").format(game_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get exec_line and parse exe_path
|
|
||||||
exec_line = self._get_exec_line(game_name, exec_line)
|
exec_line = self._get_exec_line(game_name, exec_line)
|
||||||
if not exec_line:
|
if not exec_line:
|
||||||
return
|
return
|
||||||
@ -318,7 +549,6 @@ class ContextMenuManager:
|
|||||||
exe_path = self._parse_exe_path(exec_line, game_name)
|
exe_path = self._parse_exe_path(exec_line, game_name)
|
||||||
exe_name = os.path.splitext(os.path.basename(exe_path))[0] if exe_path else None
|
exe_name = os.path.splitext(os.path.basename(exe_path))[0] if exe_path else None
|
||||||
|
|
||||||
# Remove .desktop file
|
|
||||||
if not self._remove_file(
|
if not self._remove_file(
|
||||||
desktop_path,
|
desktop_path,
|
||||||
_("Failed to delete .desktop file: {0}"),
|
_("Failed to delete .desktop file: {0}"),
|
||||||
@ -327,7 +557,6 @@ class ContextMenuManager:
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Remove custom data if we got an exe_name
|
|
||||||
if exe_name:
|
if exe_name:
|
||||||
xdg_data_home = os.getenv(
|
xdg_data_home = os.getenv(
|
||||||
"XDG_DATA_HOME",
|
"XDG_DATA_HOME",
|
||||||
@ -338,15 +567,11 @@ class ContextMenuManager:
|
|||||||
try:
|
try:
|
||||||
shutil.rmtree(custom_folder)
|
shutil.rmtree(custom_folder)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Failed to delete custom data: {0}").format(e)
|
_("Failed to delete custom data: {0}").format(e)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Refresh UI
|
|
||||||
self.parent.games = self.load_games()
|
|
||||||
self.update_game_grid()
|
|
||||||
|
|
||||||
def add_to_menu(self, game_name, exec_line):
|
def add_to_menu(self, game_name, exec_line):
|
||||||
"""Copy the .desktop file to ~/.local/share/applications."""
|
"""Copy the .desktop file to ~/.local/share/applications."""
|
||||||
if not self._check_portproton():
|
if not self._check_portproton():
|
||||||
@ -354,25 +579,29 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
desktop_path = self._get_desktop_path(game_name)
|
desktop_path = self._get_desktop_path(game_name)
|
||||||
if not os.path.exists(desktop_path):
|
if not os.path.exists(desktop_path):
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Could not locate .desktop file for '{0}'").format(game_name)
|
_("Could not locate .desktop file for '{0}'").format(game_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Destination path
|
|
||||||
applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications")
|
applications_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "applications")
|
||||||
os.makedirs(applications_dir, exist_ok=True)
|
os.makedirs(applications_dir, exist_ok=True)
|
||||||
dest_path = os.path.join(applications_dir, f"{game_name}.desktop")
|
dest_path = os.path.join(applications_dir, f"{game_name}.desktop")
|
||||||
|
|
||||||
# Copy .desktop file
|
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(desktop_path, dest_path)
|
shutil.copyfile(desktop_path, dest_path)
|
||||||
os.chmod(dest_path, 0o755) # Ensure executable permissions
|
os.chmod(dest_path, 0o755)
|
||||||
self.parent.statusBar().showMessage(_("Game '{0}' added to menu").format(game_name), 3000)
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Game '{0}' added to menu").format(game_name), 3000
|
||||||
|
)
|
||||||
|
logger.debug("Direct status message: Game '%s' added to menu", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when adding '%s' to menu", game_name)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Failed to add game to menu: {0}").format(str(e))
|
_("Failed to add game to menu: {0}").format(str(e))
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -394,25 +623,29 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
desktop_path = self._get_desktop_path(game_name)
|
desktop_path = self._get_desktop_path(game_name)
|
||||||
if not os.path.exists(desktop_path):
|
if not os.path.exists(desktop_path):
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Could not locate .desktop file for '{0}'").format(game_name)
|
_("Could not locate .desktop file for '{0}'").format(game_name)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Destination path
|
|
||||||
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()
|
||||||
os.makedirs(desktop_dir, exist_ok=True)
|
os.makedirs(desktop_dir, exist_ok=True)
|
||||||
dest_path = os.path.join(desktop_dir, f"{game_name}.desktop")
|
dest_path = os.path.join(desktop_dir, f"{game_name}.desktop")
|
||||||
|
|
||||||
# Copy .desktop file
|
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(desktop_path, dest_path)
|
shutil.copyfile(desktop_path, dest_path)
|
||||||
os.chmod(dest_path, 0o755) # Ensure executable permissions
|
os.chmod(dest_path, 0o755)
|
||||||
self.parent.statusBar().showMessage(_("Game '{0}' added to desktop").format(game_name), 3000)
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Game '{0}' added to desktop").format(game_name), 3000
|
||||||
|
)
|
||||||
|
logger.debug("Direct status message: Game '%s' added to desktop", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when adding '%s' to desktop", game_name)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
QMessageBox.warning(
|
self._show_warning_dialog(
|
||||||
self.parent, _("Error"),
|
_("Error"),
|
||||||
_("Failed to add game to desktop: {0}").format(str(e))
|
_("Failed to add game to desktop: {0}").format(str(e))
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -429,7 +662,6 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
def edit_game_shortcut(self, game_name, exec_line, cover_path):
|
def edit_game_shortcut(self, game_name, exec_line, cover_path):
|
||||||
"""Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
|
"""Opens the AddGameDialog in edit mode to modify an existing .desktop file."""
|
||||||
|
|
||||||
if not self._check_portproton():
|
if not self._check_portproton():
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -441,7 +673,6 @@ class ContextMenuManager:
|
|||||||
if not exe_path:
|
if not exe_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Open dialog in edit mode
|
|
||||||
dialog = AddGameDialog(
|
dialog = AddGameDialog(
|
||||||
parent=self.parent,
|
parent=self.parent,
|
||||||
theme=self.theme,
|
theme=self.theme,
|
||||||
@ -457,16 +688,20 @@ class ContextMenuManager:
|
|||||||
new_cover_path = dialog.coverEdit.text().strip()
|
new_cover_path = dialog.coverEdit.text().strip()
|
||||||
|
|
||||||
if not new_name or not new_exe_path:
|
if not new_name or not new_exe_path:
|
||||||
QMessageBox.warning(self.parent, _("Error"), _("Game name and executable path are required."))
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Game name and executable path are required.")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Generate new .desktop file content
|
|
||||||
desktop_entry, new_desktop_path = dialog.getDesktopEntryData()
|
desktop_entry, new_desktop_path = dialog.getDesktopEntryData()
|
||||||
if not desktop_entry or not new_desktop_path:
|
if not desktop_entry or not new_desktop_path:
|
||||||
QMessageBox.warning(self.parent, _("Error"), _("Failed to generate .desktop file data."))
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to generate .desktop file data.")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If the name has changed, remove the old .desktop file
|
|
||||||
old_desktop_path = self._get_desktop_path(game_name)
|
old_desktop_path = self._get_desktop_path(game_name)
|
||||||
if game_name != new_name and os.path.exists(old_desktop_path):
|
if game_name != new_name and os.path.exists(old_desktop_path):
|
||||||
self._remove_file(
|
self._remove_file(
|
||||||
@ -476,16 +711,17 @@ class ContextMenuManager:
|
|||||||
game_name
|
game_name
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save the updated .desktop file
|
|
||||||
try:
|
try:
|
||||||
with open(new_desktop_path, "w", encoding="utf-8") as f:
|
with open(new_desktop_path, "w", encoding="utf-8") as f:
|
||||||
f.write(desktop_entry)
|
f.write(desktop_entry)
|
||||||
os.chmod(new_desktop_path, 0o755)
|
os.chmod(new_desktop_path, 0o755)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
QMessageBox.warning(self.parent, _("Error"), _("Failed to save .desktop file: {0}").format(e))
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to save .desktop file: {0}").format(e)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update custom cover if provided
|
|
||||||
if os.path.isfile(new_cover_path):
|
if os.path.isfile(new_cover_path):
|
||||||
exe_name = os.path.splitext(os.path.basename(new_exe_path))[0]
|
exe_name = os.path.splitext(os.path.basename(new_exe_path))[0]
|
||||||
xdg_data_home = os.getenv(
|
xdg_data_home = os.getenv(
|
||||||
@ -500,16 +736,14 @@ class ContextMenuManager:
|
|||||||
try:
|
try:
|
||||||
shutil.copyfile(new_cover_path, os.path.join(custom_folder, f"cover{ext}"))
|
shutil.copyfile(new_cover_path, os.path.join(custom_folder, f"cover{ext}"))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
QMessageBox.warning(self.parent, _("Error"), _("Failed to copy cover image: {0}").format(e))
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to copy cover image: {0}").format(e)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Refresh the game list
|
|
||||||
self.parent.games = self.load_games()
|
|
||||||
self.update_game_grid()
|
|
||||||
|
|
||||||
def add_to_steam(self, game_name, exec_line, cover_path):
|
def add_to_steam(self, game_name, exec_line, cover_path):
|
||||||
"""Handle adding a non-Steam game to Steam via steam_api."""
|
"""Handle adding a non-Steam game to Steam via steam_api."""
|
||||||
|
|
||||||
if not self._check_portproton():
|
if not self._check_portproton():
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -521,37 +755,174 @@ class ContextMenuManager:
|
|||||||
if not exe_path:
|
if not exe_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
success, message = add_to_steam(game_name, exec_line, cover_path)
|
def on_add_to_steam_result(result: tuple[bool, str]):
|
||||||
if success:
|
success, message = result
|
||||||
QMessageBox.information(
|
if success:
|
||||||
self.parent, _("Restart Steam"),
|
self.signals.show_status_message.emit(
|
||||||
_("The game was added successfully.\nPlease restart Steam for changes to take effect.")
|
_("The game was added successfully. Please restart Steam for changes to take effect."), 5000
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.signals.show_warning_dialog.emit(_("Error"), message)
|
||||||
|
|
||||||
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Adding '{0}' to Steam...").format(game_name), 0
|
||||||
)
|
)
|
||||||
|
logger.debug("Direct status message: Adding '%s' to Steam", game_name)
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(self.parent, _("Error"), message)
|
logger.warning("Status bar not available when adding '%s' to Steam", game_name)
|
||||||
|
add_to_steam(game_name, exec_line, cover_path)
|
||||||
def remove_from_steam(self, game_name, exec_line):
|
|
||||||
"""Handle removing a non-Steam game from Steam via steam_api."""
|
|
||||||
|
|
||||||
|
def remove_from_steam(self, game_name, exec_line, game_source):
|
||||||
|
"""Handle removing a game from Steam via steam_api, supporting both EGS and non-EGS games."""
|
||||||
if not self._check_portproton():
|
if not self._check_portproton():
|
||||||
return
|
return
|
||||||
|
|
||||||
exec_line = self._get_exec_line(game_name, exec_line)
|
def on_remove_from_steam_result(result: tuple[bool, str]):
|
||||||
if not exec_line:
|
success, message = result
|
||||||
return
|
if success:
|
||||||
|
self.signals.show_status_message.emit(
|
||||||
|
_("The game was removed successfully. Please restart Steam for changes to take effect."), 5000
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.signals.show_warning_dialog.emit(_("Error"), message)
|
||||||
|
|
||||||
exe_path = self._parse_exe_path(exec_line, game_name)
|
if game_source == "epic":
|
||||||
if not exe_path:
|
# For EGS games, construct the script path used in Steam shortcuts.vdf
|
||||||
return
|
if not self.portproton_location:
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("PortProton directory not found")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
steam_scripts_dir = os.path.join(self.portproton_location, "steam_scripts")
|
||||||
|
safe_game_name = re.sub(r'[<>:"/\\|?*]', '_', game_name.strip())
|
||||||
|
script_path = os.path.join(steam_scripts_dir, f"{safe_game_name}_egs.sh")
|
||||||
|
quoted_script_path = f'"{script_path}"'
|
||||||
|
|
||||||
|
# Directly remove the shortcut by matching AppName and Exe
|
||||||
|
try:
|
||||||
|
from portprotonqt.steam_api import get_steam_home, get_last_steam_user, convert_steam_id
|
||||||
|
steam_home = get_steam_home()
|
||||||
|
if not steam_home:
|
||||||
|
self._show_warning_dialog(_("Error"), _("Steam directory not found"))
|
||||||
|
return
|
||||||
|
|
||||||
|
last_user = get_last_steam_user(steam_home)
|
||||||
|
if not last_user or 'SteamID' not in last_user:
|
||||||
|
self._show_warning_dialog(_("Error"), _("Failed to get Steam user ID"))
|
||||||
|
return
|
||||||
|
|
||||||
|
userdata_dir = os.path.join(steam_home, "userdata")
|
||||||
|
user_id = last_user['SteamID']
|
||||||
|
unsigned_id = convert_steam_id(user_id)
|
||||||
|
user_dir = os.path.join(userdata_dir, str(unsigned_id))
|
||||||
|
steam_shortcuts_path = os.path.join(user_dir, "config", "shortcuts.vdf")
|
||||||
|
backup_path = f"{steam_shortcuts_path}.backup"
|
||||||
|
|
||||||
|
if not os.path.exists(steam_shortcuts_path):
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Steam shortcuts file not found")
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Backup shortcuts.vdf
|
||||||
|
try:
|
||||||
|
shutil.copy2(steam_shortcuts_path, backup_path)
|
||||||
|
logger.info(f"Created backup of shortcuts.vdf at {backup_path}")
|
||||||
|
except Exception as e:
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to create backup of shortcuts.vdf: {0}").format(e)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load shortcuts.vdf
|
||||||
|
try:
|
||||||
|
with open(steam_shortcuts_path, 'rb') as f:
|
||||||
|
shortcuts_data = vdf.binary_load(f)
|
||||||
|
except Exception as e:
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to load shortcuts.vdf: {0}").format(e)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
shortcuts = shortcuts_data.get("shortcuts", {})
|
||||||
|
modified = False
|
||||||
|
new_shortcuts = {}
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
for _key, entry in shortcuts.items():
|
||||||
|
if entry.get("AppName") == game_name and entry.get("Exe") == quoted_script_path:
|
||||||
|
modified = True
|
||||||
|
logger.info(f"Removing EGS game '{game_name}' from Steam shortcuts")
|
||||||
|
continue
|
||||||
|
new_shortcuts[str(index)] = entry
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
if not modified:
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Game '{0}' not found in Steam shortcuts").format(game_name)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Save updated shortcuts.vdf
|
||||||
|
try:
|
||||||
|
with open(steam_shortcuts_path, 'wb') as f:
|
||||||
|
vdf.binary_dump({"shortcuts": new_shortcuts}, f)
|
||||||
|
logger.info(f"Updated shortcuts.vdf, removed '{game_name}'")
|
||||||
|
on_remove_from_steam_result((True, f"Game '{game_name}' removed from Steam"))
|
||||||
|
except Exception as e:
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to update shortcuts.vdf: {0}").format(e)
|
||||||
|
)
|
||||||
|
if os.path.exists(backup_path):
|
||||||
|
try:
|
||||||
|
shutil.copy2(backup_path, steam_shortcuts_path)
|
||||||
|
logger.info("Restored shortcuts.vdf from backup")
|
||||||
|
except Exception as restore_err:
|
||||||
|
logger.error(f"Failed to restore shortcuts.vdf: {restore_err}")
|
||||||
|
on_remove_from_steam_result((False, f"Failed to update shortcuts.vdf: {e}"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Optionally, remove the script file
|
||||||
|
if os.path.exists(script_path):
|
||||||
|
try:
|
||||||
|
os.remove(script_path)
|
||||||
|
logger.info(f"Removed EGS script file: {script_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to remove EGS script file {script_path}: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to remove EGS game from Steam: {0}").format(e)
|
||||||
|
)
|
||||||
|
on_remove_from_steam_result((False, f"Failed to remove EGS game from Steam: {e}"))
|
||||||
|
return
|
||||||
|
|
||||||
success, message = remove_from_steam(game_name, exec_line)
|
|
||||||
if success:
|
|
||||||
QMessageBox.information(
|
|
||||||
self.parent, _("Restart Steam"),
|
|
||||||
_("The game was removed successfully.\nPlease restart Steam for changes to take effect.")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
QMessageBox.warning(self.parent, _("Error"), message)
|
# For non-EGS games, use the existing logic
|
||||||
|
exec_line = self._get_exec_line(game_name, exec_line)
|
||||||
|
if not exec_line:
|
||||||
|
return
|
||||||
|
|
||||||
|
exe_path = self._parse_exe_path(exec_line, game_name)
|
||||||
|
if not exe_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Removing '{0}' from Steam...").format(game_name), 0
|
||||||
|
)
|
||||||
|
logger.debug("Direct status message: Removing '%s' from Steam", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when removing '%s' from Steam", game_name)
|
||||||
|
remove_from_steam(game_name, exec_line)
|
||||||
|
|
||||||
def open_game_folder(self, game_name, exec_line):
|
def open_game_folder(self, game_name, exec_line):
|
||||||
"""Open the folder containing the game's executable."""
|
"""Open the folder containing the game's executable."""
|
||||||
@ -569,6 +940,15 @@ class ContextMenuManager:
|
|||||||
try:
|
try:
|
||||||
folder_path = os.path.dirname(os.path.abspath(exe_path))
|
folder_path = os.path.dirname(os.path.abspath(exe_path))
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
|
QDesktopServices.openUrl(QUrl.fromLocalFile(folder_path))
|
||||||
self.parent.statusBar().showMessage(_("Opened folder for '{0}'").format(game_name), 3000)
|
if self.parent.statusBar():
|
||||||
|
self.parent.statusBar().showMessage(
|
||||||
|
_("Opened folder for '{0}'").format(game_name), 3000
|
||||||
|
)
|
||||||
|
logger.debug("Direct status message: Opened folder for '%s'", game_name)
|
||||||
|
else:
|
||||||
|
logger.warning("Status bar not available when opening folder for '%s'", game_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self.parent, _("Error"), _("Failed to open game folder: {0}").format(str(e)))
|
self._show_warning_dialog(
|
||||||
|
_("Error"),
|
||||||
|
_("Failed to open game folder: {0}").format(str(e))
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user