SteamShortcuts: Add action in the right-click drop-down menu
to export and delete game shortcuts from Steam.
This commit is contained in:
parent
da93dc2e5e
commit
88aecd0741
4 changed files with 189 additions and 89 deletions
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue