1
0
Fork 0
mirror of synced 2024-10-01 17:57:00 +13:00

SteamShortcuts: Add action in the right-click drop-down menu

to export and delete game shortcuts from Steam.
This commit is contained in:
loathingKernel 2024-05-31 15:53:08 +03:00
parent da93dc2e5e
commit 88aecd0741
4 changed files with 189 additions and 89 deletions

View file

@ -2,18 +2,22 @@ import platform
import random
from logging import getLogger
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent, QTimer
from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot, QObject, QEvent
from PyQt5.QtGui import QMouseEvent, QShowEvent, QPaintEvent
from PyQt5.QtWidgets import QMessageBox, QAction
from rare.models.game import RareGame
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
ImageManagerSingleton,
)
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton, ImageManagerSingleton
from rare.utils.paths import desktop_links_supported, desktop_link_path, create_desktop_link
from rare.utils.steam_shortcuts import (
steam_shortcuts_supported,
steam_shortcut_exists,
remove_steam_shortcut,
remove_steam_coverart,
add_steam_shortcut,
add_steam_coverart,
save_steam_shortcuts,
)
from .library_widget import LibraryWidget
logger = getLogger("GameWidget")
@ -40,13 +44,14 @@ class GameWidget(LibraryWidget):
self.install_action.triggered.connect(self._install)
self.desktop_link_action = QAction(self)
self.desktop_link_action.triggered.connect(
lambda: self._create_link(self.rgame.folder_name, "desktop")
)
self.desktop_link_action.triggered.connect(lambda: self._create_link(self.rgame.folder_name, "desktop"))
self.menu_link_action = QAction(self)
self.menu_link_action.triggered.connect(
lambda: self._create_link(self.rgame.folder_name, "start_menu")
self.menu_link_action.triggered.connect(lambda: self._create_link(self.rgame.folder_name, "start_menu"))
self.steam_shortcut_action = QAction(self)
self.steam_shortcut_action.triggered.connect(
lambda: self._create_steam_shortcut(self.rgame.app_name, self.rgame.app_title)
)
self.reload_action = QAction(self.tr("Reload Image"), self)
@ -65,12 +70,8 @@ class GameWidget(LibraryWidget):
self.rgame.signals.game.uninstalled.connect(self.update_actions)
self.rgame.signals.progress.start.connect(self.start_progress)
self.rgame.signals.progress.update.connect(
lambda p: self.updateProgress(p)
)
self.rgame.signals.progress.finish.connect(
lambda e: self.hideProgress(e)
)
self.rgame.signals.progress.update.connect(lambda p: self.updateProgress(p))
self.rgame.signals.progress.finish.connect(lambda e: self.hideProgress(e))
self.state_strings = {
RareGame.State.IDLE: "",
@ -83,7 +84,7 @@ class GameWidget(LibraryWidget):
"has_update": self.tr("Update available"),
"needs_verification": self.tr("Needs verification"),
"not_can_launch": self.tr("Can't launch"),
"save_not_up_to_date": self.tr("Save is not up-to-date")
"save_not_up_to_date": self.tr("Save is not up-to-date"),
}
self.hover_strings = {
@ -126,9 +127,11 @@ class GameWidget(LibraryWidget):
self.ui.status_label.setText(self.state_strings["needs_verification"])
elif not self.rgame.can_launch and self.rgame.is_installed:
self.ui.status_label.setText(self.state_strings["not_can_launch"])
elif self.rgame.igame and (
self.rgame.game.supports_cloud_saves or self.rgame.game.supports_mac_cloud_saves
) and not self.rgame.is_save_up_to_date:
elif (
self.rgame.igame
and (self.rgame.game.supports_cloud_saves or self.rgame.game.supports_mac_cloud_saves)
and not self.rgame.is_save_up_to_date
):
self.ui.status_label.setText(self.state_strings["save_not_up_to_date"])
else:
self.ui.status_label.setText(self.state_strings[self.rgame.state])
@ -143,6 +146,8 @@ class GameWidget(LibraryWidget):
self.ui.launch_btn.setVisible(self.rgame.is_installed)
self.ui.launch_btn.setEnabled(self.rgame.can_launch)
self.steam_shortcut_action.setEnabled(self.rgame.has_pixmap)
@pyqtSlot()
def update_actions(self):
for action in self.actions():
@ -165,6 +170,13 @@ class GameWidget(LibraryWidget):
self.menu_link_action.setText(self.tr("Create Start Menu link"))
self.addAction(self.menu_link_action)
if steam_shortcuts_supported() and self.rgame.is_installed:
if steam_shortcut_exists(self.rgame.app_name):
self.steam_shortcut_action.setText(self.tr("Remove from Steam"))
else:
self.steam_shortcut_action.setText(self.tr("Add to Steam"))
self.addAction(self.steam_shortcut_action)
self.addAction(self.reload_action)
if self.rgame.is_installed and not self.rgame.is_origin:
self.addAction(self.uninstall_action)
@ -217,9 +229,7 @@ class GameWidget(LibraryWidget):
offline = True
if self.rgame.has_update:
skip_version_check = True
self.rgame.launch(
offline=offline, skip_update_check=skip_version_check
)
self.rgame.launch(offline=offline, skip_update_check=skip_version_check)
@pyqtSlot()
def _install(self):
@ -229,7 +239,8 @@ class GameWidget(LibraryWidget):
def _uninstall(self):
self.show_info.emit(self.rgame)
def _create_link(self, name, link_type):
@pyqtSlot(str, str)
def _create_link(self, name: str, link_type: str):
if not desktop_links_supported():
QMessageBox.warning(
self,
@ -252,16 +263,18 @@ class GameWidget(LibraryWidget):
except PermissionError:
QMessageBox.warning(self, "Error", "Could not create shortcut.")
return
if link_type == "desktop":
self.desktop_link_action.setText(self.tr("Remove Desktop link"))
elif link_type == "start_menu":
self.menu_link_action.setText(self.tr("Remove Start Menu link"))
else:
if shortcut_path.exists():
shortcut_path.unlink(missing_ok=True)
self.update_actions()
if link_type == "desktop":
self.desktop_link_action.setText(self.tr("Create Desktop link"))
elif link_type == "start_menu":
self.menu_link_action.setText(self.tr("Create Start Menu link"))
@pyqtSlot(str, str)
def _create_steam_shortcut(self, app_name: str, app_title: str):
if steam_shortcut_exists(app_name):
if shortcut := remove_steam_shortcut(app_name):
remove_steam_coverart(shortcut)
else:
if shortcut := add_steam_shortcut(app_name, app_title):
add_steam_coverart(app_name, shortcut)
save_steam_shortcuts()
self.update_actions()

View file

@ -109,8 +109,8 @@ class SteamShortcut:
shortcut.appid = cls.calculate_appid(app_name)
shortcut.AppName = app_title
shortcut.Exe = shlex.quote(executable)
shortcut.StartDir = start_dir
shortcut.icon = icon
shortcut.StartDir = shlex.quote(start_dir)
shortcut.icon = shlex.quote(icon)
shortcut.LaunchOptions = shlex.join(launch_options)
return shortcut

View file

@ -17,6 +17,7 @@ from rare.models.game import RareGame, RareEosOverlay
from rare.models.signals import GlobalSignals
from rare.utils.metrics import timelogger
from rare.utils import config_helper
from rare.utils.steam_shortcuts import load_steam_shortcuts
from .image_manager import ImageManager
from .workers import (
QueueWorker,
@ -354,6 +355,7 @@ class RareCore(QObject):
self.__wrappers.import_wrappers(
self.__core, self.__settings, [rgame.app_name for rgame in self.games]
)
load_steam_shortcuts()
self.progress.emit(100, self.tr("Launching Rare"))
self.completed.emit()
QTimer.singleShot(100, self.__post_init)

View file

@ -2,27 +2,43 @@ import os
import platform
import shutil
from logging import getLogger
from pathlib import Path
from typing import Optional, List, Dict
from dataclasses import asdict
import vdf
from rare.utils.paths import image_icon_path, image_wide_path, image_tall_path, desktop_icon_path, get_rare_executable
from rare.models.steam import SteamUser, SteamShortcut
if platform.system() == "Windows":
import winreg
logger = getLogger("SteamShortcuts")
steam_client_install_paths = [os.path.expanduser("~/.local/share/Steam")]
def find_steam() -> Optional[str]:
if platform.system() == "Windows":
# Find the Steam install directory or raise an error
try: # 32-bit
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Valve\\Steams")
except FileNotFoundError:
try: # 64-bit
key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Wow6432Node\\Valve\\Steam")
except FileNotFoundError as e:
return None
return winreg.QueryValueEx(key, "InstallPath")[0]
# return the first valid path
if platform.system() in {"Linux", "FreeBSD"}:
elif platform.system() in {"Linux", "FreeBSD"}:
for path in steam_client_install_paths:
if os.path.isdir(path) and os.path.isfile(os.path.join(path, "steam.sh")):
return path
return None
def find_users(steam_path: str) -> List[SteamUser]:
def find_steam_users(steam_path: str) -> List[SteamUser]:
_users = []
vdf_path = os.path.join(steam_path, "config", "loginusers.vdf")
with open(vdf_path, 'r') as f:
@ -32,7 +48,7 @@ def find_users(steam_path: str) -> List[SteamUser]:
return _users
def load_shortcuts(steam_path: str, user: SteamUser) -> Dict[str, SteamShortcut]:
def _load_shortcuts(steam_path: str, user: SteamUser) -> Dict[str, SteamShortcut]:
_shortcuts = {}
vdf_path = os.path.join(steam_path, "userdata", str(user.short_id), "config", "shortcuts.vdf")
with open(vdf_path, 'rb') as f:
@ -42,67 +58,136 @@ def load_shortcuts(steam_path: str, user: SteamUser) -> Dict[str, SteamShortcut]
return _shortcuts
def save_shortcuts(steam_path: str, user: SteamUser, shortcuts: Dict[str, Dict]) -> None:
def _save_shortcuts(steam_path: str, user: SteamUser, shortcuts: Dict[str, SteamShortcut]) -> None:
_shortcuts = {k: asdict(v) for k, v in shortcuts.items()}
vdf_path = os.path.join(steam_path, "userdata", str(user.short_id), "config", "shortcuts.vdf")
with open(vdf_path, 'wb') as f:
vdf.binary_dump({"shortcuts": shortcuts}, f)
vdf.binary_dump({"shortcuts": _shortcuts}, f)
__steam_dir: Optional[str] = None
__steam_user: Optional[SteamUser] = None
__steam_shortcuts: Optional[Dict] = None
def steam_shortcuts_supported() -> bool:
return __steam_dir is not None and __steam_user is not None and __steam_shortcuts is not None
def load_steam_shortcuts():
global __steam_shortcuts, __steam_dir, __steam_user
if __steam_shortcuts is not None:
return
steam_dir = find_steam()
if not steam_dir:
logger.error("Failed to find Steam install directory")
return
__steam_dir = steam_dir
steam_users = find_steam_users(steam_dir)
if not steam_users:
logger.error("Failed to find any Steam users")
return
else:
steam_user = next(filter(lambda x: x.most_recent, steam_users))
logger.info("Found most recently logged-in user %s(%s)", steam_user.account_name, steam_user.persona_name)
__steam_user = steam_user
__steam_shortcuts = _load_shortcuts(steam_dir, steam_user)
def save_steam_shortcuts():
if __steam_shortcuts:
_save_shortcuts(__steam_dir, __steam_user, __steam_shortcuts)
logger.info("Saved Steam shortcuts for user %s(%s)", __steam_user.account_name, __steam_user.persona_name)
else:
logger.error("Failed to save Steam shortcuts")
def steam_shortcut_exists(app_name: str) -> bool:
return SteamShortcut.calculate_appid(app_name) in {s.appid for s in __steam_shortcuts.values()}
def remove_steam_shortcut(app_name: str) -> Optional[SteamShortcut]:
global __steam_shortcuts
if not steam_shortcut_exists(app_name):
logger.error("Game %s doesn't have an associated Steam shortcut", app_name)
return None
appid = SteamShortcut.calculate_appid(app_name)
removed = next(filter(lambda item: item[1].appid == appid, __steam_shortcuts.items()))
shortcuts = dict(filter(lambda item: item[1].appid != appid, __steam_shortcuts.items()))
__steam_shortcuts = shortcuts
return removed[1]
def add_steam_shortcut(app_name: str, app_title: str) -> SteamShortcut:
global __steam_shortcuts
if steam_shortcut_exists(app_name):
logger.info("Removing old Steam shortcut for %s", app_name)
remove_steam_shortcut(app_name)
command = get_rare_executable()
arguments = ["launch", app_name]
if len(command) > 1:
arguments = command[1:] + arguments
shortcut = SteamShortcut.create(
app_name=app_name,
app_title=f"{app_title} (Rare)",
executable=command[0],
start_dir=os.path.dirname(command[0]),
icon=desktop_icon_path(app_name).as_posix(),
launch_options=arguments,
)
key = int(max(__steam_shortcuts.keys(), default="0"))
__steam_shortcuts[str(key+1)] = shortcut
return shortcut
def add_steam_coverart(app_name: str, shortcut: SteamShortcut):
steam_grid_dir = os.path.join(__steam_dir, "userdata", str(__steam_user.short_id), "config", "grid")
shutil.copy(image_wide_path(app_name), os.path.join(steam_grid_dir, shortcut.game_hero))
shutil.copy(image_icon_path(app_name), os.path.join(steam_grid_dir, shortcut.game_logo))
shutil.copy(image_wide_path(app_name), os.path.join(steam_grid_dir, shortcut.grid_wide))
shutil.copy(image_tall_path(app_name), os.path.join(steam_grid_dir, shortcut.grid_tall))
def remove_steam_coverart(shortcut: SteamShortcut):
steam_grid_dir = os.path.join(__steam_dir, "userdata", str(__steam_user.short_id), "config", "grid")
Path(steam_grid_dir).joinpath(shortcut.game_hero).unlink(missing_ok=True)
Path(steam_grid_dir).joinpath(shortcut.game_logo).unlink(missing_ok=True)
Path(steam_grid_dir).joinpath(shortcut.grid_wide).unlink(missing_ok=True)
Path(steam_grid_dir).joinpath(shortcut.grid_tall).unlink(missing_ok=True)
if __name__ == "__main__":
from pprint import pprint
from dataclasses import asdict
steam_dir = find_steam()
load_steam_shortcuts()
users = find_users(steam_dir)
print(users)
print(__steam_dir)
print(__steam_user)
print(__steam_shortcuts)
user = next(filter(lambda x: x.most_recent, users))
print(user)
print()
shortcuts = load_shortcuts(steam_dir, user)
for k, s in shortcuts.items():
print(asdict(s))
print(vars(s))
def print_shortcuts():
for k, s in __steam_shortcuts.items():
print({k: asdict(s)})
print(vars(s))
print()
def image_path(app_name: str, image: str) -> str:
return f"/home/loathingkernel/.local/share/Rare/Rare/images/{app_name}/{image}"
print_shortcuts()
test_shc = SteamShortcut.create(
app_name="18fafa2d70d64831ab500a9d65ba9ab8",
app_title="Crying Suns (Rare Test)",
executable="/usr/bin/rare",
start_dir="/usr/bin",
icon=image_path("18fafa2d70d64831ab500a9d65ba9ab8", "icon.png"),
launch_options=["launch", "18fafa2d70d64831ab500a9d65ba9ab8"]
)
print(asdict(test_shc))
print(vars(test_shc))
test_vdf = vdf.binary_dumps(asdict(test_shc))
print(vdf.binary_loads(test_vdf))
add_steam_shortcut("test1", "Test1")
add_steam_shortcut("test2", "Test2")
add_steam_shortcut("test3", "Test3")
add_steam_shortcut("test1", "Test1")
save_shortcuts(steam_dir, user, {"0": asdict(test_shc)})
remove_steam_shortcut("test2")
steam_grid_dir = os.path.join(steam_dir, "userdata", str(user.short_id), "config", "grid")
shutil.copy(
image_path("18fafa2d70d64831ab500a9d65ba9ab8", "card_installed.png"),
os.path.join(steam_grid_dir, test_shc.game_hero)
)
shutil.copy(
image_path("18fafa2d70d64831ab500a9d65ba9ab8", "icon.png"),
os.path.join(steam_grid_dir, test_shc.game_logo)
)
shutil.copy(
image_path("18fafa2d70d64831ab500a9d65ba9ab8", "card_installed.png"),
os.path.join(steam_grid_dir, test_shc.grid_wide)
)
shutil.copy(
image_path("18fafa2d70d64831ab500a9d65ba9ab8", "card_installed.png"),
os.path.join(steam_grid_dir, test_shc.grid_tall)
)
shortcuts = load_shortcuts(steam_dir, user)
print(shortcuts)
print_shortcuts()