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 requests.exceptions import HTTPError from legendary.models.game import Game from .models import PathSpec # Windows if platform.system() == "Windows": # noinspection PyUnresolvedReferences from win32com.client import Dispatch # pylint: disable=E0401 from rare import image_dir, shared, 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) 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): 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"): if file.endswith(".scheme"): colors.append(file.replace(".scheme", "")) 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 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"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"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 = 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(icon + ".ico"): img = QImage() img.load(icon + ".png") img.save(icon + ".ico") logger.info("Create Icon") shortcut.IconLocation = os.path.join(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.setAutoDelete(True) self.signals = WineResolverSignals() 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 CloudResultSignal(QObject): result_ready = pyqtSignal(list) # List[SaveGameFile] class CloudWorker(QRunnable): def __init__(self): super(CloudWorker, self).__init__() self.signals = CloudResultSignal() self.setAutoDelete(True) def run(self) -> None: try: result = shared.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): if platform.system() != "Darwin" or app_name not in shared.api_results.mac_games: return "Windows" else: return "Mac"