1
0
Fork 0
mirror of synced 2024-06-26 18:20:50 +12:00

ImageManager: Add the ability to request different types of cover art images

This commit is contained in:
loathingKernel 2024-05-31 00:21:58 +03:00
parent 4587c73671
commit da93dc2e5e
14 changed files with 197 additions and 149 deletions

View file

@ -14,6 +14,7 @@ from rare.components.dialogs.install_dialog import InstallDialog
from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame
from rare.models.image import ImageSize
from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
from rare.models.options import options
from rare.shared import RareCore
@ -209,9 +210,9 @@ class DownloadsTab(QWidget):
self.__thread = dl_thread
self.download_widget.ui.kill_button.setDisabled(False)
self.download_widget.ui.dl_name.setText(item.download.game.app_title)
self.download_widget.setPixmap(
RareCore.instance().image_manager().get_pixmap(rgame.app_name, True)
)
self.download_widget.setPixmap(self.rcore.image_manager().get_pixmap(
rgame.app_name, ImageSize.Wide, True
))
self.signals.application.notify.emit(
self.tr("Downloads"),

View file

@ -29,7 +29,7 @@ class DownloadWidget(ImageWidget):
def prepare_pixmap(self, pixmap: QPixmap) -> QPixmap:
device: QImage = QImage(
pixmap.size().width() * 3,
pixmap.size().width() * 1,
int(self.sizeHint().height() * pixmap.devicePixelRatioF()) + 1,
QImage.Format_ARGB32_Premultiplied,
)
@ -38,9 +38,9 @@ class DownloadWidget(ImageWidget):
painter.fillRect(device.rect(), brush)
# the gradient could be cached and reused as it is expensive
gradient = QLinearGradient(0, 0, device.width(), 0)
gradient.setColorAt(0.15, Qt.transparent)
gradient.setColorAt(0.02, Qt.transparent)
gradient.setColorAt(0.5, Qt.black)
gradient.setColorAt(0.85, Qt.transparent)
gradient.setColorAt(0.98, Qt.transparent)
painter.setCompositionMode(QPainter.CompositionMode_DestinationIn)
painter.fillRect(device.rect(), gradient)
painter.end()

View file

@ -34,7 +34,7 @@ class QueueInfoWidget(QWidget):
self.image_manager = ImageManagerSingleton()
self.image = ImageWidget(self)
self.image.setFixedSize(ImageSize.Smallest)
self.image.setFixedSize(ImageSize.LibraryIcon)
self.ui.image_layout.addWidget(self.image)
self.ui.queue_info_layout.setAlignment(Qt.AlignTop)
@ -50,7 +50,7 @@ class QueueInfoWidget(QWidget):
if old_igame:
self.ui.title.setText(old_igame.title)
self.image.setPixmap(self.image_manager.get_pixmap(old_igame.app_name, color=True))
self.image.setPixmap(self.image_manager.get_pixmap(old_igame.app_name, ImageSize.LibraryIcon))
def update_information(self, game, igame, analysis, old_igame):
self.ui.title.setText(game.app_title)
@ -60,7 +60,7 @@ class QueueInfoWidget(QWidget):
self.ui.local_version.setText(elide_text(self.ui.local_version, igame.version))
self.ui.dl_size.setText(format_size(analysis.dl_size) if analysis else "")
self.ui.install_size.setText(format_size(analysis.install_size) if analysis else "")
self.image.setPixmap(self.image_manager.get_pixmap(game.app_name, color=True))
self.image.setPixmap(self.image_manager.get_pixmap(game.app_name, ImageSize.LibraryIcon))
class UpdateWidget(QFrame):

View file

@ -58,7 +58,7 @@ class GameDetails(QWidget, SideTabContents):
self.rgame: Optional[RareGame] = None
self.image = ImageWidget(self)
self.image.setFixedSize(ImageSize.Display)
self.image.setFixedSize(ImageSize.DisplayTall)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop)
self.ui.install_button.clicked.connect(self.__on_install)
@ -270,7 +270,7 @@ class GameDetails(QWidget, SideTabContents):
@pyqtSlot()
def __update_widget(self):
""" React to state updates from RareGame """
self.image.setPixmap(self.rgame.get_pixmap(True))
self.image.setPixmap(self.rgame.get_pixmap(ImageSize.DisplayTall, True))
self.ui.lbl_version.setDisabled(self.rgame.is_non_asset)
self.ui.version.setDisabled(self.rgame.is_non_asset)

View file

@ -23,28 +23,28 @@ class GameDlcWidget(QFrame):
self.rdlc = rdlc
self.image = ImageWidget(self)
self.image.setFixedSize(ImageSize.Smallest)
self.image.setFixedSize(ImageSize.LibraryIcon)
self.ui.dlc_layout.insertWidget(0, self.image)
self.ui.dlc_name.setText(rdlc.app_title)
self.ui.version.setText(rdlc.version)
self.ui.app_name.setText(rdlc.app_name)
self.image.setPixmap(rdlc.pixmap)
# self.image.setPixmap(rdlc.get_pixmap_icon(rdlc.is_installed))
self.__update()
rdlc.signals.widget.update.connect(self.__update)
@pyqtSlot()
def __update(self):
self.ui.action_button.setEnabled(self.rdlc.is_idle)
self.image.setPixmap(self.rdlc.pixmap)
self.image.setPixmap(self.rdlc.get_pixmap(ImageSize.LibraryIcon, self.rdlc.is_installed))
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():
return super().showEvent(a0)
if self.rdlc.pixmap.isNull():
self.rdlc.load_pixmap()
if not self.rdlc.has_pixmap:
self.rdlc.load_pixmaps()
super().showEvent(a0)

View file

@ -58,18 +58,13 @@ class GameWidget(LibraryWidget):
self.update_actions()
# signals
self.rgame.signals.widget.update.connect(lambda: self.setPixmap(self.rgame.pixmap))
self.rgame.signals.widget.update.connect(self.update_pixmap)
self.rgame.signals.widget.update.connect(self.update_buttons)
self.rgame.signals.widget.update.connect(self.update_state)
self.rgame.signals.game.installed.connect(self.update_actions)
self.rgame.signals.game.uninstalled.connect(self.update_actions)
self.rgame.signals.progress.start.connect(
lambda: self.showProgress(
self.image_manager.get_pixmap(self.rgame.app_name, True),
self.image_manager.get_pixmap(self.rgame.app_name, False)
)
)
self.rgame.signals.progress.start.connect(self.start_progress)
self.rgame.signals.progress.update.connect(
lambda p: self.updateProgress(p)
)
@ -104,10 +99,10 @@ class GameWidget(LibraryWidget):
# lk: abstract class for typing, the `self.ui` attribute should be used
# lk: by the Ui class in the children. It must contain at least the same
# lk: attributes as `GameWidgetUi` class
__slots__ = "ui"
__slots__ = "ui", "update_pixmap", "start_progress"
def paintEvent(self, a0: QPaintEvent) -> None:
if not self.visibleRegion().isNull() and self.rgame.pixmap.isNull():
if not self.visibleRegion().isNull() and not self.rgame.has_pixmap:
self.startTimer(random.randrange(42, 2361, 129), Qt.CoarseTimer)
# self.startTimer(random.randrange(42, 2361, 363), Qt.VeryCoarseTimer)
# self.rgame.load_pixmap()
@ -115,7 +110,7 @@ class GameWidget(LibraryWidget):
def timerEvent(self, a0):
self.killTimer(a0.timerId())
self.rgame.load_pixmap()
self.rgame.load_pixmaps()
def showEvent(self, a0: QShowEvent) -> None:
if a0.spontaneous():

View file

@ -1,7 +1,7 @@
from logging import getLogger
from typing import Optional
from PyQt5.QtCore import QEvent
from PyQt5.QtCore import QEvent, pyqtSlot
from rare.models.game import RareGame
from rare.models.image import ImageSize
@ -15,7 +15,7 @@ class IconGameWidget(GameWidget):
def __init__(self, rgame: RareGame, parent=None):
super().__init__(rgame, parent)
self.setObjectName(f"{rgame.app_name}")
self.setFixedSize(ImageSize.Library)
self.setFixedSize(ImageSize.LibraryTall)
self.ui = IconWidget()
self.ui.setupUi(self)
@ -34,6 +34,17 @@ class IconGameWidget(GameWidget):
self.ui.launch_btn.installEventFilter(self)
self.ui.install_btn.installEventFilter(self)
@pyqtSlot()
def update_pixmap(self):
self.setPixmap(self.rgame.get_pixmap(ImageSize.LibraryTall, self.rgame.is_installed))
@pyqtSlot()
def start_progress(self):
self.showProgress(
self.rgame.get_pixmap(ImageSize.LibraryTall, True),
self.rgame.get_pixmap(ImageSize.LibraryTall, False)
)
def enterEvent(self, a0: Optional[QEvent] = None) -> None:
if a0 is not None:
a0.accept()

View file

@ -1,6 +1,6 @@
from logging import getLogger
from PyQt5.QtCore import Qt, QEvent, QRect
from PyQt5.QtCore import Qt, QEvent, QRect, pyqtSlot
from PyQt5.QtGui import (
QPalette,
QBrush,
@ -12,6 +12,7 @@ from PyQt5.QtGui import (
)
from rare.models.game import RareGame
from rare.models.image import ImageSize
from rare.utils.misc import format_size
from .game_widget import GameWidget
from .list_widget import ListWidget
@ -50,6 +51,17 @@ class ListGameWidget(GameWidget):
self.ui.launch_btn.installEventFilter(self)
self.ui.install_btn.installEventFilter(self)
@pyqtSlot()
def update_pixmap(self):
self.setPixmap(self.rgame.get_pixmap(ImageSize.LibraryWide, self.rgame.is_installed))
@pyqtSlot()
def start_progress(self):
self.showProgress(
self.rgame.get_pixmap(ImageSize.LibraryWide, True),
self.rgame.get_pixmap(ImageSize.LibraryWide, False)
)
def enterEvent(self, a0: QEvent = None) -> None:
if a0 is not None:
a0.accept()
@ -70,7 +82,7 @@ class ListGameWidget(GameWidget):
def prepare_pixmap(self, pixmap: QPixmap) -> QPixmap:
device: QImage = QImage(
pixmap.size().width() * 3,
pixmap.size().width() * 1,
int(self.sizeHint().height() * pixmap.devicePixelRatioF()) + 1,
QImage.Format_ARGB32_Premultiplied
)
@ -79,9 +91,9 @@ class ListGameWidget(GameWidget):
painter.fillRect(device.rect(), brush)
# the gradient could be cached and reused as it is expensive
gradient = QLinearGradient(0, 0, device.width(), 0)
gradient.setColorAt(0.15, Qt.transparent)
gradient.setColorAt(0.02, Qt.transparent)
gradient.setColorAt(0.5, Qt.black)
gradient.setColorAt(0.85, Qt.transparent)
gradient.setColorAt(0.98, Qt.transparent)
painter.setCompositionMode(QPainter.CompositionMode_DestinationIn)
painter.fillRect(device.rect(), gradient)
painter.end()
@ -104,7 +116,7 @@ class ListGameWidget(GameWidget):
brush = QBrush(self._pixmap)
brush.setTransform(self._transform)
width = int(self._pixmap.width() / self._pixmap.devicePixelRatioF())
origin = self.width() - width
origin = self.width() // 2
painter.setBrushOrigin(origin, 0)
fill_rect = QRect(origin, 0, width, self.height())
painter.fillRect(fill_rect, brush)

View file

@ -41,7 +41,7 @@ class StoreDetailsWidget(QWidget, SideTabContents):
self.catalog_offer: CatalogOfferModel = None
self.image = LoadingImageWidget(store_api.cached_manager, self)
self.image.setFixedSize(ImageSize.Display)
self.image.setFixedSize(ImageSize.DisplayTall)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop)
self.ui.left_layout.setAlignment(Qt.AlignTop)

View file

@ -76,7 +76,7 @@ class StoreItemWidget(ItemWidget):
class ResultsItemWidget(ItemWidget):
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None):
super(ResultsItemWidget, self).__init__(manager, catalog_game, parent=parent)
self.setFixedSize(ImageSize.Display)
self.setFixedSize(ImageSize.DisplayTall)
self.ui.setupUi(self)
key_images = catalog_game.keyImages

View file

@ -8,10 +8,11 @@ from threading import Lock
from typing import List, Optional, Dict, Set
from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool
from PyQt5.QtGui import QPixmap, QPixmapCache
from PyQt5.QtGui import QPixmap
from legendary.lfs import eos
from legendary.models.game import Game, InstalledGame
from rare.models.image import ImageSize
from rare.lgndr.core import LegendaryCore
from rare.models.base_game import RareGameBase, RareGameSlim
from rare.models.install import InstallOptionsModel, UninstallOptionsModel
@ -85,7 +86,7 @@ class RareGame(RareGameSlim):
if self.game.app_title == "Unreal Engine":
self.game.app_title += f" {self.game.app_name.split('_')[-1]}"
self.pixmap: QPixmap = QPixmap()
self.has_pixmap: bool = False
self.metadata: RareGame.Metadata = RareGame.Metadata()
self.__load_metadata()
self.grant_date()
@ -444,11 +445,11 @@ class RareGame(RareGameSlim):
if elapsed_time.days > 3:
logger.info("Refreshing ProtonDB grade for %s", self.app_title)
def _set_steam_grade():
def set_steam_grade():
appid, rating = get_rating(self.core, self.app_name)
self.set_steam_grade(appid, rating)
worker = QRunnable.create(_set_steam_grade)
worker = QRunnable.create(set_steam_grade)
QThreadPool.globalInstance().start(worker)
self.metadata.steam_grade = "pending"
return self.metadata.steam_grade
@ -504,20 +505,19 @@ class RareGame(RareGameSlim):
return bool(not self.is_foreign or self.can_run_offline)
return False
def get_pixmap(self, color=True) -> QPixmap:
QPixmapCache.clear()
return self.image_manager.get_pixmap(self.app_name, color)
def get_pixmap(self, preset: ImageSize.Preset, color=True) -> QPixmap:
return self.image_manager.get_pixmap(self.app_name, preset, color)
@pyqtSlot(object)
def set_pixmap(self):
self.pixmap = self.image_manager.get_pixmap(self.app_name, self.is_installed)
QPixmapCache.clear()
if not self.pixmap.isNull():
# self.pixmap = not self.image_manager.get_pixmap(self.app_name, self.is_installed).isNull()
self.has_pixmap = True
if self.has_pixmap:
self.signals.widget.update.emit()
def load_pixmap(self):
""" Do not call this function, call set_pixmap instead. This is only used for startup image loading """
if self.pixmap.isNull():
def load_pixmaps(self):
""" Do not call this function, call set_pixmap instead. This is only used for initial image loading """
if not self.has_pixmap:
self.image_manager.download_image(self.game, self.set_pixmap, 0, False)
def refresh_pixmap(self):

View file

@ -4,7 +4,7 @@ from typing import Tuple
from PyQt5.QtCore import QSize
class Orientation(Enum):
class ImageType(Enum):
Tall = 0
Wide = 1
Icon = 3
@ -13,17 +13,17 @@ class Orientation(Enum):
class ImageSize:
class Preset:
def __init__(self, divisor: float, pixel_ratio: float, orientation: Orientation = Orientation.Tall,
def __init__(self, divisor: float, pixel_ratio: float, orientation: ImageType = ImageType.Tall,
base: 'ImageSize.Preset' = None):
self.__divisor = divisor
self.__pixel_ratio = pixel_ratio
if orientation == Orientation.Tall:
if orientation == ImageType.Tall:
self.__img_factor = 67
self.__size = QSize(self.__img_factor * 3, self.__img_factor * 4) * pixel_ratio / divisor
if orientation == Orientation.Wide:
if orientation == ImageType.Wide:
self.__img_factor = 34
self.__size = QSize(self.__img_factor * 16, self.__img_factor * 9) * pixel_ratio / divisor
if orientation == Orientation.Icon:
if orientation == ImageType.Icon:
self.__img_factor = 128
self.__size = QSize(self.__img_factor * 1, self.__img_factor * 1) * pixel_ratio / divisor
self.__orientation = orientation
@ -57,14 +57,14 @@ class ImageSize:
return self.__pixel_ratio
@property
def orientation(self) -> Orientation:
def orientation(self) -> ImageType:
return self.__orientation
@property
def aspect_ratio(self) -> Tuple[int, int]:
if self.__orientation == Orientation.Tall:
if self.__orientation == ImageType.Tall:
return 3, 4
elif self.__orientation == Orientation.Wide:
elif self.__orientation == ImageType.Wide:
return 16, 9
else:
return 0, 0
@ -73,34 +73,28 @@ class ImageSize:
def base(self) -> 'ImageSize.Preset':
return self.__base
Image = Preset(1, 1)
Tall = Preset(1, 1)
"""! @brief Size and pixel ratio of the image on disk"""
ImageWide = Preset(1, 1, Orientation.Wide)
"""! @brief Size and pixel ratio for wide 16/9 image on disk"""
Display = Preset(1, 1, base=Image)
DisplayTall = Preset(1, 1, base=Tall)
"""! @brief Size and pixel ratio for displaying"""
DisplayWide = Preset(2, 1, Orientation.Wide, base=ImageWide)
"""! @brief Size and pixel ratio for wide 16/9 image display"""
LibraryWide = Preset(2.41, 1, Orientation.Wide, base=ImageWide)
Library = Preset(1.21, 1, base=Image)
LibraryTall = Preset(1.21, 1, base=Tall)
"""! @brief Same as Display"""
Small = Preset(3, 1, base=Image)
"""! @brief Small image size for displaying"""
Wide = Preset(1, 1, ImageType.Wide)
"""! @brief Size and pixel ratio for wide 16/9 image on disk"""
SmallWide = Preset(6, 1, Orientation.Wide, base=ImageWide)
"""! @brief Small image size for displaying"""
DisplayWide = Preset(2, 1, ImageType.Wide, base=Wide)
"""! @brief Size and pixel ratio for wide 16/9 image display"""
Smaller = Preset(4, 1, base=Image)
"""! @brief Smaller image size for displaying"""
LibraryWide = Preset(2.41, 1, ImageType.Wide, base=Wide)
SmallerWide = Preset(8, 1, Orientation.Wide, base=ImageWide)
"""! @brief Smaller image size for displaying"""
Icon = Preset(1, 1, ImageType.Icon)
"""! @brief Size and pixel ratio of the icon on disk"""
Smallest = Preset(5, 1, base=Image)
"""! @brief Smaller image size for UI icons"""
DisplayIcon = Preset(1, 1, ImageType.Icon, base=Icon)
"""! @brief Size and pixel ratio of the icon on disk"""
LibraryIcon = Preset(2.2, 1, ImageType.Icon, base=Icon)
"""! @brief Size and pixel ratio of the icon on disk"""

View file

@ -16,9 +16,18 @@ from PyQt5.QtWidgets import QApplication
from legendary.models.game import Game
from rare.lgndr.core import LegendaryCore
from rare.models.image import ImageSize
from rare.models.image import ImageSize, ImageType
from rare.models.signals import GlobalSignals
from rare.utils.paths import image_dir, resources_path, desktop_icon_suffix
from rare.utils.paths import (
image_dir,
image_dir_game,
image_tall_path,
image_wide_path,
image_icon_path,
resources_path,
desktop_icon_suffix,
desktop_icon_path,
)
# from requests_futures.sessions import FuturesSession
@ -67,53 +76,35 @@ class ImageManager(QObject):
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(6)
def __img_dir(self, app_name: str) -> Path:
return self.image_dir.joinpath(app_name)
@staticmethod
def __img_json(app_name: str) -> Path:
return image_dir_game(app_name).joinpath("image.json")
def __img_json(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("image.json")
@staticmethod
def __img_cache(app_name: str) -> Path:
return image_dir_game(app_name).joinpath("image.cache")
def __img_cache(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("image.cache")
def __img_tall_color(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("tall.png")
def __img_tall_gray(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("tall_uninstalled.png")
def __img_wide_color(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("wide.png")
def __img_wide_gray(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("wide_uninstalled.png")
def __img_logo(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath("logo.png")
def __img_icon(self, app_name: str) -> Path:
return self.__img_dir(app_name).joinpath(f"icon.{desktop_icon_suffix()}")
def __img_all(self, app_name: str) -> Tuple:
@staticmethod
def __img_all(app_name: str) -> Tuple:
return (
self.__img_icon(app_name),
self.__img_tall_color(app_name),
self.__img_tall_gray(app_name),
self.__img_wide_color(app_name),
self.__img_tall_gray(app_name),
image_tall_path(app_name),
image_tall_path(app_name, color=False),
image_wide_path(app_name),
image_wide_path(app_name, color=False),
image_icon_path(app_name),
image_icon_path(app_name, color=False),
desktop_icon_path(app_name),
)
def __prepare_download(self, game: Game, force: bool = False) -> Tuple[List, Dict]:
if force and self.__img_dir(game.app_name).exists():
if force and image_dir_game(game.app_name).exists():
for file in self.__img_all(game.app_name):
file.unlink(missing_ok=True)
if not self.__img_dir(game.app_name).is_dir():
self.__img_dir(game.app_name).mkdir()
if not image_dir_game(game.app_name).is_dir():
image_dir_game(game.app_name).mkdir()
# Load image checksums
if not self.__img_json(game.app_name).is_file():
@ -160,8 +151,8 @@ class ImageManager(QObject):
# 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"] = {"w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
json_data["scale"] = ImageSize.Tall.pixel_ratio
json_data["size"] = {"w": ImageSize.Tall.size.width(), "h": ImageSize.Tall.size.height()}
json.dump(json_data, open(self.__img_json(game.app_name), "w"))
else:
updates = [image for image in candidates if image["type"] in self.__img_types]
@ -214,12 +205,12 @@ class ImageManager(QObject):
logger.info(f"Downloading {image['type']} for {game.app_name} ({game.app_title})")
json_data[image["type"]] = image["md5"]
if image["type"] in self.__img_tall_types:
payload = {"resize": 1, "w": ImageSize.Image.size.width(), "h": ImageSize.Image.size.height()}
payload = {"resize": 1, "w": ImageSize.Tall.size.width(), "h": ImageSize.Tall.size.height()}
elif image["type"] in self.__img_wide_types:
payload = {"resize": 1, "w": ImageSize.ImageWide.size.width(), "h": ImageSize.ImageWide.size.height()}
payload = {"resize": 1, "w": ImageSize.Wide.size.width(), "h": ImageSize.Wide.size.height()}
else:
# Set the larger of the sizes for everything else
payload = {"resize": 1, "w": ImageSize.ImageWide.size.width(), "h": ImageSize.ImageWide.size.height()}
payload = {"resize": 1, "w": ImageSize.Wide.size.width(), "h": ImageSize.Wide.size.height()}
try:
# cache_data[image["type"]] = requests.get(image["url"], params=payload).content
cache_data[image["type"]] = requests.get(image["url"], params=payload, timeout=10).content
@ -241,8 +232,8 @@ class ImageManager(QObject):
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()}
json_data["scale"] = ImageSize.Tall.pixel_ratio
json_data["size"] = {"w": ImageSize.Tall.size.width(), "h": ImageSize.Tall.size.height()}
# write image.json
with open(self.__img_json(game.app_name), "w") as file:
@ -332,11 +323,11 @@ class ImageManager(QObject):
return image.scaled(preset.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
tall = convert_image(tall_data, logo_data, ImageSize.Image)
wide = convert_image(wide_data, logo_data, ImageSize.ImageWide)
tall = convert_image(tall_data, logo_data, ImageSize.Tall)
wide = convert_image(wide_data, logo_data, ImageSize.Wide)
icon = self.__convert_icon(tall)
icon.save(str(self.__img_icon(game.app_name)), format=desktop_icon_suffix().upper())
icon.save(desktop_icon_path(game.app_name).as_posix(), format=desktop_icon_suffix().upper())
def save_image(image: QImage, color_path: Path, gray_path: Path):
# this is not required if we ever want to re-apply the alpha channel
@ -344,14 +335,17 @@ class ImageManager(QObject):
# add the alpha channel back to the cover
image = image.convertToFormat(QImage.Format_ARGB32_Premultiplied)
image.save(color_path.as_posix(), format="PNG")
# quick way to convert to grayscale
# quick way to convert to grayscale, but keep the alpha channel
alpha = image.convertToFormat(QImage.Format_Alpha8)
image = image.convertToFormat(QImage.Format_Grayscale8)
# add the alpha channel back to the grayscale cover
image = image.convertToFormat(QImage.Format_ARGB32_Premultiplied)
image.setAlphaChannel(alpha)
image.save(gray_path.as_posix(), format="PNG")
save_image(tall, self.__img_tall_color(game.app_name), self.__img_tall_gray(game.app_name))
save_image(wide, self.__img_wide_color(game.app_name), self.__img_wide_gray(game.app_name))
save_image(icon, image_icon_path(game.app_name), image_icon_path(game.app_name, color=False))
save_image(tall, image_tall_path(game.app_name), image_tall_path(game.app_name, color=False))
save_image(wide, image_wide_path(game.app_name), image_wide_path(game.app_name, color=False))
def __compress(self, game: Game, data: Dict) -> None:
archive = open(self.__img_cache(game.app_name), "wb")
@ -395,11 +389,15 @@ class ImageManager(QObject):
self.threadpool.start(image_worker, priority)
def download_image_launch(
self, game: Game, callback: Callable[[], None], priority: int, force: bool = False
self, game: Game, callback: Callable[[Game], None], priority: int, force: bool = False
) -> None:
if self.__img_cache(game.app_name).is_file() and not force:
return
self.download_image(game, callback, priority, force)
def _callback():
callback(game)
self.download_image(game, _callback, priority, force)
def download_image_blocking(self, game: Game, force: bool = False) -> None:
updates, json_data = self.__prepare_download(game, force)
@ -408,44 +406,56 @@ class ImageManager(QObject):
if updates:
self.__download(updates, json_data, game, use_async=True)
@staticmethod
def __get_cover(
self, container: Union[Type[QPixmap], Type[QImage]], app_name: str, color: bool
container: Union[Type[QPixmap], Type[QImage]], app_name: str, preset: ImageSize.Preset, color: bool,
) -> Union[QPixmap, QImage]:
ret = container()
if not app_name:
raise RuntimeError("app_name is an empty string")
if color:
if self.__img_tall_color(app_name).is_file():
ret.load(self.__img_tall_color(app_name).as_posix())
if preset.orientation == ImageType.Icon:
if image_icon_path(app_name, color).is_file():
ret.load(image_icon_path(app_name, color).as_posix())
elif preset.orientation == ImageType.Tall:
if image_tall_path(app_name, color).is_file():
ret.load(image_tall_path(app_name, color).as_posix())
elif preset.orientation == ImageType.Wide:
if image_wide_path(app_name, color).is_file():
ret.load(image_wide_path(app_name, color).as_posix())
else:
if self.__img_tall_gray(app_name).is_file():
ret.load(self.__img_tall_gray(app_name).as_posix())
raise RuntimeError("Unknown image preset")
if not ret.isNull():
ret.setDevicePixelRatio(ImageSize.Image.pixel_ratio)
device = ImageSize.Preset(
divisor=preset.base.divisor,
pixel_ratio=QApplication.instance().devicePixelRatio(),
orientation=preset.base.orientation,
base=preset
)
ret.setDevicePixelRatio(preset.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)
ret = ret.scaled(device.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
ret.setDevicePixelRatio(device.pixel_ratio)
return ret
def get_pixmap(self, app_name: str, color: bool = True) -> QPixmap:
def get_pixmap(self, app_name: str, preset: ImageSize.Preset, 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 preset:
@param color: True to load the colored pixmap, False to load the grayscale
@return: QPixmap
"""
pixmap: QPixmap = self.__get_cover(QPixmap, app_name, color)
pixmap: QPixmap = self.__get_cover(QPixmap, app_name, preset, color)
return pixmap
def get_image(self, app_name: str, color: bool = True) -> QImage:
def get_image(self, app_name: str, preset: ImageSize.Preset, 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 preset:
@param color: True to load the colored image, False to load the grayscale
@return: QImage
"""
image: QImage = self.__get_cover(QImage, app_name, color)
image: QImage = self.__get_cover(QImage, app_name, preset, color)
return image

View file

@ -55,6 +55,22 @@ def image_dir() -> Path:
return data_dir().joinpath("images")
def image_dir_game(app_name: str) -> Path:
return image_dir().joinpath(app_name)
def image_tall_path(app_name: str, color: bool = True) -> Path:
return image_dir_game(app_name).joinpath("tall.png" if color else "tall_gray.png")
def image_wide_path(app_name: str, color: bool = True) -> Path:
return image_dir_game(app_name).joinpath("wide.png" if color else "wide_gray.png")
def image_icon_path(app_name: str, color: bool = True) -> Path:
return image_dir_game(app_name).joinpath("icon.png" if color else "icon_gray.png")
def log_dir() -> Path:
return cache_dir().joinpath("logs")
@ -105,19 +121,25 @@ __link_suffix = {
"icon": "png",
},
"Darwin": {
"link": "",
"icon": "icns",
"link": "",
"icon": "icns",
},
}
def desktop_links_supported() -> bool:
supported_systems = [k for (k, v) in __link_suffix.items() if v["link"]]
return platform.system() in supported_systems
def desktop_icon_suffix() -> str:
return __link_suffix[platform.system()]["icon"]
def desktop_icon_path(app_name: str) -> Path:
return image_dir_game(app_name).joinpath(f"desktop_icon.{desktop_icon_suffix()}")
__link_type = {
"desktop": desktop_dir(),
# lk: for some undocumented reason, on Windows we used the parent directory
@ -125,8 +147,11 @@ __link_type = {
"start_menu": applications_dir().parent if platform.system() == "Windows" else applications_dir(),
}
def desktop_link_types() -> List:
return list(__link_type.keys())
# fmt: on
@ -212,7 +237,7 @@ def create_desktop_link(app_name: str, app_title: str = "", link_name: str = "",
app_title = "Rare"
link_name = "Rare"
else:
icon_path = image_dir().joinpath(app_name, f"icon.{desktop_icon_suffix()}")
icon_path = desktop_icon_path(app_name)
if not app_title or not link_name:
logger.error("Missing app_title or link_name")
return False