562 lines
18 KiB
Python
562 lines
18 KiB
Python
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, 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]}"
|
|
shared.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"):
|
|
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 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"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"
|
|
|
|
|
|
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(str(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)
|