diff --git a/rare/app.py b/rare/app.py
index d78fd3b5..33144b48 100644
--- a/rare/app.py
+++ b/rare/app.py
@@ -8,22 +8,24 @@ import time
import traceback
from argparse import Namespace
from datetime import datetime
+from typing import Optional
+import legendary
import requests.exceptions
from PyQt5.QtCore import Qt, QThreadPool, QSettings, QTranslator, QTimer
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox
from requests import HTTPError
-import legendary
# noinspection PyUnresolvedReferences
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.main_window import MainWindow
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.paths import cache_dir, resources_path, tmp_dir
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
@@ -54,8 +56,8 @@ def excepthook(exc_type, exc_value, exc_tb):
class App(QApplication):
- mainwindow: MainWindow = None
- tray_icon: QSystemTrayIcon = None
+ mainwindow: Optional[MainWindow] = None
+ tray_icon: Optional[QSystemTrayIcon] = None
def __init__(self, args: Namespace):
super(App, self).__init__(sys.argv)
@@ -63,7 +65,7 @@ class App(QApplication):
self.window_launched = False
self.setQuitOnLastWindowClosed(False)
- if hasattr(Qt, 'AA_UseHighDpiPixmaps'):
+ if hasattr(Qt, "AA_UseHighDpiPixmaps"):
self.setAttribute(Qt.AA_UseHighDpiPixmaps)
# init Legendary
@@ -102,6 +104,7 @@ class App(QApplication):
self.settings = QSettings()
self.signals = GlobalSignalsSingleton(init=True)
+ self.image_manager = ImageManagerSingleton(init=True)
self.signals.exit_app.connect(self.exit_app)
self.signals.send_notification.connect(
diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py
index b79acdc1..812d5074 100644
--- a/rare/components/dialogs/launch_dialog.py
+++ b/rare/components/dialogs/launch_dialog.py
@@ -1,45 +1,77 @@
-import os
import platform
from logging import getLogger
from PyQt5.QtCore import Qt, pyqtSignal, QRunnable, QObject, QThreadPool, QSettings
from PyQt5.QtWidgets import QDialog, QApplication
-from legendary.core import LegendaryCore
from requests.exceptions import ConnectionError, HTTPError
-from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton
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.utils.models import ApiResults
-from rare.utils.paths import image_dir
-from rare.utils.utils import download_images, CloudWorker
+from rare.utils.utils import CloudWorker
logger = getLogger("Login")
-class LaunchDialogSignals(QObject):
- image_progress = pyqtSignal(int)
- result = pyqtSignal(object, str)
+class LaunchWorker(QRunnable):
+ class Signals(QObject):
+ progress = pyqtSignal(int)
+ result = pyqtSignal(object, str)
-
-class ImageWorker(QRunnable):
def __init__(self):
- super(ImageWorker, self).__init__()
- self.signals = LaunchDialogSignals()
+ super(LaunchWorker, self).__init__()
self.setAutoDelete(True)
+ self.signals = LaunchWorker.Signals()
self.core = LegendaryCoreSingleton()
def run(self):
- download_images(self.signals.image_progress, self.signals.result, self.core)
- self.signals.image_progress.emit(100)
+ pass
-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):
super(ApiRequestWorker, self).__init__()
- self.signals = LaunchDialogSignals()
- self.setAutoDelete(True)
- self.core = LegendaryCoreSingleton()
self.settings = QSettings()
def run(self) -> None:
@@ -106,14 +138,11 @@ class LaunchDialog(QDialog, Ui_LaunchDialog):
self.quit_app.emit(0)
def launch(self):
- # self.core = core
- if not os.path.exists(image_dir):
- os.makedirs(image_dir)
if not self.args.offline:
self.image_info.setText(self.tr("Downloading Images"))
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)
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_worker = CloudWorker()
- cloud_worker.signals.result_ready.connect(
- lambda x: self.handle_api_worker_result(x, "saves")
- )
+ cloud_worker.signals.result_ready.connect(lambda x: self.handle_api_worker_result(x, "saves"))
self.thread_pool.start(cloud_worker)
else:
@@ -159,13 +186,9 @@ class LaunchDialog(QDialog, Ui_LaunchDialog):
self.api_results.dlcs,
) = self.core.get_game_and_dlc_list(False)
elif text == "32bit":
- self.api_results.bit32_games = (
- [i.app_name for i in result[0]] if result else []
- )
+ self.api_results.bit32_games = [i.app_name for i in result[0]] if result else []
elif text == "mac":
- self.api_results.mac_games = (
- [i.app_name for i in result[0]] if result else []
- )
+ self.api_results.mac_games = [i.app_name for i in result[0]] if result else []
elif text == "no_assets":
self.api_results.no_asset_games = result if result else []
diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py
index 9a38e74a..91a91a8f 100644
--- a/rare/components/tabs/games/__init__.py
+++ b/rare/components/tabs/games/__init__.py
@@ -3,12 +3,17 @@ from typing import Tuple, Dict, Union, List
from PyQt5.QtCore import QSettings, QObjectCleanupHandler
from PyQt5.QtWidgets import QStackedWidget, QVBoxLayout, QWidget
-
-from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton, ApiResultsSingleton
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.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 .game_info import GameInfoTabs
@@ -41,6 +46,7 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton()
self.api_results = ApiResultsSingleton()
+ self.image_manager = ImageManagerSingleton()
self.settings = QSettings()
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:
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():
continue
self.ue_name = i.app_name
@@ -216,15 +222,15 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
)
def add_installed_widget(self, app_name):
- pixmap = get_pixmap(app_name)
+ pixmap = self.image_manager.get_pixmap(app_name)
try:
if pixmap.isNull():
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":
- download_image(self.core.get_game(app_name), force=True)
- pixmap = get_pixmap(app_name)
+ self.image_manager.download_image_blocking(self.core.get_game(app_name), force=True)
+ pixmap = self.image_manager.get_pixmap(app_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)
list_widget = InstalledListWidget(app_name, pixmap, self.game_utils)
@@ -242,15 +248,15 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
return icon_widget, list_widget
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:
if pixmap.isNull():
if self.core.get_asset(game.app_name).namespace != "ue":
logger.warning(f"{game.app_title} has a corrupt image. Reloading...")
- download_image(game, force=True)
- pixmap = get_uninstalled_pixmap(game.app_name)
+ self.image_manager.download_image_blocking(game, force=True)
+ pixmap = self.image_manager.get_pixmap(game.app_name, color=False)
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)
list_widget = ListWidgetUninstalled(self.core, game, pixmap)
diff --git a/rare/components/tabs/games/game_info/game_dlc.py b/rare/components/tabs/games/game_info/game_dlc.py
index 7f0c375b..0cc325f7 100644
--- a/rare/components/tabs/games/game_info/game_dlc.py
+++ b/rare/components/tabs/games/game_info/game_dlc.py
@@ -1,14 +1,14 @@
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap, QResizeEvent
from PyQt5.QtWidgets import QFrame, QWidget, QMessageBox
-
from legendary.models.game import Game
-from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
+
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_widget import Ui_GameDlcWidget
from rare.utils.models import InstallOptionsModel
-from rare.utils.utils import get_pixmap
class GameDlc(QWidget, Ui_GameDlc):
@@ -90,14 +90,17 @@ class GameDlcWidget(QFrame, Ui_GameDlcWidget):
def __init__(self, dlc: Game, installed: bool, parent=None):
super(GameDlcWidget, self).__init__(parent=parent)
+ self.image_manager = ImageManagerSingleton()
self.setupUi(self)
self.dlc = dlc
+ self.image.setFixedSize(ImageSize.Smaller.size)
+
self.dlc_name.setText(dlc.app_title)
self.version.setText(dlc.app_version())
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:
self.action_button.setProperty("uninstall", 1)
diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py
index 4eacbf3b..18b4dfd2 100644
--- a/rare/components/tabs/games/game_info/game_info.py
+++ b/rare/components/tabs/games/game_info/game_info.py
@@ -1,8 +1,8 @@
import os
import platform
import shutil
-from pathlib import Path
from logging import getLogger
+from pathlib import Path
from typing import Tuple
from PyQt5.QtCore import (
@@ -25,20 +25,21 @@ from PyQt5.QtWidgets import (
QMessageBox,
QWidgetAction,
)
-
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 (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
)
+from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.extra_widgets import PathEdit
from rare.utils.legendary_utils import VerifyWorker
from rare.utils.models import InstallOptionsModel
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")
@@ -56,6 +57,7 @@ class GameInfo(QWidget, Ui_GameInfo):
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton()
+ self.image_manager = ImageManagerSingleton()
self.game_utils = game_utils
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.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():
- pixmap = get_pixmap(self.parent().parent().parent().ue_name)
- w = 200
- pixmap = pixmap.scaled(w, int(w * 4 / 3))
+ pixmap = self.image_manager.get_pixmap(self.parent().parent().parent().ue_name)
+ pixmap = pixmap.scaled(ImageSize.Display.size)
self.image.setPixmap(pixmap)
self.app_name.setText(self.game.app_name)
diff --git a/rare/components/tabs/games/game_info/uninstalled_info.py b/rare/components/tabs/games/game_info/uninstalled_info.py
index 05dd63f8..e41e6d78 100644
--- a/rare/components/tabs/games/game_info/uninstalled_info.py
+++ b/rare/components/tabs/games/game_info/uninstalled_info.py
@@ -3,15 +3,20 @@ import platform
from PyQt5.QtCore import Qt, QThreadPool
from PyQt5.QtGui import QKeyEvent
from PyQt5.QtWidgets import QWidget, QTreeView
-
-from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton, ApiResultsSingleton
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.utils.extra_widgets import SideTabWidget
from rare.utils.json_formatter import QJsonModel
from rare.utils.models import InstallOptionsModel
from rare.utils.steam_grades import SteamWorker
-from rare.utils.utils import get_pixmap
class UninstalledInfoTabs(SideTabWidget):
@@ -71,6 +76,8 @@ class UninstalledInfo(QWidget, Ui_GameInfo):
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.api_results = ApiResultsSingleton()
+ self.image_manager = ImageManagerSingleton()
+
self.install_button.clicked.connect(self.install_game)
if platform.system() != "Windows":
self.steam_worker = SteamWorker(self.core)
@@ -104,11 +111,10 @@ class UninstalledInfo(QWidget, Ui_GameInfo):
available_platforms.append("macOS")
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():
- pixmap = get_pixmap(self.ue_default_name)
- w = 200
- pixmap = pixmap.scaled(w, int(w * 4 / 3))
+ pixmap = self.image_manager.get_pixmap(self.ue_default_name, color=False)
+ pixmap = pixmap.scaled(ImageSize.Display.size)
self.image.setPixmap(pixmap)
self.app_name.setText(self.game.app_name)
diff --git a/rare/components/tabs/games/game_widgets/base_installed_widget.py b/rare/components/tabs/games/game_widgets/base_installed_widget.py
index a775ef42..f9d64842 100644
--- a/rare/components/tabs/games/game_widgets/base_installed_widget.py
+++ b/rare/components/tabs/games/game_widgets/base_installed_widget.py
@@ -6,9 +6,9 @@ from PyQt5.QtCore import pyqtSignal, QProcess, QSettings, QStandardPaths, Qt, QB
from PyQt5.QtGui import QPixmap
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.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
logger = getLogger("Game")
@@ -25,6 +25,7 @@ class BaseInstalledWidget(QGroupBox):
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton()
+ self.image_manager = ImageManagerSingleton()
self.game_utils = game_utils
self.syncing_cloud_saves = False
@@ -63,8 +64,9 @@ class BaseInstalledWidget(QGroupBox):
pass
self.image = QLabel()
+ self.image.setFixedSize(ImageSize.Display.size)
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.offline = self.args.offline
@@ -140,11 +142,9 @@ class BaseInstalledWidget(QGroupBox):
)
def reload_image(self):
- utils.download_image(self.game, True)
- pm = utils.get_pixmap(self.game.app_name)
- self.image.setPixmap(
- pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
- )
+ self.image_manager.download_image_blocking(self.game, force=True)
+ pm = self.image_manager.get_pixmap(self.game.app_name, color=True)
+ self.image.setPixmap(pm.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation))
def create_desktop_link(self, type_of_link):
if type_of_link == "desktop":
diff --git a/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py b/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py
index 1354cdfe..de216578 100644
--- a/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py
+++ b/rare/components/tabs/games/game_widgets/base_uninstalled_widget.py
@@ -2,9 +2,9 @@ from logging import getLogger
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import QGroupBox, QLabel, QAction
-
from legendary.models.game import Game
-from rare.utils import utils
+
+from rare.shared.image_manager import ImageManagerSingleton, ImageSize
logger = getLogger("Uninstalled")
@@ -14,15 +14,16 @@ class BaseUninstalledWidget(QGroupBox):
def __init__(self, game, core, pixmap):
super(BaseUninstalledWidget, self).__init__()
+ self.image_manager = ImageManagerSingleton()
+
self.game = game
if self.game.app_title == "Unreal Engine":
self.game.app_title = f"{self.game.app_title} {self.game.app_name.split('_')[-1]}"
self.core = core
self.image = QLabel()
- self.image.setPixmap(
- pixmap.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
- )
+ self.image.setFixedSize(ImageSize.Display.size)
+ self.image.setPixmap(pixmap.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation))
self.installing = False
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.setContentsMargins(0, 0, 0, 0)
@@ -32,11 +33,9 @@ class BaseUninstalledWidget(QGroupBox):
self.addAction(reload_image)
def reload_image(self):
- utils.download_image(self.game, True)
- pm = utils.get_uninstalled_pixmap(self.game.app_name)
- self.image.setPixmap(
- pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
- )
+ self.image_manager.download_image_blocking(self.game, force=True)
+ pm = self.image_manager.get_pixmap(self.game.app_name, color=False)
+ self.image.setPixmap(pm.scaled(ImageSize.Display.size, transformMode=Qt.SmoothTransformation))
def install(self):
self.show_uninstalled_info.emit(self.game)
diff --git a/rare/components/tabs/games/game_widgets/installed_icon_widget.py b/rare/components/tabs/games/game_widgets/installed_icon_widget.py
index 5821b32e..5f39ebd1 100644
--- a/rare/components/tabs/games/game_widgets/installed_icon_widget.py
+++ b/rare/components/tabs/games/game_widgets/installed_icon_widget.py
@@ -1,14 +1,16 @@
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.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 (
BaseInstalledWidget,
)
+from rare.shared import LegendaryCoreSingleton
+from rare.shared.image_manager import ImageSize
from rare.utils.utils import icon
+from rare.widgets.elide_label import ElideLabel
logger = getLogger("GameWidgetInstalled")
@@ -21,55 +23,54 @@ class InstalledIconWidget(BaseInstalledWidget):
self.setObjectName("game_widget_icon")
self.setContextMenuPolicy(Qt.ActionsContextMenu)
- self.layout = QVBoxLayout()
+ layout = QVBoxLayout()
self.core = LegendaryCoreSingleton()
if self.update_available:
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.cloud_save_finished.connect(self.sync_finished)
- self.title_label = QLabel(f"
{self.game.app_title}
")
- self.title_label.setAutoFillBackground(False)
- self.title_label.setWordWrap(True)
- self.title_label.setFixedWidth(175)
+ miniwidget = QWidget(self)
+ miniwidget.setFixedWidth(ImageSize.Display.size.width())
minilayout = QHBoxLayout()
+ minilayout.setContentsMargins(0, 0, 0, 0)
+ minilayout.setSpacing(0)
+ miniwidget.setLayout(minilayout)
+
+ self.title_label = ElideLabel(f"{self.game.app_title}
", parent=miniwidget)
self.title_label.setObjectName("game_widget")
- minilayout.addWidget(self.title_label)
+ minilayout.addWidget(self.title_label, stretch=2)
# Info Button
- self.menu_btn = QPushButton()
+ self.menu_btn = QPushButton(parent=miniwidget)
self.menu_btn.setIcon(icon("ei.info-circle"))
# 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)
# remove Border
self.menu_btn.setObjectName("menu_button")
self.menu_btn.clicked.connect(lambda: self.show_info.emit(self.game.app_name))
- self.menu_btn.setFixedWidth(17)
- minilayout.addWidget(self.menu_btn)
- minilayout.addStretch(1)
- self.layout.addLayout(minilayout)
+ self.menu_btn.setFixedSize(22, 22)
+ minilayout.addWidget(self.menu_btn, stretch=0)
+ minilayout.setAlignment(Qt.AlignTop)
+ 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.info_label.setAutoFillBackground(False)
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:
self.info_label.setText(self.texts["needs_verification"])
- self.setLayout(self.layout)
- self.setFixedWidth(self.sizeHint().width())
+ self.setLayout(layout)
self.game_utils.game_launched.connect(self.game_started)
@@ -99,7 +100,7 @@ class InstalledIconWidget(BaseInstalledWidget):
elif self.igame and self.igame.needs_verification:
self.info_label.setText(self.texts["needs_verification"])
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):
# left button
diff --git a/rare/components/tabs/games/game_widgets/installing_game_widget.py b/rare/components/tabs/games/game_widgets/installing_game_widget.py
index cb636771..0299e313 100644
--- a/rare/components/tabs/games/game_widgets/installing_game_widget.py
+++ b/rare/components/tabs/games/game_widgets/installing_game_widget.py
@@ -1,14 +1,13 @@
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPaintEvent, QPainter, QPixmap, QPen, QFont, QColor
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QHBoxLayout, QWidget
-
from legendary.models.game import Game
+
from rare.shared import LegendaryCoreSingleton
+from rare.shared.image_manager import ImageManagerSingleton, ImageSize
from rare.utils.utils import (
- get_pixmap,
optimal_text_background,
text_color_for_background,
- get_uninstalled_pixmap,
)
@@ -24,11 +23,9 @@ class InstallingGameWidget(QWidget):
self.core = LegendaryCoreSingleton()
self.pixmap = QPixmap()
- w = 200
- # self.pixmap = self.pixmap.scaled(w, int(w * 4 / 3), transformMode=Qt.SmoothTransformation)
self.image_widget = PaintWidget()
- self.setContentsMargins(4, 4, 4, 4)
- self.image_widget.setFixedSize(w, int(w * 4 / 3))
+ self.setContentsMargins(0, 0, 0, 0)
+ self.image_widget.setFixedSize(ImageSize.Display.size)
self.layout().addWidget(self.image_widget)
self.title_label = QLabel(f"Error
")
@@ -62,18 +59,18 @@ class PaintWidget(QWidget):
def __init__(self):
super(PaintWidget, self).__init__()
self.core = LegendaryCoreSingleton()
+ self.image_manager = ImageManagerSingleton()
def set_game(self, app_name: str):
game = self.core.get_game(app_name, False)
- self.color_image = get_pixmap(game.app_name)
- w = 200
+ self.color_image = self.image_manager.get_pixmap(game.app_name, color=False)
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.bw_image = get_uninstalled_pixmap(app_name)
+ self.setFixedSize(ImageSize.Display.size)
+ self.bw_image = self.image_manager.get_pixmap(app_name, color=False)
self.bw_image = self.bw_image.scaled(
- w, int(w * 4 // 3), transformMode=Qt.SmoothTransformation
+ ImageSize.Display.size, transformMode=Qt.SmoothTransformation
)
self.progress = 0
diff --git a/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py b/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py
index 4a653828..0bd959ec 100644
--- a/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py
+++ b/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py
@@ -1,12 +1,15 @@
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.models.game import Game
+
from rare.components.tabs.games.game_widgets.base_uninstalled_widget import (
BaseUninstalledWidget,
)
+from rare.shared.image_manager import ImageSize
+from rare.widgets.elide_label import ElideLabel
logger = getLogger("Uninstalled")
@@ -14,19 +17,31 @@ logger = getLogger("Uninstalled")
class IconWidgetUninstalled(BaseUninstalledWidget):
def __init__(self, game: Game, core: LegendaryCore, pixmap):
super(IconWidgetUninstalled, self).__init__(game, core, pixmap)
- self.layout = QVBoxLayout()
+ layout = QVBoxLayout()
self.setObjectName("game_widget_icon")
- self.layout.addWidget(self.image)
+ layout.addWidget(self.image)
- self.title_label = QLabel(f"{game.app_title}
")
- self.title_label.setWordWrap(True)
- self.layout.addWidget(self.title_label)
+ miniwidget = QWidget(self)
+ miniwidget.setFixedWidth(ImageSize.Display.size.width())
+ minilayout = QHBoxLayout()
+ minilayout.setContentsMargins(0, 0, 0, 0)
+ minilayout.setSpacing(0)
+ miniwidget.setLayout(minilayout)
- self.info_label = QLabel("")
- self.layout.addWidget(self.info_label)
+ self.title_label = ElideLabel(f"{game.app_title}
", parent=miniwidget)
+ self.title_label.setObjectName("game_widget")
+ minilayout.addWidget(self.title_label, stretch=2)
- self.setLayout(self.layout)
- self.setFixedWidth(self.sizeHint().width())
+ minilayout.setAlignment(Qt.AlignTop)
+ 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:
# left button
@@ -47,4 +62,4 @@ class IconWidgetUninstalled(BaseUninstalledWidget):
if self.installing:
self.info_label.setText("Installation...")
else:
- self.info_label.setText("")
+ self.info_label.setText(" ") # invisible text, cheap way to always have vertical size in label
diff --git a/rare/models/__init__.py b/rare/models/__init__.py
new file mode 100644
index 00000000..7f20d0cb
--- /dev/null
+++ b/rare/models/__init__.py
@@ -0,0 +1,3 @@
+"""
+Models and data structures module for Rare
+"""
diff --git a/rare/models/apiresults.py b/rare/models/apiresults.py
new file mode 100644
index 00000000..2e67d8ca
--- /dev/null
+++ b/rare/models/apiresults.py
@@ -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
+ )
diff --git a/rare/models/signals.py b/rare/models/signals.py
new file mode 100644
index 00000000..97119b09
--- /dev/null
+++ b/rare/models/signals.py
@@ -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()
diff --git a/rare/resources/images/cover.png b/rare/resources/images/cover.png
new file mode 100644
index 00000000..343b52a3
Binary files /dev/null and b/rare/resources/images/cover.png differ
diff --git a/rare/shared.py b/rare/shared/__init__.py
similarity index 88%
rename from rare/shared.py
rename to rare/shared/__init__.py
index 33ce579f..c0fff881 100644
--- a/rare/shared.py
+++ b/rare/shared/__init__.py
@@ -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 typing import Optional
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
_global_signals_singleton: Optional[GlobalSignals] = None
diff --git a/rare/shared/image_manager.py b/rare/shared/image_manager.py
new file mode 100644
index 00000000..88be2145
--- /dev/null
+++ b/rare/shared/image_manager.py
@@ -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
diff --git a/rare/utils/models.py b/rare/utils/models.py
index 81352b75..16f457d2 100644
--- a/rare/utils/models.py
+++ b/rare/utils/models.py
@@ -4,8 +4,6 @@ from dataclasses import field, dataclass
from multiprocessing import Queue
from typing import Union, List, Optional
-from PyQt5.QtCore import QObject, pyqtSignal
-
from legendary.core import LegendaryCore
from legendary.downloader.mp.manager import DLManager
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
@@ -103,49 +101,3 @@ class PathSpec:
return prefixes[0]
else:
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()
diff --git a/rare/utils/utils.py b/rare/utils/utils.py
index 4dfd947a..1387727d 100644
--- a/rare/utils/utils.py
+++ b/rare/utils/utils.py
@@ -1,13 +1,11 @@
-import json
import math
import os
import platform
import shlex
-import shutil
import subprocess
import sys
from logging import getLogger
-from typing import Tuple, List
+from typing import List, Tuple
import qtawesome
import requests
@@ -18,11 +16,10 @@ from PyQt5.QtCore import (
QRunnable,
QSettings,
QStandardPaths,
- Qt,
QFile,
QDir,
)
-from PyQt5.QtGui import QPalette, QColor, QPixmap, QImage
+from PyQt5.QtGui import QPalette, QColor, QImage
from PyQt5.QtWidgets import qApp, QStyleFactory
from legendary.models.game import Game
from requests.exceptions import HTTPError
@@ -45,85 +42,6 @@ from legendary.core import LegendaryCore
logger = getLogger("Utils")
settings = QSettings("Rare", "Rare")
-
-def download_images(progress: pyqtSignal, results: pyqtSignal, core: LegendaryCore):
- if not os.path.isdir(image_dir):
- os.makedirs(image_dir)
- logger.info("Create Image dir")
-
- # Download Images
- games, dlcs = core.get_game_and_dlc_list(True, skip_ue=False)
- results.emit((games, dlcs), "gamelist")
- dlc_list = []
- for i in dlcs.values():
- dlc_list.append(i[0])
-
- no_assets = core.get_non_asset_library_items()[0]
- results.emit(no_assets, "no_assets")
-
- game_list = games + dlc_list + no_assets
- for i, game in enumerate(game_list):
- if game.app_title == "Unreal Engine":
- game.app_title += f" {game.app_name.split('_')[-1]}"
- core.lgd.set_game_meta(game.app_name, game)
- try:
- download_image(game)
- except json.decoder.JSONDecodeError:
- shutil.rmtree(f"{image_dir}/{game.app_name}")
- download_image(game)
- progress.emit(i * 100 // len(game_list))
-
-
-def download_image(game, force=False):
- if force and os.path.exists(f"{image_dir}/{game.app_name}"):
- shutil.rmtree(f"{image_dir}/{game.app_name}")
- if not os.path.isdir(f"{image_dir}/{game.app_name}"):
- os.mkdir(f"{image_dir}/{game.app_name}")
-
- # to get picture updates
- if not os.path.isfile(f"{image_dir}/{game.app_name}/image.json"):
- json_data = {
- "DieselGameBoxTall": None,
- "DieselGameBoxLogo": None,
- "Thumbnail": None,
- }
- else:
- json_data = json.load(open(f"{image_dir}/{game.app_name}/image.json", "r"))
- # Download
- for image in game.metadata["keyImages"]:
- if (
- image["type"] == "DieselGameBoxTall"
- or image["type"] == "DieselGameBoxLogo"
- or image["type"] == "Thumbnail"
- ):
- if image["type"] not in json_data.keys():
- json_data[image["type"]] = None
- if json_data[image["type"]] != image["md5"] or not os.path.isfile(
- f"{image_dir}/{game.app_name}/{image['type']}.png"
- ):
- # Download
- json_data[image["type"]] = image["md5"]
- # os.remove(f"{image_dir}/{game.app_name}/{image['type']}.png")
- json.dump(
- json_data, open(f"{image_dir}/{game.app_name}/image.json", "w")
- )
- logger.info(f"Download Image for Game: {game.app_title}")
- url = image["url"]
- resp = requests.get(url)
- img = QImage()
- img.loadFromData(resp.content)
- img = img.scaled(
- 200,
- 200 * 4 // 3,
- Qt.KeepAspectRatio,
- transformMode=Qt.SmoothTransformation,
- )
- img.save(
- os.path.join(image_dir, game.app_name, image["type"] + ".png"),
- format="PNG",
- )
-
-
color_role_map = {
0: "WindowText",
1: "Button",
@@ -272,16 +190,7 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link=
if not for_rare:
igame = core.get_installed_game(app_name)
- if os.path.exists(p := os.path.join(image_dir, igame.app_name, "Thumbnail.png")):
- icon = p
- elif os.path.exists(
- p := os.path.join(image_dir, igame.app_name, "DieselGameBoxLogo.png")
- ):
- icon = p
- else:
- icon = os.path.join(
- os.path.join(image_dir, igame.app_name, "DieselGameBoxTall.png")
- )
+ icon = os.path.join(os.path.join(image_dir, igame.app_name, "installed.png"))
icon = icon.replace(".png", "")
if platform.system() == "Linux":
@@ -397,22 +306,6 @@ def create_desktop_link(app_name=None, core: LegendaryCore = None, type_of_link=
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
diff --git a/rare/widgets/__init__.py b/rare/widgets/__init__.py
new file mode 100644
index 00000000..320a85f1
--- /dev/null
+++ b/rare/widgets/__init__.py
@@ -0,0 +1,3 @@
+"""
+Reusable widgets module for Rare
+"""
diff --git a/rare/widgets/elide_label.py b/rare/widgets/elide_label.py
new file mode 100644
index 00000000..e8e58966
--- /dev/null
+++ b/rare/widgets/elide_label.py
@@ -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)