import json import math import os import platform import shlex 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, QStandardPaths, 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 get_rare_executable() -> List[str]: if platform.system() == "Linux": # TODO flatpak if p := os.environ.get("APPIMAGE"): executable = [p] else: executable = [sys.executable, os.path.abspath(sys.argv[0])] elif platform.system() == "Windows": executable = [sys.executable] if not sys.executable.endswith("Rare.exe"): # be sure to start consoleless then executable[0] = executable[0].replace("python.exe", "pythonw.exe") executable.extend(["-m", "rare"]) else: # macos not tested executable = [sys.executable] return executable def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link="desktop", for_rare: bool = False) -> bool: if not for_rare: 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", "") if platform.system() == "Linux": if type_of_link == "desktop": path = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation) elif type_of_link == "start_menu": path = QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation) else: return False if not os.path.exists(path): return False executable = get_rare_executable() if for_rare: 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" ) else: with open(os.path.join(path, f"{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" ) os.chmod(os.path.join(path, f"{igame.title}.desktop"), 0o755) return True elif platform.system() == "Windows": # Target of shortcut if type_of_link == "desktop": target_folder = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation) elif type_of_link == "start_menu": target_folder = os.path.join( QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation), ".." ) else: logger.warning("No valid type of link") return False if not os.path.exists(target_folder): return False if for_rare: linkName = "Rare.lnk" else: linkName = igame.title # TODO: this conversion is not applied everywhere (see base_installed_widget), should it? for c in r'<>?":|\/*': linkName = 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) executable = get_rare_executable() arguments = [] if len(executable) > 1: arguments.extend(executable[1:]) executable = executable[0] if not sys.executable.endswith("Rare.exe"): # be sure to start consoleless then executable = sys.executable.replace("python.exe", "pythonw.exe") arguments.append(os.path.abspath(sys.argv[0])) if not for_rare: arguments.extend(["launch", app_name]) shortcut.Targetpath = executable shortcut.Arguments = shlex.join(arguments) # Maybe there is a better solution, but windows does not accept single quotes (Windows is weird) shortcut.Arguments = shortcut.Arguments.replace("'", '"') if for_rare: shortcut.WorkingDirectory = QStandardPaths.writableLocation(QStandardPaths.HomeLocation) # Icon if for_rare: icon_location = os.path.join(resources_path, "images", "Rare.ico") else: if not os.path.exists(f"{icon}.ico"): img = QImage() img.load(f"{icon}.png") img.save(f"{icon}.ico") logger.info("Created ico file") icon_location = f"{icon}.ico" shortcut.IconLocation = os.path.abspath(icon_location) shortcut.save() return True # mac OS is based on Darwin 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): super(WineResolver, self).__init__() self.signals = WineResolverSignals() self.setAutoDelete(True) self.wine_env = os.environ.copy() core = LegendaryCoreSingleton() 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)