1
0
Fork 0
mirror of synced 2024-05-06 13:42:52 +12:00
Rare/rare/utils/misc.py
2022-08-17 23:46:10 +03:00

431 lines
13 KiB
Python

import os
import platform
import shlex
import subprocess
import sys
from logging import getLogger
from typing import List, Union
import qtawesome
import requests
from PyQt5.QtCore import (
pyqtSignal,
pyqtSlot,
QObject,
QRunnable,
QSettings,
QStandardPaths,
QFile,
QDir,
)
from PyQt5.QtGui import QPalette, QColor, QImage
from PyQt5.QtWidgets import qApp, 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")
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:
qApp.setStyle(QStyleFactory.create(qApp.property("rareDefaultQtStyle")))
qApp.setStyleSheet("")
qApp.setPalette(qApp.style().standardPalette())
return
qApp.setStyle(QStyleFactory.create("Fusion"))
custom_palette = load_color_scheme(f":/schemes/{color_scheme}")
if custom_palette is not None:
qApp.setPalette(custom_palette)
icon_color = qApp.palette().color(QPalette.Foreground).name()
qtawesome.set_defaults(color=icon_color)
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:
qApp.setStyle(QStyleFactory.create(qApp.property("rareDefaultQtStyle")))
qApp.setStyleSheet("")
return
qApp.setStyle(QStyleFactory.create("Fusion"))
file = QFile(f":/stylesheets/{style_sheet}/stylesheet.qss")
file.open(QFile.ReadOnly)
stylesheet = file.readAll().data().decode("utf-8")
qApp.setStyleSheet(stylesheet)
icon_color = qApp.palette().color(QPalette.Text).name()
qtawesome.set_defaults(color="#eeeeee")
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: Union[int, float]) -> 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" or platform.system() == "Darwin":
# TODO flatpak
if p := os.environ.get("APPIMAGE"):
executable = [p]
else:
if sys.executable == os.path.abspath(sys.argv[0]):
executable = [sys.executable]
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:
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)
icon = os.path.join(os.path.join(image_dir, igame.app_name, "installed.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
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)