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)