import json import math import os import platform import shutil import subprocess import sys from logging import getLogger from typing import Tuple, List import qtawesome import requests from PyQt5.QtCore import ( pyqtSignal, pyqtSlot, QObject, QRunnable, QSettings, Qt, QFile, QDir, ) from PyQt5.QtGui import QPalette, QColor, QPixmap, QImage from PyQt5.QtWidgets import QApplication, QStyleFactory from legendary.models.game import Game from requests.exceptions import HTTPError from .models import PathSpec # Windows if platform.system() == "Windows": # noinspection PyUnresolvedReferences from win32com.client import Dispatch # pylint: disable=E0401 from rare.shared import LegendaryCoreSingleton, ApiResultsSingleton from rare.utils.paths import image_dir, resources_path # Mac not supported from legendary.core import LegendaryCore logger = getLogger("Utils") settings = QSettings("Rare", "Rare") def download_images(progress: pyqtSignal, results: pyqtSignal, core: LegendaryCore): if not os.path.isdir(image_dir): os.makedirs(image_dir) logger.info("Create Image dir") # Download Images games, dlcs = core.get_game_and_dlc_list(True, skip_ue=False) results.emit((games, dlcs), "gamelist") dlc_list = [] for i in dlcs.values(): dlc_list.append(i[0]) no_assets = core.get_non_asset_library_items()[0] results.emit(no_assets, "no_assets") game_list = games + dlc_list + no_assets for i, game in enumerate(game_list): if game.app_title == "Unreal Engine": game.app_title += f" {game.app_name.split('_')[-1]}" core.lgd.set_game_meta(game.app_name, game) try: download_image(game) except json.decoder.JSONDecodeError: shutil.rmtree(f"{image_dir}/{game.app_name}") download_image(game) progress.emit(i * 100 // len(game_list)) def download_image(game, force=False): if force and os.path.exists(f"{image_dir}/{game.app_name}"): shutil.rmtree(f"{image_dir}/{game.app_name}") if not os.path.isdir(f"{image_dir}/{game.app_name}"): os.mkdir(f"{image_dir}/{game.app_name}") # to get picture updates if not os.path.isfile(f"{image_dir}/{game.app_name}/image.json"): json_data = { "DieselGameBoxTall": None, "DieselGameBoxLogo": None, "Thumbnail": None, } else: json_data = json.load(open(f"{image_dir}/{game.app_name}/image.json", "r")) # Download for image in game.metadata["keyImages"]: if ( image["type"] == "DieselGameBoxTall" or image["type"] == "DieselGameBoxLogo" or image["type"] == "Thumbnail" ): if image["type"] not in json_data.keys(): json_data[image["type"]] = None if json_data[image["type"]] != image["md5"] or not os.path.isfile( f"{image_dir}/{game.app_name}/{image['type']}.png" ): # Download json_data[image["type"]] = image["md5"] # os.remove(f"{image_dir}/{game.app_name}/{image['type']}.png") json.dump( json_data, open(f"{image_dir}/{game.app_name}/image.json", "w") ) logger.info(f"Download Image for Game: {game.app_title}") url = image["url"] resp = requests.get(url) img = QImage() img.loadFromData(resp.content) img = img.scaled( 200, 200 * 4 // 3, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation, ) img.save( os.path.join(image_dir, game.app_name, image["type"] + ".png"), format="PNG", ) color_role_map = { 0: "WindowText", 1: "Button", 2: "Light", 3: "Midlight", 4: "Dark", 5: "Mid", 6: "Text", 7: "BrightText", 8: "ButtonText", 9: "Base", 10: "Window", 11: "Shadow", 12: "Highlight", 13: "HighlightedText", 14: "Link", 15: "LinkVisited", 16: "AlternateBase", # 17: "NoRole", 18: "ToolTipBase", 19: "ToolTipText", 20: "PlaceholderText", # 21: "NColorRoles", } color_group_map = { 0: "Active", 1: "Disabled", 2: "Inactive", } def load_color_scheme(path: str) -> QPalette: palette = QPalette() scheme = QSettings(path, QSettings.IniFormat) try: scheme.beginGroup("ColorScheme") for g in color_group_map: scheme.beginGroup(color_group_map[g]) group = QPalette.ColorGroup(g) for r in color_role_map: role = QPalette.ColorRole(r) color = scheme.value(color_role_map[r], None) if color is not None: palette.setColor(group, role, QColor(color)) else: palette.setColor(group, role, palette.color(QPalette.Active, role)) scheme.endGroup() scheme.endGroup() except: palette = None return palette def set_color_pallete(color_scheme: str): if not color_scheme: QApplication.instance().setStyle( QStyleFactory.create(QApplication.instance().property("rareDefaultQtStyle")) ) QApplication.instance().setStyleSheet("") QApplication.instance().setPalette( QApplication.instance().style().standardPalette() ) return QApplication.instance().setStyle(QStyleFactory.create("Fusion")) custom_palette = load_color_scheme(f":/schemes/{color_scheme}") if custom_palette is not None: QApplication.instance().setPalette(custom_palette) qtawesome.set_defaults(color=custom_palette.color(QPalette.Text)) def get_color_schemes() -> List[str]: colors = [] for file in QDir(":/schemes"): colors.append(file) return colors def set_style_sheet(style_sheet: str): if not style_sheet: QApplication.instance().setStyle( QStyleFactory.create(QApplication.instance().property("rareDefaultQtStyle")) ) QApplication.instance().setStyleSheet("") return QApplication.instance().setStyle(QStyleFactory.create("Fusion")) file = QFile(f":/stylesheets/{style_sheet}") file.open(QFile.ReadOnly) stylesheet = file.readAll().data().decode("utf-8") QApplication.instance().setStyleSheet(stylesheet) qtawesome.set_defaults(color="white") def get_style_sheets() -> List[str]: styles = [] for file in QDir(":/stylesheets/"): styles.append(file) return styles def get_translations(): langs = ["en"] for i in os.listdir(os.path.join(resources_path, "languages")): if i.endswith(".qm") and not i.startswith("qt_"): langs.append(i.split(".")[0]) return langs def get_latest_version(): try: resp = requests.get( "https://api.github.com/repos/Dummerle/Rare/releases/latest" ) tag = resp.json()["tag_name"] return tag except requests.exceptions.ConnectionError: return "0.0.0" def get_size(b: int) -> str: for i in ["", "K", "M", "G", "T", "P", "E"]: if b < 1024: return f"{b:.2f}{i}B" b /= 1024 def create_rare_desktop_link(type_of_link): # Linux if platform.system() == "Linux": if type_of_link == "desktop": path = os.path.expanduser("~/Desktop/") elif type_of_link == "start_menu": path = os.path.expanduser("~/.local/share/applications/") else: return if not os.path.exists(path): return if p := os.environ.get("APPIMAGE"): executable = p else: executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}" with open(os.path.join(path, "Rare.desktop"), "w") as desktop_file: desktop_file.write( "[Desktop Entry]\n" f"Name=Rare\n" f"Type=Application\n" f"Categories=Game;\n" f"Icon={os.path.join(resources_path, 'images', 'Rare.png')}\n" f"Exec={executable}\n" "Terminal=false\n" "StartupWMClass=rare\n" ) desktop_file.close() os.chmod(os.path.expanduser(os.path.join(path, "Rare.desktop")), 0o755) elif platform.system() == "Windows": # Target of shortcut if type_of_link == "desktop": target_folder = os.path.expanduser("~/Desktop/") elif type_of_link == "start_menu": target_folder = os.path.expandvars("%appdata%/Microsoft/Windows/Start Menu") else: logger.warning("No valid type of link") return linkName = "Rare.lnk" # Path to location of link file pathLink = os.path.join(target_folder, linkName) executable = sys.executable executable = executable.replace("python.exe", "pythonw.exe") logger.debug(executable) # Add shortcut shell = Dispatch("WScript.Shell") shortcut = shell.CreateShortCut(pathLink) shortcut.Targetpath = executable if not sys.executable.endswith("Rare.exe"): shortcut.Arguments = os.path.abspath(sys.argv[0]) # Icon shortcut.IconLocation = os.path.join(resources_path, "images", "Rare.ico") shortcut.save() def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop") -> bool: igame = core.get_installed_game(app_name) if os.path.exists(p := os.path.join(image_dir, igame.app_name, "Thumbnail.png")): icon = p elif os.path.exists( p := os.path.join(image_dir, igame.app_name, "DieselGameBoxLogo.png") ): icon = p else: icon = os.path.join( os.path.join(image_dir, igame.app_name, "DieselGameBoxTall.png") ) icon = icon.replace(".png", "") # Linux if platform.system() == "Linux": if type_of_link == "desktop": path = os.path.expanduser(f"~/Desktop/") elif type_of_link == "start_menu": path = os.path.expanduser("~/.local/share/applications/") else: return False if not os.path.exists(path): return False if p := os.environ.get("APPIMAGE"): executable = p else: executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}" with open(f"{path}{igame.title}.desktop", "w") as desktop_file: desktop_file.write( "[Desktop Entry]\n" f"Name={igame.title}\n" f"Type=Application\n" f"Categories=Game;\n" f"Icon={icon}.png\n" f"Exec={executable} launch {app_name}\n" "Terminal=false\n" "StartupWMClass=rare-game\n" ) desktop_file.close() os.chmod(os.path.expanduser(f"{path}{igame.title}.desktop"), 0o755) # Windows elif platform.system() == "Windows": # Target of shortcut if type_of_link == "desktop": target_folder = os.path.expanduser("~/Desktop/") elif type_of_link == "start_menu": target_folder = os.path.expandvars("%appdata%/Microsoft/Windows/Start Menu") else: logger.warning("No valid type of link") return False if not os.path.exists(target_folder): return False # Name of link file linkName = igame.title for c in r'<>?":|\/*': linkName.replace(c, "") linkName = f"{linkName.strip()}.lnk" # Path to location of link file pathLink = os.path.join(target_folder, linkName) # Add shortcut shell = Dispatch("WScript.Shell") shortcut = shell.CreateShortCut(pathLink) if sys.executable.endswith("Rare.exe"): executable = sys.executable else: executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}" shortcut.Targetpath = executable shortcut.Arguments = f"launch {app_name}" shortcut.WorkingDirectory = os.getcwd() # Icon if not os.path.exists(f"{icon}.ico"): img = QImage() img.load(f"{icon}.png") img.save(f"{icon}.ico") logger.info("Create Icon") shortcut.IconLocation = os.path.join(f"{icon}.ico") shortcut.save() return True elif platform.system() == "Darwin": return False def get_pixmap(app_name: str) -> QPixmap: for img in ["FinalArt.png", "DieselGameBoxTall.png", "DieselGameBoxLogo.png"]: if os.path.exists(image := os.path.join(image_dir, app_name, img)): pixmap = QPixmap(image) break else: pixmap = QPixmap() return pixmap def get_uninstalled_pixmap(app_name: str) -> QPixmap: pm = get_pixmap(app_name) grey_image = pm.toImage().convertToFormat(QImage.Format_Grayscale8) return QPixmap.fromImage(grey_image) def optimal_text_background(image: list) -> Tuple[int, int, int]: """ Finds an optimal background color for text on the image by calculating the average color of the image and inverting it. The image list is supposed to be a one-dimensional list of arbitrary length containing RGB tuples, ranging from 0 to 255. """ # cursed, I know average = map(lambda value: value // len(image), map(sum, zip(*image))) inverted = map(lambda value: 255 - value, average) return tuple(inverted) def text_color_for_background(background: Tuple[int, int, int]) -> Tuple[int, int, int]: """ Calculates whether a black or white text color would fit better for the given background, and returns that color. This is done by calculating the luminance and simple comparing of bounds """ # see https://alienryderflex.com/hsp.html (red, green, blue) = background luminance = math.sqrt(0.299 * red ** 2 + 0.587 * green ** 2 + 0.114 * blue ** 2) if luminance < 127: return 255, 255, 255 else: return 0, 0, 0 class WineResolverSignals(QObject): result_ready = pyqtSignal(str) class WineResolver(QRunnable): def __init__(self, path: str, app_name: str, core: LegendaryCore): super(WineResolver, self).__init__() self.signals = WineResolverSignals() self.setAutoDelete(True) self.wine_env = os.environ.copy() self.wine_env.update(core.get_app_environment(app_name)) self.wine_env["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;" self.wine_env["DISPLAY"] = "" self.wine_binary = core.lgd.config.get( app_name, "wine_executable", fallback=core.lgd.config.get("default", "wine_executable", fallback="wine"), ) self.winepath_binary = os.path.join( os.path.dirname(self.wine_binary), "winepath" ) self.path = PathSpec(core, app_name).cook(path) @pyqtSlot() def run(self): if "WINEPREFIX" not in self.wine_env or not os.path.exists( self.wine_env["WINEPREFIX"] ): # pylint: disable=E1136 self.signals.result_ready[str].emit(str()) return if not os.path.exists(self.wine_binary) or not os.path.exists( self.winepath_binary ): # pylint: disable=E1136 self.signals.result_ready[str].emit(str()) return path = self.path.strip().replace("/", "\\") # lk: if path does not exist form cmd = [self.wine_binary, "cmd", "/c", "echo", path] # lk: if path exists and needs a case sensitive interpretation form # cmd = [self.wine_binary, 'cmd', '/c', f'cd {path} & cd'] proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.wine_env, shell=False, text=True, ) out, err = proc.communicate() # Clean wine output out = out.strip().strip('"') proc = subprocess.Popen( [self.winepath_binary, "-u", out], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.wine_env, shell=False, text=True, ) out, err = proc.communicate() real_path = os.path.realpath(out.strip()) # pylint: disable=E1136 self.signals.result_ready[str].emit(real_path) return class CloudSignals(QObject): result_ready = pyqtSignal(list) # List[SaveGameFile] class CloudWorker(QRunnable): def __init__(self): super(CloudWorker, self).__init__() self.signals = CloudSignals() self.setAutoDelete(True) self.core = LegendaryCoreSingleton() def run(self) -> None: try: result = self.core.get_save_games() except HTTPError: result = None self.signals.result_ready.emit(result) def get_raw_save_path(game: Game): if game.supports_cloud_saves: return ( game.metadata.get("customAttributes", {}) .get("CloudSaveFolder", {}) .get("value") ) def get_default_platform(app_name): api_results = ApiResultsSingleton() if platform.system() != "Darwin" or app_name not in api_results.mac_games: return "Windows" else: return "Mac" def icon(icn_str: str, fallback: str = None, **kwargs): try: return qtawesome.icon(icn_str, **kwargs) except Exception as e: if not fallback: logger.warning(f"{e} {icn_str}") if fallback: try: return qtawesome.icon(fallback, **kwargs) except Exception as e: logger.error(str(e)) if kwargs.get("color"): kwargs["color"] = "red" return qtawesome.icon("ei.error", **kwargs)