1
0
Fork 0
mirror of synced 2024-06-02 18:54:41 +12:00

Implement image manager

Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com>
This commit is contained in:
loathingKernel 2022-06-18 21:45:36 +03:00
parent 7073a6ad7a
commit 3a28f2f0a2
21 changed files with 652 additions and 296 deletions

View file

@ -8,22 +8,24 @@ import time
import traceback import traceback
from argparse import Namespace from argparse import Namespace
from datetime import datetime from datetime import datetime
from typing import Optional
import legendary
import requests.exceptions import requests.exceptions
from PyQt5.QtCore import Qt, QThreadPool, QSettings, QTranslator, QTimer from PyQt5.QtCore import Qt, QThreadPool, QSettings, QTranslator, QTimer
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox
from requests import HTTPError from requests import HTTPError
import legendary
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import rare.resources.resources import rare.resources.resources
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.utils.paths import cache_dir, resources_path, tmp_dir
from rare.components.dialogs.launch_dialog import LaunchDialog from rare.components.dialogs.launch_dialog import LaunchDialog
from rare.components.main_window import MainWindow from rare.components.main_window import MainWindow
from rare.components.tray_icon import TrayIcon from rare.components.tray_icon import TrayIcon
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.shared.image_manager import ImageManagerSingleton
from rare.utils import legendary_utils, config_helper from rare.utils import legendary_utils, config_helper
from rare.utils.paths import cache_dir, resources_path, tmp_dir
from rare.utils.utils import set_color_pallete, set_style_sheet from rare.utils.utils import set_color_pallete, set_style_sheet
start_time = time.strftime("%y-%m-%d--%H-%M") # year-month-day-hour-minute start_time = time.strftime("%y-%m-%d--%H-%M") # year-month-day-hour-minute
@ -54,8 +56,8 @@ def excepthook(exc_type, exc_value, exc_tb):
class App(QApplication): class App(QApplication):
mainwindow: MainWindow = None mainwindow: Optional[MainWindow] = None
tray_icon: QSystemTrayIcon = None tray_icon: Optional[QSystemTrayIcon] = None
def __init__(self, args: Namespace): def __init__(self, args: Namespace):
super(App, self).__init__(sys.argv) super(App, self).__init__(sys.argv)
@ -63,7 +65,7 @@ class App(QApplication):
self.window_launched = False self.window_launched = False
self.setQuitOnLastWindowClosed(False) self.setQuitOnLastWindowClosed(False)
if hasattr(Qt, 'AA_UseHighDpiPixmaps'): if hasattr(Qt, "AA_UseHighDpiPixmaps"):
self.setAttribute(Qt.AA_UseHighDpiPixmaps) self.setAttribute(Qt.AA_UseHighDpiPixmaps)
# init Legendary # init Legendary
@ -102,6 +104,7 @@ class App(QApplication):
self.settings = QSettings() self.settings = QSettings()
self.signals = GlobalSignalsSingleton(init=True) self.signals = GlobalSignalsSingleton(init=True)
self.image_manager = ImageManagerSingleton(init=True)
self.signals.exit_app.connect(self.exit_app) self.signals.exit_app.connect(self.exit_app)
self.signals.send_notification.connect( self.signals.send_notification.connect(

View file

@ -1,45 +1,77 @@
import os
import platform import platform
from logging import getLogger from logging import getLogger
from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings
from PyQt5.QtWidgets import QDialog, QApplication from PyQt5.QtWidgets import QDialog, QApplication
from legendary.core import LegendaryCore
from requests.exceptions import ConnectionError, HTTPError from requests.exceptions import ConnectionError, HTTPError
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
from rare.components.dialogs.login import LoginDialog from rare.components.dialogs.login import LoginDialog
from rare.models.apiresults import ApiResults
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
from rare.shared.image_manager import ImageManagerSingleton
from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog
from rare.utils.models import ApiResults from rare.utils.utils import CloudWorker
from rare.utils.paths import image_dir
from rare.utils.utils import download_images, CloudWorker
logger = getLogger("Login") logger = getLogger("Login")
class LaunchDialogSignals(QObject): class LaunchWorker(QRunnable):
image_progress = pyqtSignal(int) class Signals(QObject):
result = pyqtSignal(object, str) progress = pyqtSignal(int)
result = pyqtSignal(object, str)
class ImageWorker(QRunnable):
def __init__(self): def __init__(self):
super(ImageWorker, self).__init__() super(LaunchWorker, self).__init__()
self.signals = LaunchDialogSignals()
self.setAutoDelete(True) self.setAutoDelete(True)
self.signals = LaunchWorker.Signals()
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
def run(self): def run(self):
download_images(self.signals.image_progress, self.signals.result, self.core) pass
self.signals.image_progress.emit(100)
class ApiRequestWorker(QRunnable): class ImageWorker(LaunchWorker):
def __init__(self):
super(ImageWorker, self).__init__()
self.image_manager = ImageManagerSingleton()
def run(self):
# Download Images
games, dlcs = self.core.get_game_and_dlc_list(update_assets=True, skip_ue=False)
self.signals.result.emit((games, dlcs), "gamelist")
dlc_list = [dlc[0] for dlc in dlcs.values()]
na_games, na_dlcs = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False)
self.signals.result.emit(na_games, "no_assets")
na_dlc_list = [dlc[0] for dlc in na_dlcs.values()]
game_list = games + dlc_list + na_games + na_dlc_list
fetched = [False] * len(game_list)
def set_fetched(idx):
fetched[idx] = True
self.signals.progress.emit(sum(fetched))
for i, game in enumerate(game_list):
if game.app_title == "Unreal Engine":
game.app_title += f" {game.app_name.split('_')[-1]}"
self.core.lgd.set_game_meta(game.app_name, game)
self.image_manager.download_image_blocking(game)
self.signals.progress.emit(int(i / len(game_list) * 100))
# self.image_manager.download_image(
# game,
# load_callback=lambda: set_fetched(i),
# priority=i
# )
# while not all(fetched):
# continue
self.signals.progress.emit(100)
class ApiRequestWorker(LaunchWorker):
def __init__(self): def __init__(self):
super(ApiRequestWorker, self).__init__() super(ApiRequestWorker, self).__init__()
self.signals = LaunchDialogSignals()
self.setAutoDelete(True)
self.core = LegendaryCoreSingleton()
self.settings = QSettings() self.settings = QSettings()
def run(self) -> None: def run(self) -> None:
@ -106,14 +138,11 @@ class LaunchDialog(QDialog, Ui_LaunchDialog):
self.quit_app.emit(0) self.quit_app.emit(0)
def launch(self): def launch(self):
# self.core = core
if not os.path.exists(image_dir):
os.makedirs(image_dir)
if not self.args.offline: if not self.args.offline:
self.image_info.setText(self.tr("Downloading Images")) self.image_info.setText(self.tr("Downloading Images"))
image_worker = ImageWorker() image_worker = ImageWorker()
image_worker.signals.image_progress.connect(self.update_image_progbar) image_worker.signals.progress.connect(self.update_image_progbar)
image_worker.signals.result.connect(self.handle_api_worker_result) image_worker.signals.result.connect(self.handle_api_worker_result)
self.thread_pool.start(image_worker) self.thread_pool.start(image_worker)
@ -124,9 +153,7 @@ class LaunchDialog(QDialog, Ui_LaunchDialog):
# cloud save from another worker, because it is used in cloud_save_utils too # cloud save from another worker, because it is used in cloud_save_utils too
cloud_worker = CloudWorker() cloud_worker = CloudWorker()
cloud_worker.signals.result_ready.connect( cloud_worker.signals.result_ready.connect(lambda x: self.handle_api_worker_result(x, "saves"))
lambda x: self.handle_api_worker_result(x, "saves")
)
self.thread_pool.start(cloud_worker) self.thread_pool.start(cloud_worker)
else: else:
@ -159,13 +186,9 @@ class LaunchDialog(QDialog, Ui_LaunchDialog):
self.api_results.dlcs, self.api_results.dlcs,
) = self.core.get_game_and_dlc_list(False) ) = self.core.get_game_and_dlc_list(False)
elif text == "32bit": elif text == "32bit":
self.api_results.bit32_games = ( self.api_results.bit32_games = [i.app_name for i in result[0]] if result else []
[i.app_name for i in result[0]] if result else []
)
elif text == "mac": elif text == "mac":
self.api_results.mac_games = ( self.api_results.mac_games = [i.app_name for i in result[0]] if result else []
[i.app_name for i in result[0]] if result else []
)
elif text == "no_assets": elif text == "no_assets":
self.api_results.no_asset_games = result if result else [] self.api_results.no_asset_games = result if result else []

View file

@ -3,12 +3,17 @@ from typing import Tuple, Dict, Union, List
from PyQt5.QtCore import QSettings, QObjectCleanupHandler from PyQt5.QtCore import QSettings, QObjectCleanupHandler
from PyQt5.QtWidgets import QStackedWidget, QVBoxLayout, QWidget from PyQt5.QtWidgets import QStackedWidget, QVBoxLayout, QWidget
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton, ApiResultsSingleton
from legendary.models.game import InstalledGame, Game from legendary.models.game import InstalledGame, Game
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
ApiResultsSingleton,
)
from rare.shared.image_manager import ImageManagerSingleton
from rare.ui.components.tabs.games.games_tab import Ui_GamesTab from rare.ui.components.tabs.games.games_tab import Ui_GamesTab
from rare.utils.extra_widgets import FlowLayout from rare.utils.extra_widgets import FlowLayout
from rare.utils.utils import get_pixmap, download_image, get_uninstalled_pixmap
from .cloud_save_utils import CloudSaveUtils from .cloud_save_utils import CloudSaveUtils
from .cloud_save_utils import CloudSaveUtils from .cloud_save_utils import CloudSaveUtils
from .game_info import GameInfoTabs from .game_info import GameInfoTabs
@ -41,6 +46,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
self.signals = GlobalSignalsSingleton() self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton() self.args = ArgumentsSingleton()
self.api_results = ApiResultsSingleton() self.api_results = ApiResultsSingleton()
self.image_manager = ImageManagerSingleton()
self.settings = QSettings() self.settings = QSettings()
self.game_list: List[Game] = self.api_results.game_list self.game_list: List[Game] = self.api_results.game_list
@ -71,7 +77,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
for i in self.game_list: for i in self.game_list:
if i.app_name.startswith("UE_4"): if i.app_name.startswith("UE_4"):
pixmap = get_pixmap(i.app_name) pixmap = self.image_manager.get_pixmap(i.app_name)
if pixmap.isNull(): if pixmap.isNull():
continue continue
self.ue_name = i.app_name self.ue_name = i.app_name
@ -216,15 +222,15 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
) )
def add_installed_widget(self, app_name): def add_installed_widget(self, app_name):
pixmap = get_pixmap(app_name) pixmap = self.image_manager.get_pixmap(app_name)
try: try:
if pixmap.isNull(): if pixmap.isNull():
logger.info(f"{app_name} has a corrupt image.") logger.info(f"{app_name} has a corrupt image.")
if app_name in self.no_asset_names and self.core.get_asset(app_name).namespace != "ue": if app_name in self.no_asset_names and self.core.get_asset(app_name).namespace != "ue":
download_image(self.core.get_game(app_name), force=True) self.image_manager.download_image_blocking(self.core.get_game(app_name), force=True)
pixmap = get_pixmap(app_name) pixmap = self.image_manager.get_pixmap(app_name)
elif self.ue_name: elif self.ue_name:
pixmap = get_pixmap(self.ue_name) pixmap = self.image_manager.get_pixmap(self.ue_name)
icon_widget = InstalledIconWidget(app_name, pixmap, self.game_utils) icon_widget = InstalledIconWidget(app_name, pixmap, self.game_utils)
list_widget = InstalledListWidget(app_name, pixmap, self.game_utils) list_widget = InstalledListWidget(app_name, pixmap, self.game_utils)
@ -242,15 +248,15 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
return icon_widget, list_widget return icon_widget, list_widget
def add_uninstalled_widget(self, game): def add_uninstalled_widget(self, game):
pixmap = get_uninstalled_pixmap(game.app_name) pixmap = self.image_manager.get_pixmap(game.app_name, color=False)
try: try:
if pixmap.isNull(): if pixmap.isNull():
if self.core.get_asset(game.app_name).namespace != "ue": if self.core.get_asset(game.app_name).namespace != "ue":
logger.warning(f"{game.app_title} has a corrupt image. Reloading...") logger.warning(f"{game.app_title} has a corrupt image. Reloading...")
download_image(game, force=True) self.image_manager.download_image_blocking(game, force=True)
pixmap = get_uninstalled_pixmap(game.app_name) pixmap = self.image_manager.get_pixmap(game.app_name, color=False)
elif self.ue_name: elif self.ue_name:
pixmap = get_uninstalled_pixmap(self.ue_name) pixmap = self.image_manager.get_pixmap(self.ue_name, color=False)
icon_widget = IconWidgetUninstalled(game, self.core, pixmap) icon_widget = IconWidgetUninstalled(game, self.core, pixmap)
list_widget = ListWidgetUninstalled(self.core, game, pixmap) list_widget = ListWidgetUninstalled(self.core, game, pixmap)

View file

@ -1,14 +1,14 @@
from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QResizeEvent from PyQt5.QtGui import QPixmap, QResizeEvent
from PyQt5.QtWidgets import QFrame, QWidget, QMessageBox from PyQt5.QtWidgets import QFrame, QWidget, QMessageBox
from legendary.models.game import Game from legendary.models.game import Game
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.components.tabs.games.game_utils import GameUtils from rare.components.tabs.games.game_utils import GameUtils
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc from rare.ui.components.tabs.games.game_info.game_dlc import Ui_GameDlc
from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget from rare.ui.components.tabs.games.game_info.game_dlc_widget import Ui_GameDlcWidget
from rare.utils.models import InstallOptionsModel from rare.utils.models import InstallOptionsModel
from rare.utils.utils import get_pixmap
class GameDlc(QWidget, Ui_GameDlc): class GameDlc(QWidget, Ui_GameDlc):
@ -90,14 +90,17 @@ class GameDlcWidget(QFrame, Ui_GameDlcWidget):
def __init__(self, dlc: Game, installed: bool, parent=None): def __init__(self, dlc: Game, installed: bool, parent=None):
super(GameDlcWidget, self).__init__(parent=parent) super(GameDlcWidget, self).__init__(parent=parent)
self.image_manager = ImageManagerSingleton()
self.setupUi(self) self.setupUi(self)
self.dlc = dlc self.dlc = dlc
self.image.setFixedSize(ImageSize.Smaller.size)
self.dlc_name.setText(dlc.app_title) self.dlc_name.setText(dlc.app_title)
self.version.setText(dlc.app_version()) self.version.setText(dlc.app_version())
self.app_name.setText(dlc.app_name) self.app_name.setText(dlc.app_name)
self.pixmap = get_pixmap(dlc.app_name) self.pixmap = self.image_manager.get_pixmap(dlc.app_name)
if installed: if installed:
self.action_button.setProperty("uninstall", 1) self.action_button.setProperty("uninstall", 1)

View file

@ -1,8 +1,8 @@
import os import os
import platform import platform
import shutil import shutil
from pathlib import Path
from logging import getLogger from logging import getLogger
from pathlib import Path
from typing import Tuple from typing import Tuple
from PyQt5.QtCore import ( from PyQt5.QtCore import (
@ -25,20 +25,21 @@ from PyQt5.QtWidgets import (
QMessageBox, QMessageBox,
QWidgetAction, QWidgetAction,
) )
from legendary.models.game import Game, InstalledGame, VerifyResult from legendary.models.game import Game, InstalledGame, VerifyResult
from rare.legendary.legendary.utils.lfs import validate_files from legendary.utils.lfs import validate_files
from rare.shared import ( from rare.shared import (
LegendaryCoreSingleton, LegendaryCoreSingleton,
GlobalSignalsSingleton, GlobalSignalsSingleton,
ArgumentsSingleton, ArgumentsSingleton,
) )
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.extra_widgets import PathEdit from rare.utils.extra_widgets import PathEdit
from rare.utils.legendary_utils import VerifyWorker from rare.utils.legendary_utils import VerifyWorker
from rare.utils.models import InstallOptionsModel from rare.utils.models import InstallOptionsModel
from rare.utils.steam_grades import SteamWorker from rare.utils.steam_grades import SteamWorker
from rare.utils.utils import get_size, get_pixmap from rare.utils.utils import get_size
logger = getLogger("GameInfo") logger = getLogger("GameInfo")
@ -56,6 +57,7 @@ class GameInfo(QWidget, Ui_GameInfo):
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton() self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton() self.args = ArgumentsSingleton()
self.image_manager = ImageManagerSingleton()
self.game_utils = game_utils self.game_utils = game_utils
if platform.system() == "Windows": if platform.system() == "Windows":
@ -281,11 +283,10 @@ class GameInfo(QWidget, Ui_GameInfo):
self.igame = self.core.get_installed_game(self.game.app_name) self.igame = self.core.get_installed_game(self.game.app_name)
self.title.setTitle(self.game.app_title) self.title.setTitle(self.game.app_title)
pixmap = get_pixmap(self.game.app_name) pixmap = self.image_manager.get_pixmap(self.game.app_name)
if pixmap.isNull(): if pixmap.isNull():
pixmap = get_pixmap(self.parent().parent().parent().ue_name) pixmap = self.image_manager.get_pixmap(self.parent().parent().parent().ue_name)
w = 200 pixmap = pixmap.scaled(ImageSize.Display.size)
pixmap = pixmap.scaled(w, int(w * 4 / 3))
self.image.setPixmap(pixmap) self.image.setPixmap(pixmap)
self.app_name.setText(self.game.app_name) self.app_name.setText(self.game.app_name)

View file

@ -3,15 +3,20 @@ import platform
from PyQt5.QtCore import Qt, QThreadPool from PyQt5.QtCore import Qt, QThreadPool
from PyQt5.QtGui import QKeyEvent from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QWidget, QTreeView from PyQt5.QtWidgets import QWidget, QTreeView
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton, ApiResultsSingleton
from legendary.models.game import Game from legendary.models.game import Game
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
ApiResultsSingleton,
)
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.extra_widgets import SideTabWidget from rare.utils.extra_widgets import SideTabWidget
from rare.utils.json_formatter import QJsonModel from rare.utils.json_formatter import QJsonModel
from rare.utils.models import InstallOptionsModel from rare.utils.models import InstallOptionsModel
from rare.utils.steam_grades import SteamWorker from rare.utils.steam_grades import SteamWorker
from rare.utils.utils import get_pixmap
class UninstalledInfoTabs(SideTabWidget): class UninstalledInfoTabs(SideTabWidget):
@ -71,6 +76,8 @@ class UninstalledInfo(QWidget, Ui_GameInfo):
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton() self.signals = GlobalSignalsSingleton()
self.api_results = ApiResultsSingleton() self.api_results = ApiResultsSingleton()
self.image_manager = ImageManagerSingleton()
self.install_button.clicked.connect(self.install_game) self.install_button.clicked.connect(self.install_game)
if platform.system() != "Windows": if platform.system() != "Windows":
self.steam_worker = SteamWorker(self.core) self.steam_worker = SteamWorker(self.core)
@ -104,11 +111,10 @@ class UninstalledInfo(QWidget, Ui_GameInfo):
available_platforms.append("macOS") available_platforms.append("macOS")
self.platform.setText(", ".join(available_platforms)) self.platform.setText(", ".join(available_platforms))
pixmap = get_pixmap(game.app_name) pixmap = self.image_manager.get_pixmap(game.app_name, color=False)
if pixmap.isNull(): if pixmap.isNull():
pixmap = get_pixmap(self.ue_default_name) pixmap = self.image_manager.get_pixmap(self.ue_default_name, color=False)
w = 200 pixmap = pixmap.scaled(ImageSize.Display.size)
pixmap = pixmap.scaled(w, int(w * 4 / 3))
self.image.setPixmap(pixmap) self.image.setPixmap(pixmap)
self.app_name.setText(self.game.app_name) self.app_name.setText(self.game.app_name)

View file

@ -6,9 +6,9 @@ from PyQt5.QtCore import pyqtSignal, QProcess, QSettings, QStandardPaths, Qt, QB
from PyQt5.QtGui import QPixmap from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QGroupBox, QMessageBox, QAction, QLabel from PyQt5.QtWidgets import QGroupBox, QMessageBox, QAction, QLabel
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.components.tabs.games.game_utils import GameUtils from rare.components.tabs.games.game_utils import GameUtils
from rare.utils import utils from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.utils.utils import create_desktop_link from rare.utils.utils import create_desktop_link
logger = getLogger("Game") logger = getLogger("Game")
@ -25,6 +25,7 @@ class BaseInstalledWidget(QGroupBox):
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton() self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton() self.args = ArgumentsSingleton()
self.image_manager = ImageManagerSingleton()
self.game_utils = game_utils self.game_utils = game_utils
self.syncing_cloud_saves = False self.syncing_cloud_saves = False
@ -63,8 +64,9 @@ class BaseInstalledWidget(QGroupBox):
pass pass
self.image = QLabel() self.image = QLabel()
self.image.setFixedSize(ImageSize.Display.size)
self.image.setPixmap( self.image.setPixmap(
pixmap.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation) pixmap.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation)
) )
self.game_running = False self.game_running = False
self.offline = self.args.offline self.offline = self.args.offline
@ -140,11 +142,9 @@ class BaseInstalledWidget(QGroupBox):
) )
def reload_image(self): def reload_image(self):
utils.download_image(self.game, True) self.image_manager.download_image_blocking(self.game, force=True)
pm = utils.get_pixmap(self.game.app_name) pm = self.image_manager.get_pixmap(self.game.app_name, color=True)
self.image.setPixmap( self.image.setPixmap(pm.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation))
pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
)
def create_desktop_link(self, type_of_link): def create_desktop_link(self, type_of_link):
if type_of_link == "desktop": if type_of_link == "desktop":

View file

@ -2,9 +2,9 @@ from logging import getLogger
from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import QGroupBox, QLabel, QAction from PyQt5.QtWidgets import QGroupBox, QLabel, QAction
from legendary.models.game import Game from legendary.models.game import Game
from rare.utils import utils
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
logger = getLogger("Uninstalled") logger = getLogger("Uninstalled")
@ -14,15 +14,16 @@ class BaseUninstalledWidget(QGroupBox):
def __init__(self, game, core, pixmap): def __init__(self, game, core, pixmap):
super(BaseUninstalledWidget, self).__init__() super(BaseUninstalledWidget, self).__init__()
self.image_manager = ImageManagerSingleton()
self.game = game self.game = game
if self.game.app_title == "Unreal Engine": if self.game.app_title == "Unreal Engine":
self.game.app_title = f"{self.game.app_title} {self.game.app_name.split('_')[-1]}" self.game.app_title = f"{self.game.app_title} {self.game.app_name.split('_')[-1]}"
self.core = core self.core = core
self.image = QLabel() self.image = QLabel()
self.image.setPixmap( self.image.setFixedSize(ImageSize.Display.size)
pixmap.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation) self.image.setPixmap(pixmap.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation))
)
self.installing = False self.installing = False
self.setContextMenuPolicy(Qt.ActionsContextMenu) self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
@ -32,11 +33,9 @@ class BaseUninstalledWidget(QGroupBox):
self.addAction(reload_image) self.addAction(reload_image)
def reload_image(self): def reload_image(self):
utils.download_image(self.game, True) self.image_manager.download_image_blocking(self.game, force=True)
pm = utils.get_uninstalled_pixmap(self.game.app_name) pm = self.image_manager.get_pixmap(self.game.app_name, color=False)
self.image.setPixmap( self.image.setPixmap(pm.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation))
pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
)
def install(self): def install(self):
self.show_uninstalled_info.emit(self.game) self.show_uninstalled_info.emit(self.game)

View file

@ -1,14 +1,16 @@
from logging import getLogger from logging import getLogger
from PyQt5.QtCore import QEvent, pyqtSignal, QSize, Qt from PyQt5.QtCore import QEvent, pyqtSignal, Qt
from PyQt5.QtGui import QMouseEvent from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QLabel from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QWidget
from rare.shared import LegendaryCoreSingleton
from rare.components.tabs.games.game_widgets.base_installed_widget import ( from rare.components.tabs.games.game_widgets.base_installed_widget import (
BaseInstalledWidget, BaseInstalledWidget,
) )
from rare.shared import LegendaryCoreSingleton
from rare.shared.image_manager import ImageSize
from rare.utils.utils import icon from rare.utils.utils import icon
from rare.widgets.elide_label import ElideLabel
logger = getLogger("GameWidgetInstalled") logger = getLogger("GameWidgetInstalled")
@ -21,55 +23,54 @@ class InstalledIconWidget(BaseInstalledWidget):
self.setObjectName("game_widget_icon") self.setObjectName("game_widget_icon")
self.setContextMenuPolicy(Qt.ActionsContextMenu) self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.layout = QVBoxLayout() layout = QVBoxLayout()
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
if self.update_available: if self.update_available:
logger.info(f"Update available for game: {self.game.app_name}") logger.info(f"Update available for game: {self.game.app_name}")
self.layout.addWidget(self.image) layout.addWidget(self.image)
self.game_utils.finished.connect(self.game_finished) self.game_utils.finished.connect(self.game_finished)
self.game_utils.cloud_save_finished.connect(self.sync_finished) self.game_utils.cloud_save_finished.connect(self.sync_finished)
self.title_label = QLabel(f"<h4>{self.game.app_title}</h4>") miniwidget = QWidget(self)
self.title_label.setAutoFillBackground(False) miniwidget.setFixedWidth(ImageSize.Display.size.width())
self.title_label.setWordWrap(True)
self.title_label.setFixedWidth(175)
minilayout = QHBoxLayout() minilayout = QHBoxLayout()
minilayout.setContentsMargins(0, 0, 0, 0)
minilayout.setSpacing(0)
miniwidget.setLayout(minilayout)
self.title_label = ElideLabel(f"<h4>{self.game.app_title}</h4>", parent=miniwidget)
self.title_label.setObjectName("game_widget") self.title_label.setObjectName("game_widget")
minilayout.addWidget(self.title_label) minilayout.addWidget(self.title_label, stretch=2)
# Info Button # Info Button
self.menu_btn = QPushButton() self.menu_btn = QPushButton(parent=miniwidget)
self.menu_btn.setIcon(icon("ei.info-circle")) self.menu_btn.setIcon(icon("ei.info-circle"))
# self.menu_btn.setObjectName("installed_menu_button") # self.menu_btn.setObjectName("installed_menu_button")
self.menu_btn.setIconSize(QSize(18, 18)) self.menu_btn.enterEvent = lambda x: self.info_label.setText(self.tr("Information"))
self.menu_btn.enterEvent = lambda x: self.info_label.setText(
self.tr("Information")
)
self.menu_btn.leaveEvent = lambda x: self.enterEvent(None) self.menu_btn.leaveEvent = lambda x: self.enterEvent(None)
# remove Border # remove Border
self.menu_btn.setObjectName("menu_button") self.menu_btn.setObjectName("menu_button")
self.menu_btn.clicked.connect(lambda: self.show_info.emit(self.game.app_name)) self.menu_btn.clicked.connect(lambda: self.show_info.emit(self.game.app_name))
self.menu_btn.setFixedWidth(17) self.menu_btn.setFixedSize(22, 22)
minilayout.addWidget(self.menu_btn) minilayout.addWidget(self.menu_btn, stretch=0)
minilayout.addStretch(1) minilayout.setAlignment(Qt.AlignTop)
self.layout.addLayout(minilayout) layout.addWidget(miniwidget)
self.info_label = QLabel("") self.info_label = ElideLabel(" ", parent=self)
self.info_label.setFixedWidth(ImageSize.Display.size.width())
self.leaveEvent(None) self.leaveEvent(None)
self.info_label.setAutoFillBackground(False)
self.info_label.setObjectName("info_label") self.info_label.setObjectName("info_label")
self.layout.addWidget(self.info_label) layout.addWidget(self.info_label)
if self.igame and self.igame.needs_verification: if self.igame and self.igame.needs_verification:
self.info_label.setText(self.texts["needs_verification"]) self.info_label.setText(self.texts["needs_verification"])
self.setLayout(self.layout) self.setLayout(layout)
self.setFixedWidth(self.sizeHint().width())
self.game_utils.game_launched.connect(self.game_started) self.game_utils.game_launched.connect(self.game_started)
@ -99,7 +100,7 @@ class InstalledIconWidget(BaseInstalledWidget):
elif self.igame and self.igame.needs_verification: elif self.igame and self.igame.needs_verification:
self.info_label.setText(self.texts["needs_verification"]) self.info_label.setText(self.texts["needs_verification"])
else: else:
self.info_label.setText("") self.info_label.setText(" ") # invisible text, cheap way to always vertical have size in label
def mousePressEvent(self, e: QMouseEvent): def mousePressEvent(self, e: QMouseEvent):
# left button # left button

View file

@ -1,14 +1,13 @@
from PyQt5.QtCore import Qt, QRect from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPaintEvent, QPainter, QPixmap, QPen, QFont, QColor from PyQt5.QtGui import QPaintEvent, QPainter, QPixmap, QPen, QFont, QColor
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QHBoxLayout, QWidget from PyQt5.QtWidgets import QVBoxLayout, QLabel, QHBoxLayout, QWidget
from legendary.models.game import Game from legendary.models.game import Game
from rare.shared import LegendaryCoreSingleton from rare.shared import LegendaryCoreSingleton
from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.utils.utils import ( from rare.utils.utils import (
get_pixmap,
optimal_text_background, optimal_text_background,
text_color_for_background, text_color_for_background,
get_uninstalled_pixmap,
) )
@ -24,11 +23,9 @@ class InstallingGameWidget(QWidget):
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.pixmap = QPixmap() self.pixmap = QPixmap()
w = 200
# self.pixmap = self.pixmap.scaled(w, int(w * 4 / 3), transformMode=Qt.SmoothTransformation)
self.image_widget = PaintWidget() self.image_widget = PaintWidget()
self.setContentsMargins(4, 4, 4, 4) self.setContentsMargins(0, 0, 0, 0)
self.image_widget.setFixedSize(w, int(w * 4 / 3)) self.image_widget.setFixedSize(ImageSize.Display.size)
self.layout().addWidget(self.image_widget) self.layout().addWidget(self.image_widget)
self.title_label = QLabel(f"<h4>Error</h4>") self.title_label = QLabel(f"<h4>Error</h4>")
@ -62,18 +59,18 @@ class PaintWidget(QWidget):
def __init__(self): def __init__(self):
super(PaintWidget, self).__init__() super(PaintWidget, self).__init__()
self.core = LegendaryCoreSingleton() self.core = LegendaryCoreSingleton()
self.image_manager = ImageManagerSingleton()
def set_game(self, app_name: str): def set_game(self, app_name: str):
game = self.core.get_game(app_name, False) game = self.core.get_game(app_name, False)
self.color_image = get_pixmap(game.app_name) self.color_image = self.image_manager.get_pixmap(game.app_name, color=False)
w = 200
self.color_image = self.color_image.scaled( self.color_image = self.color_image.scaled(
w, int(w * 4 // 3), transformMode=Qt.SmoothTransformation ImageSize.Display.size, transformMode=Qt.SmoothTransformation
) )
self.setFixedSize(self.color_image.size()) self.setFixedSize(ImageSize.Display.size)
self.bw_image = get_uninstalled_pixmap(app_name) self.bw_image = self.image_manager.get_pixmap(app_name, color=False)
self.bw_image = self.bw_image.scaled( self.bw_image = self.bw_image.scaled(
w, int(w * 4 // 3), transformMode=Qt.SmoothTransformation ImageSize.Display.size, transformMode=Qt.SmoothTransformation
) )
self.progress = 0 self.progress = 0

View file

@ -1,12 +1,15 @@
from logging import getLogger from logging import getLogger
from PyQt5.QtWidgets import QVBoxLayout, QLabel from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QVBoxLayout, QWidget, QHBoxLayout
from legendary.core import LegendaryCore from legendary.core import LegendaryCore
from legendary.models.game import Game from legendary.models.game import Game
from rare.components.tabs.games.game_widgets.base_uninstalled_widget import ( from rare.components.tabs.games.game_widgets.base_uninstalled_widget import (
BaseUninstalledWidget, BaseUninstalledWidget,
) )
from rare.shared.image_manager import ImageSize
from rare.widgets.elide_label import ElideLabel
logger = getLogger("Uninstalled") logger = getLogger("Uninstalled")
@ -14,19 +17,31 @@ logger = getLogger("Uninstalled")
class IconWidgetUninstalled(BaseUninstalledWidget): class IconWidgetUninstalled(BaseUninstalledWidget):
def __init__(self, game: Game, core: LegendaryCore, pixmap): def __init__(self, game: Game, core: LegendaryCore, pixmap):
super(IconWidgetUninstalled, self).__init__(game, core, pixmap) super(IconWidgetUninstalled, self).__init__(game, core, pixmap)
self.layout = QVBoxLayout() layout = QVBoxLayout()
self.setObjectName("game_widget_icon") self.setObjectName("game_widget_icon")
self.layout.addWidget(self.image) layout.addWidget(self.image)
self.title_label = QLabel(f"<h3>{game.app_title}</h3>") miniwidget = QWidget(self)
self.title_label.setWordWrap(True) miniwidget.setFixedWidth(ImageSize.Display.size.width())
self.layout.addWidget(self.title_label) minilayout = QHBoxLayout()
minilayout.setContentsMargins(0, 0, 0, 0)
minilayout.setSpacing(0)
miniwidget.setLayout(minilayout)
self.info_label = QLabel("") self.title_label = ElideLabel(f"<h4>{game.app_title}</h4>", parent=miniwidget)
self.layout.addWidget(self.info_label) self.title_label.setObjectName("game_widget")
minilayout.addWidget(self.title_label, stretch=2)
self.setLayout(self.layout) minilayout.setAlignment(Qt.AlignTop)
self.setFixedWidth(self.sizeHint().width()) layout.addWidget(miniwidget)
self.info_label = ElideLabel(" ", parent=self)
self.info_label.setFixedWidth(ImageSize.Display.size.width())
self.leaveEvent(None)
self.info_label.setObjectName("info_label")
layout.addWidget(self.info_label)
self.setLayout(layout)
def mousePressEvent(self, e) -> None: def mousePressEvent(self, e) -> None:
# left button # left button
@ -47,4 +62,4 @@ class IconWidgetUninstalled(BaseUninstalledWidget):
if self.installing: if self.installing:
self.info_label.setText("Installation...") self.info_label.setText("Installation...")
else: else:
self.info_label.setText("") self.info_label.setText(" ") # invisible text, cheap way to always have vertical size in label

3
rare/models/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
Models and data structures module for Rare
"""

22
rare/models/apiresults.py Normal file
View file

@ -0,0 +1,22 @@
from dataclasses import dataclass
from typing import Optional, List, Dict
@dataclass
class ApiResults:
game_list: Optional[List] = None
dlcs: Optional[Dict] = None
bit32_games: Optional[List] = None
mac_games: Optional[List] = None
no_asset_games: Optional[List] = None
saves: Optional[List] = None
def __bool__(self):
return (
self.game_list is not None
and self.dlcs is not None
and self.bit32_games is not None
and self.mac_games is not None
and self.no_asset_games is not None
and self.saves is not None
)

29
rare/models/signals.py Normal file
View file

@ -0,0 +1,29 @@
from PyQt5.QtCore import QObject, pyqtSignal
from rare.utils.models import InstallOptionsModel
class GlobalSignals(QObject):
exit_app = pyqtSignal(int) # exit code
send_notification = pyqtSignal(str) # app_title
set_main_tab_index = pyqtSignal(int) # tab index
update_download_tab_text = pyqtSignal()
dl_progress = pyqtSignal(int) # 0-100
# set visibility of installing widget in games tab
installation_started = pyqtSignal(str) # app_name
add_download = pyqtSignal(str)
install_game = pyqtSignal(InstallOptionsModel)
installation_finished = pyqtSignal(bool, str)
overlay_installation_finished = pyqtSignal()
update_gamelist = pyqtSignal(list)
game_uninstalled = pyqtSignal(str) # appname
set_discord_rpc = pyqtSignal(str) # app_name of running game
rpc_settings_updated = pyqtSignal()
wine_prefix_updated = pyqtSignal()

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

View file

@ -1,9 +1,17 @@
"""
Shared controller resources module
Each of the objects in this module should be instantiated ONCE
and only ONCE!
"""
from argparse import Namespace from argparse import Namespace
from typing import Optional from typing import Optional
from legendary.core import LegendaryCore from legendary.core import LegendaryCore
from rare.utils.models import ApiResults, GlobalSignals from rare.models.apiresults import ApiResults
from rare.models.signals import GlobalSignals
_legendary_core_singleton: Optional[LegendaryCore] = None _legendary_core_singleton: Optional[LegendaryCore] = None
_global_signals_singleton: Optional[GlobalSignals] = None _global_signals_singleton: Optional[GlobalSignals] = None

View file

@ -0,0 +1,367 @@
from __future__ import annotations
import hashlib
import json
import pickle
import zlib
from logging import getLogger
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Tuple, Dict, Union, Type, List, Callable, Optional
import requests
from PyQt5.QtCore import (
Qt,
pyqtSignal,
QObject,
QSize,
QThreadPool,
QRunnable,
)
from PyQt5.QtGui import (
QPixmap,
QImage,
QPainter,
)
from PyQt5.QtWidgets import QApplication
from legendary.models.game import Game
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
from rare.utils.paths import image_dir, resources_path
if TYPE_CHECKING:
pass
logger = getLogger("ImageManager")
class ImageSize:
class Preset:
__img_factor = 67
__size: QSize
__divisor: float = 1.0
__pixel_ratio: float = 1.0
# lk: for prettier images set this to true
__smooth_transform: bool = False
def __init__(self, divisor: float, pixel_ratio: float):
self.__pixel_ratio = pixel_ratio
self.__divisor = divisor
self.__size = QSize(self.__img_factor * 3, self.__img_factor * 4) * pixel_ratio / divisor
if divisor > 2:
self.__smooth_transform = False
@property
def size(self) -> QSize:
return self.__size
@property
def divisor(self) -> float:
return self.__divisor
@property
def smooth(self) -> bool:
return self.__smooth_transform
@property
def pixel_ratio(self) -> float:
return self.__pixel_ratio
Image = Preset(1, 2)
"""! @brief Size and pixel ratio of the image on disk"""
Display = Preset(1, 1)
"""! @brief Size and pixel ratio for displaying"""
Normal = Display
"""! @brief Same as Display"""
Small = Preset(3, 1)
"""! @brief Small image size for displaying"""
Smaller = Preset(4, 1)
"""! @brief Smaller image size for displaying"""
Icon = Preset(5, 1)
"""! @brief Smaller image size for UI icons"""
class ImageManager(QObject):
class Worker(QRunnable):
class Signals(QObject):
# str: app_name
completed = pyqtSignal(str)
def __init__(self, func: Callable, updates: List, json_data: Dict, game: Game):
super(ImageManager.Worker, self).__init__()
self.signals = ImageManager.Worker.Signals()
self.setAutoDelete(True)
self.func = func
self.updates = updates
self.json_data = json_data
self.game = game
def run(self):
self.func(self.updates, self.json_data, self.game)
logger.debug(f" Emitting singal for game {self.game.app_name} - {self.game.app_title}")
self.signals.completed.emit(self.game.app_name)
# lk: the ordering in __img_types matters for the order of fallbacks
__img_types: List = ["DieselGameBoxTall", "Thumbnail", "DieselGameBoxLogo"]
__dl_retries = 1
__worker_app_names: List[str] = list()
def __init__(self):
super(QObject, self).__init__()
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.image_dir = Path(image_dir)
if not self.image_dir.is_dir():
self.image_dir.mkdir()
logger.info(f"Created image directory at {self.image_dir}")
self.device = ImageSize.Preset(1, QApplication.instance().devicePixelRatio())
self.threadpool = QThreadPool()
self.threadpool.setMaxThreadCount(8)
def __img_dir(self, app_name: str) -> Path:
return self.image_dir.joinpath(app_name)
def __img_json(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("image.json")
def __img_cache(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("image.cache")
def __img_color(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("installed.png")
def __img_gray(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("uninstalled.png")
def __prepare_download(self, game: Game, force: bool = False) -> Tuple[List, Dict]:
if force and self.__img_dir(game.app_name).exists():
self.__img_color(game.app_name).unlink(missing_ok=True)
self.__img_color(game.app_name).unlink(missing_ok=True)
if not self.__img_dir(game.app_name).is_dir():
self.__img_dir(game.app_name).mkdir()
# Load image checksums
if not self.__img_json(game.app_name).is_file():
json_data: Dict = dict(zip(self.__img_types, [None] * len(self.__img_types)))
else:
json_data = json.load(open(self.__img_json(game.app_name), "r"))
# lk: fast path for games without images, convert Rare's logo
if not game.metadata["keyImages"]:
if not self.__img_color(game.app_name).is_file() or not self.__img_gray(game.app_name).is_file():
cache_data: Dict = dict(zip(self.__img_types, [None] * len(self.__img_types)))
cache_data["DieselGameBoxTall"] = open(
resources_path.joinpath("images", "cover.png"), "rb"
).read()
# cache_data["DieselGameBoxLogo"] = open(
# resources_path.joinpath("images", "Rare_nonsquared.png"), "rb").read()
self.__convert(game, cache_data)
json_data["cache"] = None
json_data["scale"] = ImageSize.Image.pixel_ratio
json_data["size"] = ImageSize.Image.size.__str__()
json.dump(json_data, open(self.__img_json(game.app_name), "w"))
# lk: Find updates or initialize if images are missing.
# lk: `updates` will be empty for games without images
# lk: so everything below it is skipped
if not self.__img_color(game.app_name).is_file() or not self.__img_gray(game.app_name).is_file():
updates = [image for image in game.metadata["keyImages"] if image["type"] in self.__img_types]
else:
updates = list()
for image in game.metadata["keyImages"]:
if image["type"] in self.__img_types:
if json_data[image["type"]] != image["md5"]:
updates.append(image)
return updates, json_data
def __download(self, updates, json_data, game) -> bool:
# Decompress existing image.cache
if not self.__img_cache(game.app_name).is_file():
cache_data = dict(zip(self.__img_types, [None] * len(self.__img_types)))
else:
cache_data = self.__decompress(game)
# lk: filter updates again against the cache now that it is available
updates = [
image
for image in updates
if cache_data[image["type"]] is None or json_data[image["type"]] != image["md5"]
]
for image in updates:
logger.info(f"Downloading {image['type']} for {game.app_title}")
json_data[image["type"]] = image["md5"]
payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
cache_data[image["type"]] = requests.get(image["url"], params=payload).content
self.__convert(game, cache_data)
# lk: don't keep the cache if there is no logo (kept for me)
# if cache_data["DieselGameBoxLogo"] is not None:
# self.__compress(game, cache_data)
self.__compress(game, cache_data)
# hash image cache
try:
with open(self.__img_cache(game.app_name), "rb") as archive:
archive_hash = hashlib.md5(archive.read()).hexdigest()
except FileNotFoundError:
archive_hash = None
json_data["cache"] = archive_hash
json_data["scale"] = ImageSize.Image.pixel_ratio
json_data["size"] = {"w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
# write image.json
with open(self.__img_json(game.app_name), "w") as file:
json.dump(json_data, file)
return bool(updates)
def __convert(self, game, images, force=False) -> None:
for image in [self.__img_color(game.app_name), self.__img_gray(game.app_name)]:
if force and image.exists():
image.unlink(missing_ok=True)
cover_data = None
for image_type in self.__img_types:
if images[image_type] is not None:
cover_data = images[image_type]
break
cover = QImage()
cover.loadFromData(cover_data)
cover.convertToFormat(QImage.Format_ARGB32_Premultiplied)
# lk: Images are not always 4/3, crop them to size
factor = min(cover.width() // 3, cover.height() // 4)
rem_w = (cover.width() - factor * 3) // 2
rem_h = (cover.height() - factor * 4) // 2
cover = cover.copy(rem_w, rem_h, factor * 3, factor * 4)
if images["DieselGameBoxLogo"] is not None:
logo = QImage()
logo.loadFromData(images["DieselGameBoxLogo"])
logo.convertToFormat(QImage.Format_ARGB32_Premultiplied)
if logo.width() > cover.width():
logo = logo.scaled(cover.width(), cover.height(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
painter = QPainter(cover)
painter.drawImage((cover.width() - logo.width()) // 2, cover.height() - logo.height(), logo)
painter.end()
cover = cover.scaled(ImageSize.Image.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# this is not required if we ever want to re-apply the alpha channel
# cover = cover.convertToFormat(QImage.Format_Indexed8)
# add the alpha channel back to the cover
cover = cover.convertToFormat(QImage.Format_ARGB32_Premultiplied)
cover.save(
str(self.__img_color(game.app_name)),
format="PNG",
)
# quick way to convert to grayscale
cover = cover.convertToFormat(QImage.Format_Grayscale8)
# add the alpha channel back to the grayscale cover
cover = cover.convertToFormat(QImage.Format_ARGB32_Premultiplied)
cover.save(
str(self.__img_gray(game.app_name)),
format="PNG",
)
def __compress(self, game: Game, data: Dict) -> None:
archive = open(self.__img_cache(game.app_name), "wb")
cdata = zlib.compress(pickle.dumps(data), level=-1)
archive.write(cdata)
archive.close()
def __decompress(self, game: Game) -> Dict:
archive = open(self.__img_cache(game.app_name), "rb")
data = zlib.decompress(archive.read())
archive.close()
data = pickle.loads(data)
return data
def download_image(
self, game: Game, load_callback: Callable[[], None], priority: int, force: bool = False
) -> None:
updates, json_data = self.__prepare_download(game, force)
if not updates:
load_callback()
return
if updates and game.app_name not in self.__worker_app_names:
image_worker = ImageManager.Worker(self.__download, updates, json_data, game)
self.__worker_app_names.append(game.app_name)
image_worker.signals.completed.connect(load_callback)
image_worker.signals.completed.connect(lambda app_name: self.__worker_app_names.remove(app_name))
self.threadpool.start(image_worker, priority)
def download_image_blocking(self, game: Game, force: bool = False) -> None:
updates, json_data = self.__prepare_download(game, force)
if not updates:
return
if updates:
self.__download(updates, json_data, game)
def __get_cover(
self, container: Union[Type[QPixmap], Type[QImage]], app_name: str, color: bool = True
) -> Union[QPixmap, QImage]:
ret = container()
if not app_name:
raise RuntimeError("app_name is an empty string")
if color:
if self.__img_color(app_name).is_file():
ret.load(str(self.__img_color(app_name)))
else:
if self.__img_gray(app_name).is_file():
ret.load(str(self.__img_gray(app_name)))
if not ret.isNull():
ret.setDevicePixelRatio(ImageSize.Image.pixel_ratio)
# lk: Scaling happens at painting. It might be inefficient so leave this here as an alternative
# lk: If this is uncommented, the transformation in ImageWidget should be adjusted also
ret = ret.scaled(self.device.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
ret.setDevicePixelRatio(self.device.pixel_ratio)
return ret
def get_pixmap(self, app_name: str, color: bool = True) -> QPixmap:
"""
Use when the image is to be presented directly on the screen.
@param app_name: The RareGame object for this game
@param color: True to load the colored pixmap, False to load the grayscale
@return: QPixmap
"""
pixmap: QPixmap = self.__get_cover(QPixmap, app_name, color)
return pixmap
def get_image(self, app_name: str, color: bool = True) -> QImage:
"""
Use when the image has to be manipulated before being rendered.
@param app_name: The RareGame object for this game
@param color: True to load the colored image, False to load the grayscale
@return: QImage
"""
image: QImage = self.__get_cover(QImage, app_name, color)
return image
_image_manager_singleton: Optional[ImageManager] = None
def ImageManagerSingleton(init: bool = False) -> ImageManager:
global _image_manager_singleton
if _image_manager_singleton is None and not init:
raise RuntimeError("Uninitialized use of ImageManagerSingleton")
if _image_manager_singleton is None:
_image_manager_singleton = ImageManager()
return _image_manager_singleton

View file

@ -4,8 +4,6 @@ from dataclasses import field, dataclass
from multiprocessing import Queue from multiprocessing import Queue
from typing import Union, List, Optional from typing import Union, List, Optional
from PyQt5.QtCore import QObject, pyqtSignal
from legendary.core import LegendaryCore from legendary.core import LegendaryCore
from legendary.downloader.mp.manager import DLManager from legendary.downloader.mp.manager import DLManager
from legendary.models.downloading import AnalysisResult, ConditionCheckResult from legendary.models.downloading import AnalysisResult, ConditionCheckResult
@ -103,49 +101,3 @@ class PathSpec:
return prefixes[0] return prefixes[0]
else: else:
return prefixes[:results] return prefixes[:results]
@dataclass
class ApiResults:
game_list: Optional[list] = None
dlcs: Optional[dict] = None
bit32_games: Optional[list] = None
mac_games: Optional[list] = None
no_asset_games: Optional[list] = None
saves: Optional[list] = None
def __bool__(self):
return (
self.game_list is not None
and self.dlcs is not None
and self.bit32_games is not None
and self.mac_games is not None
and self.no_asset_games is not None
and self.saves is not None
)
class GlobalSignals(QObject):
exit_app = pyqtSignal(int) # exit code
send_notification = pyqtSignal(str) # app_title
set_main_tab_index = pyqtSignal(int) # tab index
update_download_tab_text = pyqtSignal()
dl_progress = pyqtSignal(int) # 0-100
# set visibility of installing widget in games tab
installation_started = pyqtSignal(str) # app_name
add_download = pyqtSignal(str)
install_game = pyqtSignal(InstallOptionsModel)
installation_finished = pyqtSignal(bool, str)
overlay_installation_finished = pyqtSignal()
update_gamelist = pyqtSignal(list)
game_uninstalled = pyqtSignal(str) # appname
set_discord_rpc = pyqtSignal(str) # app_name of running game
rpc_settings_updated = pyqtSignal()
wine_prefix_updated = pyqtSignal()

View file

@ -1,13 +1,11 @@
import json
import math import math
import os import os
import platform import platform
import shlex import shlex
import shutil
import subprocess import subprocess
import sys import sys
from logging import getLogger from logging import getLogger
from typing import Tuple, List from typing import List, Tuple
import qtawesome import qtawesome
import requests import requests
@ -18,11 +16,10 @@ from PyQt5.QtCore import (
QRunnable, QRunnable,
QSettings, QSettings,
QStandardPaths, QStandardPaths,
Qt,
QFile, QFile,
QDir, QDir,
) )
from PyQt5.QtGui import QPalette, QColor, QPixmap, QImage from PyQt5.QtGui import QPalette, QColor, QImage
from PyQt5.QtWidgets import qApp, QStyleFactory from PyQt5.QtWidgets import qApp, QStyleFactory
from legendary.models.game import Game from legendary.models.game import Game
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
@ -45,85 +42,6 @@ from legendary.core import LegendaryCore
logger = getLogger("Utils") logger = getLogger("Utils")
settings = QSettings("Rare", "Rare") 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 = { color_role_map = {
0: "WindowText", 0: "WindowText",
1: "Button", 1: "Button",
@ -272,16 +190,7 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link=
if not for_rare: if not for_rare:
igame = core.get_installed_game(app_name) igame = core.get_installed_game(app_name)
if os.path.exists(p := os.path.join(image_dir, igame.app_name, "Thumbnail.png")): icon = os.path.join(os.path.join(image_dir, igame.app_name, "installed.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", "") icon = icon.replace(".png", "")
if platform.system() == "Linux": if platform.system() == "Linux":
@ -397,22 +306,6 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link=
return False 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]: def optimal_text_background(image: list) -> Tuple[int, int, int]:
""" """
Finds an optimal background color for text on the image by calculating the Finds an optimal background color for text on the image by calculating the

3
rare/widgets/__init__.py Normal file
View file

@ -0,0 +1,3 @@
"""
Reusable widgets module for Rare
"""

View file

@ -0,0 +1,25 @@
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontMetrics, QPaintEvent
from PyQt5.QtWidgets import QLabel
class ElideLabel(QLabel):
__text: str = ""
def __init__(self, text="", parent=None, flags=Qt.WindowFlags()):
super(ElideLabel, self).__init__(parent=parent, flags=flags)
if text:
self.setText(text)
def setText(self, a0: str) -> None:
self.__text = a0
self.__setElideText(a0)
def __setElideText(self, a0: str):
metrics = QFontMetrics(self.font())
elided_text = metrics.elidedText(a0, Qt.ElideRight, self.width())
super(ElideLabel, self).setText(elided_text)
def paintEvent(self, a0: QPaintEvent) -> None:
self.__setElideText(self.__text)
super(ElideLabel, self).paintEvent(a0)