diff --git a/misc/requirements.in b/misc/requirements.in
index af17172f5f..3da70743cd 100644
--- a/misc/requirements.in
+++ b/misc/requirements.in
@@ -4,7 +4,7 @@ setuptools-scm
requests < 3.0
PySide6-Essentials >= 6.8.1
QtAwesome
-legendary-gl @ https://github.com/RareDevs/legendary/archive/refs/tags/rare-1.12.0.zip
+legendary-gl @ https://github.com/RareDevs/legendary/archive/85d9ea9.zip
orjson
vdf @ https://github.com/solsticegamestudios/vdf/archive/be1f7220238022f8b29fe747f0b643f280bfdb6e.zip
pywin32 ; platform_system == "Windows"
diff --git a/pyproject.toml b/pyproject.toml
index fdacfb63f6..769d282bbd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ dependencies = [
"requests < 3.0",
"PySide6-Essentials >= 6.8.1",
"QtAwesome",
- "legendary-gl @ https://github.com/RareDevs/legendary/archive/refs/tags/rare-1.12.0.zip",
+ "legendary-gl @ git+https://github.com/RareDevs/legendary@rare/achievements",
"orjson",
"vdf @ https://github.com/solsticegamestudios/vdf/archive/be1f7220238022f8b29fe747f0b643f280bfdb6e.zip",
"pywin32 ; platform_system == 'Windows'",
diff --git a/rare/components/tabs/integrations/ubisoft_group.py b/rare/components/tabs/integrations/ubisoft_group.py
index b2041bdf97..2a3f8b587f 100644
--- a/rare/components/tabs/integrations/ubisoft_group.py
+++ b/rare/components/tabs/integrations/ubisoft_group.py
@@ -1,7 +1,6 @@
import time
import webbrowser
from logging import getLogger
-from typing import Optional
from legendary.models.game import Game
from PySide6.QtCore import QObject, QSize, Qt, QThreadPool, Signal, Slot
@@ -24,22 +23,21 @@
from rare.widgets.elide_label import ElideLabel
from rare.widgets.loading_widget import LoadingWidget
-logger = getLogger("Ubisoft")
+class UbiGetInfoWorkerSignals(QObject):
+ worker_finished = Signal(set, set, str)
-class UbiGetInfoWorker(Worker):
- class Signals(QObject):
- worker_finished = Signal(set, set, str)
+class UbiGetInfoWorker(Worker):
def __init__(self, core: LegendaryCore):
super(UbiGetInfoWorker, self).__init__()
- self.signals = UbiGetInfoWorker.Signals()
+ self.signals = UbiGetInfoWorkerSignals()
self.setAutoDelete(True)
self.core = core
def run_real(self) -> None:
try:
- with timelogger(logger, "Request external auths"):
+ with timelogger(self.logger, "Request external auths"):
external_auths = self.core.egs.get_external_auths()
for ext_auth in external_auths:
if ext_auth["type"] != "ubisoft":
@@ -50,34 +48,34 @@ def run_real(self) -> None:
self.signals.worker_finished.emit(set(), set(), "")
return
- with timelogger(logger, "Request uplay codes"):
+ with timelogger(self.logger, "Request uplay codes"):
uplay_keys = self.core.egs.store_get_uplay_codes()
key_list = uplay_keys["data"]["PartnerIntegration"]["accountUplayCodes"]
redeemed = {k["gameId"] for k in key_list if k["redeemedOnUplay"]}
if (entitlements := self.core.lgd.entitlements) is None:
- with timelogger(logger, "Request entitlements"):
+ with timelogger(self.logger, "Request entitlements"):
try:
entitlements = self.core.egs.get_user_entitlements_full()
- except AttributeError as e:
- logger.warning(e)
- entitlements = self.core.egs.get_user_entitlements()
+ except Exception as e:
+ self.logger.warning(e)
self.core.lgd.entitlements = entitlements
entitlements = {i["entitlementName"] for i in entitlements}
self.signals.worker_finished.emit(redeemed, entitlements, ubi_account_id)
except Exception as e:
- logger.error(e)
+ self.logger.error(e)
self.signals.worker_finished.emit(set(), set(), "error")
-class UbiConnectWorker(Worker):
- class Signals(QObject):
- linked = Signal(str)
+class UbiConnectWorkerSignals(QObject):
+ linked = Signal(str)
+
+class UbiConnectWorker(Worker):
def __init__(self, core: LegendaryCore, ubi_account_id, partner_link_id):
super(UbiConnectWorker, self).__init__()
- self.signals = UbiConnectWorker.Signals()
+ self.signals = UbiConnectWorkerSignals()
self.core = core
self.ubi_account_id = ubi_account_id
self.partner_link_id = partner_link_id
@@ -160,6 +158,8 @@ def worker_finished(self, error):
class UbisoftGroup(QGroupBox):
def __init__(self, rcore: RareCore, parent=None):
super(UbisoftGroup, self).__init__(parent=parent)
+ self.logger = getLogger(type(self).__name__)
+
self.rcore = rcore
self.core = rcore.core()
self.args = rcore.args()
@@ -167,7 +167,7 @@ def __init__(self, rcore: RareCore, parent=None):
self.setTitle(self.tr("Link Ubisoft Games"))
self.thread_pool = QThreadPool.globalInstance()
- self.worker: Optional[UbiGetInfoWorker] = None
+ self.worker: UbiGetInfoWorker = None
self.info_label = QLabel(parent=self)
self.info_label.setText(self.tr("Getting information about your redeemable Ubisoft games."))
@@ -192,7 +192,7 @@ def showEvent(self, a0: QShowEvent) -> None:
return super().showEvent(a0)
if self.worker is not None:
- return
+ return super().showEvent(a0)
for widget in self.findChildren(UbiLinkWidget, options=Qt.FindChildOption.FindDirectChildrenOnly):
widget.deleteLater()
@@ -201,14 +201,14 @@ def showEvent(self, a0: QShowEvent) -> None:
self.worker = UbiGetInfoWorker(self.core)
self.worker.signals.worker_finished.connect(self.show_ubi_games)
self.thread_pool.start(self.worker)
- super().showEvent(a0)
+ return super().showEvent(a0)
@Slot(set, set, str)
def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
self.worker = None
self.loading_widget.stop()
if not redeemed and ubi_account_id != "error":
- logger.error("No linked ubisoft account found! Link your accounts via your browser and try again.")
+ self.logger.error("No linked ubisoft account found! Link your accounts via your browser and try again.")
self.info_label.setText(self.tr("Your account is not linked with Ubisoft. Please link your account and try again."))
self.link_button.setEnabled(True)
elif ubi_account_id == "error":
@@ -254,7 +254,7 @@ def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
self.info_label.setText(
self.tr("You have {} games available to redeem.").format(len(uplay_games) - activated)
)
- logger.info(f"Found {len(uplay_games) - activated} game(s) to redeem.")
+ self.logger.info(f"Found {len(uplay_games) - activated} game(s) to redeem.")
for game in uplay_games:
widget = UbiLinkWidget(
diff --git a/rare/components/tabs/library/details/details.py b/rare/components/tabs/library/details/details.py
index 9d828de5a0..32568d2609 100644
--- a/rare/components/tabs/library/details/details.py
+++ b/rare/components/tabs/library/details/details.py
@@ -2,7 +2,7 @@
import platform
from hashlib import sha1
from logging import getLogger
-from typing import Optional, Tuple
+from typing import Dict, Optional, Tuple
from PySide6.QtCore import (
QCoreApplication,
@@ -14,6 +14,9 @@
from PySide6.QtGui import QFontMetrics, QHideEvent, QShowEvent
from PySide6.QtWidgets import (
QCheckBox,
+ QFrame,
+ QHBoxLayout,
+ QLabel,
QLineEdit,
QMessageBox,
QSizePolicy,
@@ -28,9 +31,11 @@
from rare.shared import RareCore
from rare.shared.workers import MoveInfoWorker, MoveWorker, VerifyWorker
from rare.ui.components.tabs.library.details.details import Ui_GameDetails
-from rare.utils.misc import format_size, qta_icon, style_hyperlink
+from rare.utils.misc import format_size, qta_icon, relative_date, style_hyperlink
+from rare.utils.paths import cache_dir
+from rare.utils.qt_requests import QtRequests
from rare.widgets.dialogs import ButtonDialog, game_title
-from rare.widgets.image_widget import ImageSize, ImageWidget
+from rare.widgets.image_widget import ImageSize, ImageWidget, LoadingImageWidget
from rare.widgets.side_tab import SideTabContents
logger = getLogger("GameInfo")
@@ -42,6 +47,7 @@ class GameDetails(QWidget, SideTabContents):
def __init__(self, rcore: RareCore, parent=None):
super(GameDetails, self).__init__(parent=parent)
+ self.implements_scrollarea = True
self.ui = Ui_GameDetails()
self.ui.setupUi(self)
# lk: set object names for CSS properties
@@ -67,12 +73,14 @@ def __init__(self, rcore: RareCore, parent=None):
self.rcore = rcore
self.core = rcore.core()
self.args = rcore.args()
+ self.net_manager = QtRequests(cache=str(cache_dir().joinpath("achievements")), parent=self)
self.rgame: Optional[RareGame] = None
self.image = ImageWidget(self)
self.image.setFixedSize(ImageSize.DisplayTall)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignmentFlag.AlignTop)
+ self.ui.left_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.ui.install_button.clicked.connect(self.__on_install)
self.ui.import_button.clicked.connect(self.__on_import)
@@ -96,8 +104,21 @@ def __init__(self, rcore: RareCore, parent=None):
self.ui.add_tag_button.setIcon(qta_icon("mdi.plus"))
self.ui.add_tag_button.clicked.connect(self.__on_tag_add)
+ ach_progress_layout = QVBoxLayout(self.ui.ach_progress_page)
+ ach_progress_layout.setContentsMargins(0, 0, 0, 0)
+ ach_progress_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+ ach_completed_layout = QVBoxLayout(self.ui.ach_completed_page)
+ ach_completed_layout.setContentsMargins(0, 0, 0, 0)
+ ach_completed_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+ ach_uninitiated_layout = QVBoxLayout(self.ui.ach_uninitiated_page)
+ ach_uninitiated_layout.setContentsMargins(0, 0, 0, 0)
+ ach_uninitiated_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+ ach_hidden_layout = QVBoxLayout(self.ui.ach_hidden_page)
+ ach_hidden_layout.setContentsMargins(0, 0, 0, 0)
+ ach_hidden_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
+
# lk: hide unfinished things
- self.ui.description_label.setVisible(False)
+ self.ui.description_field.setVisible(False)
self.ui.requirements_group.setVisible(False)
@Slot()
@@ -417,9 +438,71 @@ def update_game(self, rgame: RareGame):
else:
self.ui.install_button.setText(self.tr("Install"))
+ self.ui.description_field.setText(rgame.game.metadata['description'])
+
+ for page in (
+ self.ui.ach_progress_page, self.ui.ach_completed_page, self.ui.ach_uninitiated_page, self.ui.ach_hidden_page,
+ ):
+ for w in page.findChildren(AchievementWidget, options=Qt.FindChildOption.FindDirectChildrenOnly):
+ page.layout().removeWidget(w)
+ w.deleteLater()
+
+ if ach := rgame.achievements:
+ self.ui.progress_field.setText(f"{ach.user_unlocked}/{ach.total_achievements}")
+ self.ui.exp_field.setText(f"{ach.user_xp}/{ach.total_product_xp}")
+
+ for group, page in zip(
+ (ach.hidden, ach.uninitiated, ach.completed, ach.in_progress, ),
+ (self.ui.ach_hidden_page, self.ui.ach_uninitiated_page, self.ui.ach_completed_page, self.ui.ach_progress_page, )
+ ):
+ self.ui.achievements_toolbox.setItemEnabled(self.ui.achievements_toolbox.indexOf(page), bool(group))
+ if bool(group):
+ self.ui.achievements_toolbox.setCurrentWidget(page)
+ for item in group:
+ page.layout().addWidget(AchievementWidget(self.net_manager, item), alignment=Qt.AlignmentFlag.AlignTop)
+ else:
+ self.ui.progress_field.setText(self.tr("No data"))
+ self.ui.exp_field.setText(self.tr("No data"))
+ self.ui.achievements_group.setVisible(bool(ach))
+
self.rgame = rgame
+class AchievementWidget(QFrame):
+ def __init__(self, manager: QtRequests, achievement: Dict, parent=None):
+ super().__init__(parent=parent)
+ self.setFrameShape(QFrame.Shape.StyledPanel)
+ self.setFrameShadow(QFrame.Shadow.Sunken)
+
+ image = LoadingImageWidget(manager, parent=self)
+ image.setFixedSize(ImageSize.LibraryIcon)
+ image.fetchPixmap(achievement['icon_link'])
+
+ title = QLabel(
+ f"{achievement['display_name']}"
+ f" ({achievement['xp']} XP)",
+ parent=self
+ )
+ title.setWordWrap(True)
+ description = QLabel(achievement['description'], parent=self)
+ description.setWordWrap(True)
+ unlock_date = achievement['unlock_date'].astimezone() if achievement['unlock_date'] else None
+ unlock_date_str = f" ( On: {relative_date(unlock_date)} )" if unlock_date else ""
+ progress = QLabel(f"Progress: {achievement['progress'] * 100:,.2f}% {unlock_date_str}", parent=self)
+ if unlock_date:
+ progress.setToolTip(str(unlock_date))
+
+ right_layout = QVBoxLayout()
+ right_layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignTop)
+ right_layout.addWidget(description, alignment=Qt.AlignmentFlag.AlignTop, stretch=1)
+ right_layout.addWidget(progress, alignment=Qt.AlignmentFlag.AlignBottom)
+
+ main_layout = QHBoxLayout(self)
+ main_layout.setContentsMargins(3, 3, 3, 3)
+ main_layout.addWidget(image)
+ main_layout.addLayout(right_layout, stretch=1)
+
+
class GameTagCheckBox(QCheckBox):
checkStateChangedData = Signal(Qt.CheckState, str)
diff --git a/rare/components/tabs/store/widgets/details.py b/rare/components/tabs/store/widgets/details.py
index f98b255daa..1ad4100696 100644
--- a/rare/components/tabs/store/widgets/details.py
+++ b/rare/components/tabs/store/widgets/details.py
@@ -22,10 +22,9 @@
from rare.ui.components.tabs.store.details import Ui_StoreDetailsWidget
from rare.utils.misc import qta_icon
from rare.widgets.elide_label import ElideLabel
+from rare.widgets.image_widget import LoadingSpinnerImageWidget
from rare.widgets.side_tab import SideTabContents, SideTabWidget
-from .image import LoadingImageWidget
-
logger = getLogger("StoreDetails")
@@ -45,7 +44,7 @@ def __init__(self, installed: List, store_api: StoreAPI, parent=None):
self.installed = installed
self.catalog_offer: CatalogOfferModel = None
- self.image = LoadingImageWidget(store_api.cached_manager, self)
+ self.image = LoadingSpinnerImageWidget(store_api.cached_manager, self)
self.image.setFixedSize(ImageSize.DisplayTall)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignmentFlag.AlignTop)
self.ui.left_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
diff --git a/rare/components/tabs/store/widgets/image.py b/rare/components/tabs/store/widgets/icon_widget.py
similarity index 69%
rename from rare/components/tabs/store/widgets/image.py
rename to rare/components/tabs/store/widgets/icon_widget.py
index ab0200057a..c7905ae7dd 100644
--- a/rare/components/tabs/store/widgets/image.py
+++ b/rare/components/tabs/store/widgets/icon_widget.py
@@ -1,8 +1,4 @@
from PySide6.QtCore import Qt
-from PySide6.QtGui import (
- QImage,
- QPixmap,
-)
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
@@ -12,10 +8,6 @@
QWidget,
)
-from rare.utils.qt_requests import QtRequests
-from rare.widgets.image_widget import ImageWidget
-from rare.widgets.loading_widget import LoadingWidget
-
class IconWidget(object):
def __init__(self):
@@ -81,36 +73,3 @@ def setupUi(self, widget: QWidget):
image_layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding))
image_layout.addWidget(self.mini_widget)
widget.setLayout(image_layout)
-
-
-class LoadingImageWidget(ImageWidget):
- def __init__(self, manager: QtRequests, parent=None):
- super(LoadingImageWidget, self).__init__(parent=parent)
- self.ui = IconWidget()
- self.spinner = LoadingWidget(parent=self)
- self.spinner.setVisible(False)
- self.manager = manager
-
- def fetchPixmap(self, url):
- self.setPixmap(QPixmap())
- self.spinner.setFixedSize(self._image_size.size)
- self.spinner.start()
- self.manager.get(
- url,
- self.__on_image_ready,
- params={
- "resize": 1,
- "w": self._image_size.base.size.width(),
- "h": self._image_size.base.size.height(),
- },
- )
-
- def __on_image_ready(self, data):
- cover = QImage()
- cover.loadFromData(data)
- # cover = cover.scaled(self._image_size.size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
- cover.setDevicePixelRatio(self._image_size.base.pixel_ratio)
- cover = cover.convertToFormat(QImage.Format.Format_ARGB32_Premultiplied)
- cover = QPixmap(cover)
- self.setPixmap(cover)
- self.spinner.stop()
diff --git a/rare/components/tabs/store/widgets/items.py b/rare/components/tabs/store/widgets/items.py
index 2f15030d92..3e2528df9c 100644
--- a/rare/components/tabs/store/widgets/items.py
+++ b/rare/components/tabs/store/widgets/items.py
@@ -8,17 +8,19 @@
from rare.models.image import ImageSize
from rare.utils.misc import qta_icon
from rare.utils.qt_requests import QtRequests
+from rare.widgets.image_widget import LoadingSpinnerImageWidget
-from .image import LoadingImageWidget
+from .icon_widget import IconWidget
logger = getLogger("StoreWidgets")
-class ItemWidget(LoadingImageWidget):
+class ItemWidgetSpinner(LoadingSpinnerImageWidget):
show_details = Signal(CatalogOfferModel)
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None):
- super(ItemWidget, self).__init__(manager, parent=parent)
+ super(ItemWidgetSpinner, self).__init__(manager, parent=parent)
+ self.ui = IconWidget()
self.catalog_game = catalog_game
def mousePressEvent(self, a0: QMouseEvent) -> None:
@@ -29,7 +31,7 @@ def mousePressEvent(self, a0: QMouseEvent) -> None:
a0.accept()
-class StoreItemWidget(ItemWidget):
+class StoreItemWidget(ItemWidgetSpinner):
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None):
super(StoreItemWidget, self).__init__(manager, catalog_game, parent=parent)
self.setFixedSize(ImageSize.DisplayWide)
@@ -74,7 +76,7 @@ def init_ui(self, game: CatalogOfferModel):
# logger.info(", ".join([img["type"] for img in json_info["keyImages"]]))
-class SearchItemWidget(ItemWidget):
+class SearchItemWidget(ItemWidgetSpinner):
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None):
super(SearchItemWidget, self).__init__(manager, catalog_game, parent=parent)
self.setFixedSize(ImageSize.LibraryTall)
@@ -97,7 +99,7 @@ def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=
self.ui.discount_label.setVisible(False)
-class WishlistItemWidget(ItemWidget):
+class WishlistItemWidget(ItemWidgetSpinner):
delete_from_wishlist = Signal(CatalogOfferModel)
def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None):
diff --git a/rare/models/game.py b/rare/models/game.py
index f2cbdf130c..9d840b68dd 100644
--- a/rare/models/game.py
+++ b/rare/models/game.py
@@ -1,6 +1,8 @@
import json
import os
import platform
+import re
+from argparse import Namespace
from dataclasses import dataclass, field
from datetime import datetime, timezone
from threading import Lock
@@ -30,6 +32,7 @@ class Metadata:
queued: bool = False
queue_pos: Optional[int] = None
last_played: datetime = datetime.min.replace(tzinfo=timezone.utc)
+ achievements_date: datetime = datetime.min.replace(tzinfo=timezone.utc)
grant_date: datetime = datetime.min.replace(tzinfo=timezone.utc)
steam_appid: Optional[str] = None
steam_grade: Optional[str] = None
@@ -49,6 +52,7 @@ def from_dict(cls, data: Dict):
queued=data.get("queued", False),
queue_pos=data.get("queue_pos", None),
last_played=RareGame.Metadata.parse_date(data.get("last_played", "")),
+ achievements_date=RareGame.Metadata.parse_date(data.get("achievements_date", "")),
grant_date=RareGame.Metadata.parse_date(data.get("grant_date", "")),
steam_appid=str(appid) if (appid := data.get("steam_appid", "")) else None,
steam_grade=data.get("steam_grade", None),
@@ -63,6 +67,7 @@ def __dict__(self):
queued=self.queued,
queue_pos=self.queue_pos,
last_played=self.last_played.isoformat() if self.last_played else datetime.min.replace(tzinfo=timezone.utc),
+ achievements_date=self.last_played.isoformat() if self.achievements_date else datetime.min.replace(tzinfo=timezone.utc),
grant_date=self.grant_date.isoformat() if self.grant_date else datetime.min.replace(tzinfo=timezone.utc),
steam_appid=str(self.steam_appid) if self.steam_appid else None,
steam_grade=self.steam_grade,
@@ -464,6 +469,50 @@ def save_path(self, path: str) -> None:
self.store_igame()
self.signals.widget.update.emit()
+ @property
+ def achievements(self) -> Optional[Namespace]:
+ if not self.game.achievements or not self.game.achievements.achievements:
+ self.logger.info("No achievements found for %s (%s)", self.app_name, self.app_title)
+ return None
+
+ # if self.game.achievements is None:
+ # _ = self.core.get_game(self.app_name, update_meta=True, platform=self.default_platform)
+ # try:
+ # ret = self.core.get_achievements(self.game, update=True)
+ # except Exception as e:
+ # ret = self.core.get_achievements(self.game, update=False)
+
+ update = not self.core.lgd.achievements or not self.core.lgd.achievements.get(self.app_name, None)
+ update = update or self.metadata.achievements_date < self.metadata.last_played
+ if update:
+ self.logger.info("Updating achievements for %s (%s)", self.app_name, self.app_title)
+ ret = self.core.get_achievements(self.game, update=update)
+ self.metadata.achievements_date = self.metadata.last_played
+ self.__save_metadata()
+ if ret is not None:
+ ret = Namespace(**ret)
+ return ret
+
+ @property
+ def eulas(self) -> List:
+ eulas = self.game.metadata.get('eulaIds') or ['$']
+
+ pattern = r'\w+'
+ keys = []
+ for eula in eulas:
+ keys += re.findall(pattern, eula)
+
+ not_accepted_eulas = []
+ for key in keys:
+ # if args.skip_epic and key == 'egstore':
+ # continue
+ self.logger.debug(f'Fetching eula status for "{key}"')
+ eula = self.core.egs.eula_get_status(key)
+ if eula:
+ not_accepted_eulas.append(eula)
+
+ return not_accepted_eulas
+
def reset_steam_date(self):
self.metadata.steam_date = datetime.min.replace(tzinfo=timezone.utc)
self.signals.widget.update.emit()
@@ -515,7 +564,7 @@ def grant_date(self, force=False) -> datetime:
grant_date = (
datetime.fromisoformat(entitlement["grantDate"].replace("Z", "+00:00"))
if entitlement
- else datetime.min.replace(tzinfo=timezone.utc)
+ else datetime.fromisoformat(self.game.metadata["creationDate"].replace("Z", "+00:00"))
)
self.metadata.grant_date = grant_date
self.__save_metadata()
diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py
index 4393cabad0..4c2dad2adc 100644
--- a/rare/shared/workers/fetch.py
+++ b/rare/shared/workers/fetch.py
@@ -13,6 +13,11 @@
from .worker import Worker
+class FetchWorkerSignals(QObject):
+ progress = Signal(int, str)
+ result = Signal(object, int)
+
+
class FetchWorker(Worker):
class Result(IntEnum):
ERROR = 0
@@ -20,13 +25,9 @@ class Result(IntEnum):
ENTITLEMENTS = 2
STEAMAPPIDS = 3
- class Signals(QObject):
- progress = Signal(int, str)
- result = Signal(object, int)
-
def __init__(self, settings: RareAppSettings, core: LegendaryCore, args: Namespace):
super(FetchWorker, self).__init__()
- self.signals = FetchWorker.Signals()
+ self.signals = FetchWorkerSignals()
self.settings = settings
self.core = core
self.args = args
@@ -56,9 +57,8 @@ def run_real(self):
with timelogger(self.logger, "Request entitlements"):
try:
entitlements = self.core.egs.get_user_entitlements_full()
- except AttributeError as e:
+ except Exception as e:
self.logger.warning(e)
- entitlements = self.core.egs.get_user_entitlements()
self.core.lgd.entitlements = entitlements
self.logger.info("Entitlements: %s", len(list(entitlements)))
self.signals.result.emit(entitlements, FetchWorker.Result.ENTITLEMENTS)
diff --git a/rare/ui/components/tabs/library/details/details.py b/rare/ui/components/tabs/library/details/details.py
index d4cda99ada..c7a74d1414 100644
--- a/rare/ui/components/tabs/library/details/details.py
+++ b/rare/ui/components/tabs/library/details/details.py
@@ -3,12 +3,12 @@
################################################################################
## Form generated from reading UI file 'details.ui'
##
-## Created by: Qt User Interface Compiler version 6.9.1
+## Created by: Qt User Interface Compiler version 6.10.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
-from PySide6.QtCore import QCoreApplication, QSize, Qt
+from PySide6.QtCore import QCoreApplication, QRect, QSize, Qt
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QFormLayout,
@@ -24,6 +24,7 @@
QSpacerItem,
QStackedWidget,
QTextBrowser,
+ QToolBox,
QVBoxLayout,
QWidget,
)
@@ -33,7 +34,7 @@ class Ui_GameDetails(object):
def setupUi(self, GameDetails):
if not GameDetails.objectName():
GameDetails.setObjectName(u"GameDetails")
- GameDetails.resize(795, 404)
+ GameDetails.resize(888, 603)
GameDetails.setWindowTitle(u"GameDetails")
self.main_layout = QHBoxLayout(GameDetails)
self.main_layout.setObjectName(u"main_layout")
@@ -324,11 +325,11 @@ def setupUi(self, GameDetails):
self.right_layout.addLayout(self.details_layout, 0, 0, 1, 1)
- self.description_label = QTextBrowser(GameDetails)
- self.description_label.setObjectName(u"description_label")
- self.description_label.setOpenExternalLinks(True)
+ self.description_field = QTextBrowser(GameDetails)
+ self.description_field.setObjectName(u"description_field")
+ self.description_field.setOpenExternalLinks(True)
- self.right_layout.addWidget(self.description_label, 0, 1, 1, 1)
+ self.right_layout.addWidget(self.description_field, 1, 0, 1, 1)
self.requirements_group = QFrame(GameDetails)
self.requirements_group.setObjectName(u"requirements_group")
@@ -336,9 +337,65 @@ def setupUi(self, GameDetails):
self.requirements_group.setFrameShadow(QFrame.Shadow.Sunken)
self.requirements_layout = QHBoxLayout(self.requirements_group)
self.requirements_layout.setObjectName(u"requirements_layout")
- self.requirements_layout.setContentsMargins(0, 0, 0, 0)
- self.right_layout.addWidget(self.requirements_group, 1, 0, 1, 2, Qt.AlignmentFlag.AlignBottom)
+ self.right_layout.addWidget(self.requirements_group, 2, 0, 2, 2)
+
+ self.achievements_group = QWidget(GameDetails)
+ self.achievements_group.setObjectName(u"achievements_group")
+ self.achievements_layout = QVBoxLayout(self.achievements_group)
+ self.achievements_layout.setObjectName(u"achievements_layout")
+ self.achievements_layout.setContentsMargins(0, 0, 0, 0)
+ self.achievement_stats_layout = QHBoxLayout()
+ self.achievement_stats_layout.setObjectName(u"achievement_stats_layout")
+ self.achievement_stats_layout.setContentsMargins(0, -1, 0, -1)
+ self.progress_label = QLabel(self.achievements_group)
+ self.progress_label.setObjectName(u"progress_label")
+ self.progress_label.setFont(font)
+
+ self.achievement_stats_layout.addWidget(self.progress_label)
+
+ self.progress_field = QLabel(self.achievements_group)
+ self.progress_field.setObjectName(u"progress_field")
+
+ self.achievement_stats_layout.addWidget(self.progress_field)
+
+ self.exp_label = QLabel(self.achievements_group)
+ self.exp_label.setObjectName(u"exp_label")
+ self.exp_label.setFont(font)
+
+ self.achievement_stats_layout.addWidget(self.exp_label)
+
+ self.exp_field = QLabel(self.achievements_group)
+ self.exp_field.setObjectName(u"exp_field")
+
+ self.achievement_stats_layout.addWidget(self.exp_field)
+
+ self.achievement_stats_layout.setStretch(1, 1)
+ self.achievement_stats_layout.setStretch(3, 1)
+
+ self.achievements_layout.addLayout(self.achievement_stats_layout)
+
+ self.achievements_toolbox = QToolBox(self.achievements_group)
+ self.achievements_toolbox.setObjectName(u"achievements_toolbox")
+ self.ach_progress_page = QWidget()
+ self.ach_progress_page.setObjectName(u"ach_progress_page")
+ self.ach_progress_page.setGeometry(QRect(0, 0, 320, 419))
+ self.achievements_toolbox.addItem(self.ach_progress_page, u"In progress")
+ self.ach_completed_page = QWidget()
+ self.ach_completed_page.setObjectName(u"ach_completed_page")
+ self.ach_completed_page.setGeometry(QRect(0, 0, 320, 419))
+ self.achievements_toolbox.addItem(self.ach_completed_page, u"Completed")
+ self.ach_uninitiated_page = QWidget()
+ self.ach_uninitiated_page.setObjectName(u"ach_uninitiated_page")
+ self.achievements_toolbox.addItem(self.ach_uninitiated_page, u"Uninitiated")
+ self.ach_hidden_page = QWidget()
+ self.ach_hidden_page.setObjectName(u"ach_hidden_page")
+ self.achievements_toolbox.addItem(self.ach_hidden_page, u"Hidden")
+
+ self.achievements_layout.addWidget(self.achievements_toolbox)
+
+
+ self.right_layout.addWidget(self.achievements_group, 0, 1, 2, 1)
self.right_layout.setRowStretch(1, 1)
self.right_layout.setColumnStretch(1, 1)
@@ -352,6 +409,7 @@ def setupUi(self, GameDetails):
self.actions_stack.setCurrentIndex(0)
self.verify_stack.setCurrentIndex(0)
self.move_stack.setCurrentIndex(0)
+ self.achievements_toolbox.setCurrentIndex(0)
# setupUi
@@ -376,6 +434,14 @@ def retranslateUi(self, GameDetails):
self.uninstall_button.setText(QCoreApplication.translate("GameDetails", u"Uninstall", None))
self.install_button.setText(QCoreApplication.translate("GameDetails", u"Install", None))
self.import_button.setText(QCoreApplication.translate("GameDetails", u"Import", None))
+ self.progress_label.setText(QCoreApplication.translate("GameDetails", u"Progress:", None))
+ self.progress_field.setText(QCoreApplication.translate("GameDetails", u"progress_error", None))
+ self.exp_label.setText(QCoreApplication.translate("GameDetails", u"Experience:", None))
+ self.exp_field.setText(QCoreApplication.translate("GameDetails", u"xp_error", None))
+ self.achievements_toolbox.setItemText(self.achievements_toolbox.indexOf(self.ach_progress_page), QCoreApplication.translate("GameDetails", u"In progress", None))
+ self.achievements_toolbox.setItemText(self.achievements_toolbox.indexOf(self.ach_completed_page), QCoreApplication.translate("GameDetails", u"Completed", None))
+ self.achievements_toolbox.setItemText(self.achievements_toolbox.indexOf(self.ach_uninitiated_page), QCoreApplication.translate("GameDetails", u"Uninitiated", None))
+ self.achievements_toolbox.setItemText(self.achievements_toolbox.indexOf(self.ach_hidden_page), QCoreApplication.translate("GameDetails", u"Hidden", None))
pass
# retranslateUi
diff --git a/rare/ui/components/tabs/library/details/details.ui b/rare/ui/components/tabs/library/details/details.ui
index 7c0000688b..c3369b32f2 100644
--- a/rare/ui/components/tabs/library/details/details.ui
+++ b/rare/ui/components/tabs/library/details/details.ui
@@ -6,8 +6,8 @@
0
0
- 795
- 404
+ 842
+ 603
@@ -53,7 +53,7 @@
-
-
+
24
@@ -546,14 +546,14 @@
- -
-
+
-
+
true
- -
+
-
QFrame::Shape::StyledPanel
@@ -561,7 +561,12 @@
QFrame::Shadow::Sunken
-
+
+
+
+ -
+
+
0
@@ -574,6 +579,113 @@
0
+
-
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ true
+
+
+
+ Progress:
+
+
+
+ -
+
+
+ progress_error
+
+
+
+ -
+
+
+
+ true
+
+
+
+ Experience:
+
+
+
+ -
+
+
+ xp_error
+
+
+
+
+
+ -
+
+
+ 0
+
+
+
+
+ 0
+ 0
+ 274
+ 419
+
+
+
+ In progress
+
+
+
+
+
+ 0
+ 0
+ 274
+ 419
+
+
+
+ Completed
+
+
+
+
+
+ 0
+ 0
+ 274
+ 419
+
+
+
+ Uninitiated
+
+
+
+
+
+ 0
+ 0
+ 274
+ 419
+
+
+
+ Hidden
+
+
+
+
diff --git a/rare/utils/misc.py b/rare/utils/misc.py
index e32a8de3bc..e81cb4bda4 100644
--- a/rare/utils/misc.py
+++ b/rare/utils/misc.py
@@ -1,4 +1,5 @@
import os
+from datetime import UTC, datetime
from enum import IntEnum
from logging import getLogger
from typing import Dict, Iterable, Tuple, Type, Union
@@ -171,6 +172,30 @@ def format_size(b: Union[int, float]) -> str:
if b < 1024:
return f"{b:.2f} {s}B"
b /= 1024
+ return str(b)
+
+
+def relative_date(date):
+ diff = datetime.now(UTC) - date
+ s = diff.seconds
+ if diff.days > 7 or diff.days < 0:
+ return date.strftime('%d %b %y')
+ elif diff.days == 1:
+ return '1 day ago'
+ elif diff.days > 1:
+ return '{} days ago'.format(diff.days)
+ elif s <= 1:
+ return 'just now'
+ elif s < 60:
+ return '{} seconds ago'.format(s)
+ elif s < 120:
+ return '1 minute ago'
+ elif s < 3600:
+ return '{} minutes ago'.format(s//60)
+ elif s < 7200:
+ return '1 hour ago'
+ else:
+ return '{} hours ago'.format(s//3600)
def qta_icon(icn_str: str, fallback: str = None, **kwargs):
diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py
index d06c1f87fe..a9a0431592 100644
--- a/rare/utils/qt_requests.py
+++ b/rare/utils/qt_requests.py
@@ -34,18 +34,18 @@ class QtRequests(QObject):
def __init__(self, cache: str = None, token: str = None, parent=None):
super(QtRequests, self).__init__(parent=parent)
- self.log = getLogger(f"{type(self).__name__}_{type(parent).__name__}")
- self.manager = QNetworkAccessManager(self)
- self.manager.finished.connect(self.__on_finished)
- self.cache = None
+ self.logger = getLogger(f"{type(self).__name__}_{type(parent).__name__}")
+ self._manager = QNetworkAccessManager(self)
+ self._manager.finished.connect(self.__on_finished)
+ self._cache = None
if cache is not None:
- self.log.debug("Using cache dir %s", cache)
- self.cache = QNetworkDiskCache(self)
- self.cache.setCacheDirectory(cache)
- self.manager.setCache(self.cache)
+ self.logger.debug("Using cache dir %s", cache)
+ self._cache = QNetworkDiskCache(self)
+ self._cache.setCacheDirectory(cache)
+ self._manager.setCache(self._cache)
if token is not None:
- self.log.debug("Manager is authorized")
- self.token = token
+ self.logger.debug("Manager is authorized")
+ self._token = token
self.__active_requests: Dict[QNetworkReply, RequestQueueItem] = {}
@@ -69,19 +69,19 @@ def __prepare_request(self, item: RequestQueueItem) -> QNetworkRequest:
QNetworkRequest.Attribute.RedirectPolicyAttribute,
QNetworkRequest.RedirectPolicy.NoLessSafeRedirectPolicy,
)
- if self.cache is not None:
+ if self._cache is not None:
request.setAttribute(
QNetworkRequest.Attribute.CacheLoadControlAttribute,
QNetworkRequest.CacheLoadControl.PreferCache,
)
- if self.token is not None:
- request.setRawHeader(b"Authorization", self.token.encode())
+ if self._token is not None:
+ request.setRawHeader(b"Authorization", self._token.encode())
return request
def __post(self, item: RequestQueueItem):
request = self.__prepare_request(item)
payload = orjson.dumps(item.payload)
- reply = self.manager.post(request, payload)
+ reply = self._manager.post(request, payload)
reply.errorOccurred.connect(self.__on_error)
self.__active_requests[reply] = item
@@ -91,7 +91,7 @@ def post(self, url: str, handler: RequestHandler, payload: dict):
def __get(self, item: RequestQueueItem):
request = self.__prepare_request(item)
- reply = self.manager.get(request)
+ reply = self._manager.get(request)
reply.errorOccurred.connect(self.__on_error)
self.__active_requests[reply] = item
@@ -107,7 +107,7 @@ def get(
self.__get(item)
def __on_error(self, error: QNetworkReply.NetworkError) -> None:
- self.log.error(error)
+ self.logger.error(error)
@staticmethod
def __parse_content_type(header) -> Tuple[str, str]:
@@ -120,11 +120,11 @@ def __parse_content_type(header) -> Tuple[str, str]:
def __on_finished(self, reply: QNetworkReply):
item = self.__active_requests.pop(reply, None)
if item is None:
- self.log.error("QNetworkReply: %s without associated item", reply.url().toString())
+ self.logger.error("QNetworkReply: %s without associated item", reply.url().toString())
reply.deleteLater()
return
if reply.error() != QNetworkReply.NetworkError.NoError:
- self.log.error(reply.errorString())
+ self.logger.error(reply.errorString())
else:
mimetype, charset = self.__parse_content_type(reply.header(QNetworkRequest.KnownHeaders.ContentTypeHeader))
maintype, subtype = mimetype.split("/")
diff --git a/rare/widgets/image_widget.py b/rare/widgets/image_widget.py
index c1ad3eb58e..2ad859060e 100644
--- a/rare/widgets/image_widget.py
+++ b/rare/widgets/image_widget.py
@@ -1,10 +1,11 @@
from enum import Enum
-from typing import Optional, Tuple, Union
+from typing import Dict, Optional, Tuple, Union
from PySide6.QtCore import QRectF, QSize, Qt
from PySide6.QtGui import (
QBrush,
QColor,
+ QImage,
QLinearGradient,
QPainter,
QPainterPath,
@@ -16,6 +17,9 @@
from PySide6.QtWidgets import QWidget
from rare.models.image import ImageSize
+from rare.utils.qt_requests import QtRequests
+
+from .loading_widget import LoadingWidget
OverlayPath = Tuple[QPainterPath, Union[QColor, QLinearGradient]]
@@ -154,4 +158,47 @@ def paintEvent(self, a0: QPaintEvent) -> None:
painter.end()
-__all__ = ["ImageSize", "ImageWidget"]
+class LoadingImageWidget(ImageWidget):
+ def __init__(self, manager: QtRequests, parent=None):
+ super(LoadingImageWidget, self).__init__(parent=parent)
+ self.manager = manager
+
+ def fetchPixmap(self, url: str, params: Dict = None):
+ self.setPixmap(QPixmap())
+ self.manager.get(url, self._on_image_ready, params=params)
+
+ def _on_image_ready(self, data):
+ cover = QImage()
+ cover.loadFromData(data)
+ cover.setDevicePixelRatio(self._image_size.base.pixel_ratio)
+ if cover.size() != self._image_size.base.size:
+ cover = cover.scaled(
+ self._image_size.base.size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
+ )
+ cover = cover.convertToFormat(QImage.Format.Format_ARGB32_Premultiplied)
+ cover = QPixmap(cover)
+ self.setPixmap(cover)
+
+
+class LoadingSpinnerImageWidget(LoadingImageWidget):
+ def __init__(self, manager: QtRequests, parent=None):
+ super(LoadingSpinnerImageWidget, self).__init__(manager, parent=parent)
+ self.spinner = LoadingWidget(parent=self)
+ self.spinner.setVisible(False)
+
+ def fetchPixmap(self, url: str, params: Dict = None):
+ self.spinner.start()
+ self.spinner.setFixedSize(self._image_size.size)
+ params = {
+ "resize": 1,
+ "w": self._image_size.base.size.width(),
+ "h": self._image_size.base.size.height(),
+ } if not params else params
+ super().fetchPixmap(url, params=params)
+
+ def _on_image_ready(self, data):
+ super()._on_image_ready(data)
+ self.spinner.stop()
+
+
+__all__ = ["ImageSize", "ImageWidget", "LoadingImageWidget", "LoadingSpinnerImageWidget"]