From 9ec349e2d1608fcfc63b6f1a2cdbf864e4f7d50d Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:29:39 +0200 Subject: [PATCH] WIP --- rare/components/tabs/store/__init__.py | 11 +- .../tabs/store/api/graphql/schema.graphql | 4 +- .../tabs/store/api/models/diesel.py | 12 +- .../tabs/store/api/models/response.py | 225 +++++++++++------- .../components/tabs/store/api/models/utils.py | 2 +- rare/components/tabs/store/landing.py | 93 +++----- rare/components/tabs/store/search.py | 20 +- rare/components/tabs/store/store_api.py | 16 +- rare/components/tabs/store/widgets/details.py | 83 +++---- rare/components/tabs/store/widgets/items.py | 24 +- rare/components/tabs/store/wishlist.py | 4 +- rare/ui/components/tabs/store/details.py | 8 +- rare/ui/components/tabs/store/details.ui | 49 ++-- rare/utils/qt_requests.py | 2 +- 14 files changed, 270 insertions(+), 283 deletions(-) diff --git a/rare/components/tabs/store/__init__.py b/rare/components/tabs/store/__init__.py index 273d2293..c011ae45 100644 --- a/rare/components/tabs/store/__init__.py +++ b/rare/components/tabs/store/__init__.py @@ -28,21 +28,15 @@ class StoreTab(SideTabWidget): self.landing = LandingPage(self.api, parent=self) self.landing_index = self.addTab(self.landing, self.tr("Store")) - self.search = SearchPage(self.core, self.api, parent=self) + self.search = SearchPage(self.api, parent=self) self.search_index = self.addTab(self.search, self.tr("Search")) self.wishlist = WishlistPage(self.api, parent=self) self.wishlist_index = self.addTab(self.wishlist, self.tr("Wishlist")) - self.api.update_wishlist.connect(self.update_wishlist) - - self.previous_index = self.landing_index - def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous() or self.init: return super().showEvent(a0) - # self.landing.load() - # self.wishlist.update_wishlist() self.init = True return super().showEvent(a0) @@ -51,6 +45,3 @@ class StoreTab(SideTabWidget): return super().hideEvent(a0) # TODO: Implement store unloading return super().hideEvent(a0) - - def update_wishlist(self): - self.landing.update_wishlist() diff --git a/rare/components/tabs/store/api/graphql/schema.graphql b/rare/components/tabs/store/api/graphql/schema.graphql index 676fcfe5..6ccfe7a7 100644 --- a/rare/components/tabs/store/api/graphql/schema.graphql +++ b/rare/components/tabs/store/api/graphql/schema.graphql @@ -25,14 +25,14 @@ type DiscountSetting { discountType: String } -type AppliedRuled { +type AppliedRules { id: ID endDate: Date discountSetting: DiscountSetting } type LineOfferRes { - appliedRules: [AppliedRuled] + appliedRules: [AppliedRules] } type GetPriceRes { diff --git a/rare/components/tabs/store/api/models/diesel.py b/rare/components/tabs/store/api/models/diesel.py index b9629343..437c8b8c 100644 --- a/rare/components/tabs/store/api/models/diesel.py +++ b/rare/components/tabs/store/api/models/diesel.py @@ -33,7 +33,7 @@ class DieselSystemDetailItem: class DieselSystemDetail: p_type: Optional[str] = None details: Optional[List[DieselSystemDetailItem]] = None - system_type: Optional[str] = None + systemType: Optional[str] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod @@ -47,7 +47,7 @@ class DieselSystemDetail: tmp = cls( p_type=d.pop("_type", ""), details=details, - system_type=d.pop("systemType", ""), + systemType=d.pop("systemType", ""), ) tmp.unmapped = d return tmp @@ -107,7 +107,7 @@ class DieselProductDetail: p_type: Optional[str] = None about: Optional[DieselProductAbout] = None requirements: Optional[DieselSystemDetails] = None - social_links: Optional[DieselSocialLinks] = None + socialLinks: Optional[DieselSocialLinks] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod @@ -119,7 +119,7 @@ class DieselProductDetail: p_type=d.pop("_type", ""), about=about, requirements=requirements, - social_links=d.pop("socialLinks", {}), + socialLinks=d.pop("socialLinks", {}), ) tmp.unmapped = d return tmp @@ -136,7 +136,7 @@ class DieselProduct: namespace: Optional[str] = None pages: Optional[List["DieselProduct"]] = None data: Optional[DieselProductDetail] = None - product_name: Optional[str] = None + productName: Optional[str] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod @@ -158,7 +158,7 @@ class DieselProduct: namespace=d.pop("namespace", ""), pages=pages, data=data, - product_name=d.pop("productName", ""), + productName=d.pop("productName", ""), ) tmp.unmapped = d return tmp diff --git a/rare/components/tabs/store/api/models/response.py b/rare/components/tabs/store/api/models/response.py index 6a7672f2..23332d51 100644 --- a/rare/components/tabs/store/api/models/response.py +++ b/rare/components/tabs/store/api/models/response.py @@ -1,7 +1,7 @@ import logging from dataclasses import dataclass, field from datetime import datetime -from typing import List, Dict, Any, Type, Optional +from typing import List, Dict, Any, Type, Optional, Tuple from .utils import parse_date @@ -17,7 +17,6 @@ ItemModel = Dict SellerModel = Dict PageSandboxModel = Dict TagModel = Dict -PromotionsModel = Dict @dataclass @@ -39,16 +38,15 @@ class ImageUrlModel: d = src.copy() type = d.pop("type", None) url = d.pop("url", None) - tmp = cls( - type=type, - url=url, - ) + tmp = cls(type=type, url=url) return tmp @dataclass class KeyImagesModel: key_images: Optional[List[ImageUrlModel]] = None + tall_types = ("DieselStoreFrontTall", "OfferImageTall", "Thumbnail", "ProductLogo", "DieselGameBoxLogo") + wide_types = ("DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo") def __getitem__(self, item): return self.key_images[item] @@ -76,21 +74,13 @@ class KeyImagesModel: return tmp def available_tall(self) -> List[ImageUrlModel]: - tall_types = [ - "DieselStoreFrontTall", - "OfferImageTall", - "Thumbnail", - "ProductLogo", - "DieselGameBoxLogo", - ] - tall_images = filter(lambda img: img.type in tall_types, self.key_images) - tall_images = sorted(tall_images, key=lambda x: tall_types.index(x.type)) + tall_images = filter(lambda img: img.type in KeyImagesModel.tall_types, self.key_images) + tall_images = sorted(tall_images, key=lambda x: KeyImagesModel.tall_types.index(x.type)) return tall_images def available_wide(self) -> List[ImageUrlModel]: - wide_types = ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"] - wide_images = filter(lambda img: img.type in wide_types, self.key_images) - wide_images = sorted(wide_images, key=lambda x: wide_types.index(x.type)) + wide_images = filter(lambda img: img.type in KeyImagesModel.wide_types, self.key_images) + wide_images = sorted(wide_images, key=lambda x: KeyImagesModel.wide_types.index(x.type)) return wide_images def for_dimensions(self, w: int, h: int) -> ImageUrlModel: @@ -107,55 +97,133 @@ class KeyImagesModel: return model -TotalPriceModel = Dict -FmtPriceModel = Dict +CurrencyModel = Dict +FormattedPriceModel = Dict LineOffersModel = Dict @dataclass -class GetPriceResModel: - total_price: Optional[TotalPriceModel] = None - fmt_price: Optional[FmtPriceModel] = None - line_offers: Optional[LineOffersModel] = None +class TotalPriceModel: + discountPrice: Optional[int] = None + originalPrice: Optional[int] = None + voucherDiscount: Optional[int] = None + discount: Optional[int] = None + currencyCode: Optional[str] = None + currencyInfo: Optional[CurrencyModel] = None + fmtPrice: Optional[FormattedPriceModel] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["GetPriceResModel"], src: Dict[str, Any]) -> "GetPriceResModel": + def from_dict(cls: Type["TotalPriceModel"], src: Dict[str, Any]) -> "TotalPriceModel": d = src.copy() tmp = cls( - total_price=d.pop("totalPrice", {}), - fmt_price=d.pop("fmtPrice", {}), - line_offers=d.pop("lineOffers", {}), + discountPrice=d.pop("discountPrice", None), + originalPrice=d.pop("originalPrice", None), + voucherDiscount=d.pop("voucherDiscount", None), + discount=d.pop("discount", None), + currencyCode=d.pop("currencyCode", None), + currencyInfo=d.pop("currrencyInfo", {}), + fmtPrice=d.pop("fmtPrice", {}), ) tmp.unmapped = d return tmp +@dataclass +class GetPriceResModel: + totalPrice: Optional[TotalPriceModel] = None + lineOffers: Optional[LineOffersModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["GetPriceResModel"], src: Dict[str, Any]) -> "GetPriceResModel": + d = src.copy() + total_price = TotalPriceModel.from_dict(x) if (x := d.pop("totalPrice", {})) else None + tmp = cls(totalPrice=total_price, lineOffers=d.pop("lineOffers", {})) + tmp.unmapped = d + return tmp + + +DiscountSettingModel = Dict + + +@dataclass +class PromotionalOfferModel: + startDate: Optional[datetime] = None + endDate: Optional[datetime] = None + discountSetting: Optional[DiscountSettingModel] = None + + @classmethod + def from_dict(cls: Type["PromotionalOfferModel"], src: Dict[str, Any]) -> "PromotionalOfferModel": + d = src.copy() + start_date = parse_date(x) if (x := d.pop("startDate", "")) else None + end_date = parse_date(x) if (x := d.pop("endDate", "")) else None + tmp = cls(startDate=start_date, endDate=end_date, discountSetting=d.pop("discountSetting", {})) + tmp.unmapped = d + return tmp + + +@dataclass +class PromotionalOffersModel: + promotionalOffers: Optional[Tuple[PromotionalOfferModel]] = None + + @classmethod + def from_list(cls: Type["PromotionalOffersModel"], src: Dict[str, List]) -> "PromotionalOffersModel": + d = src.copy() + promotional_offers = ( + tuple([PromotionalOfferModel.from_dict(y) for y in x]) if (x := d.pop("promotionalOffers", [])) else None + ) + tmp = cls(promotionalOffers=promotional_offers) + tmp.unmapped = d + return tmp + + +@dataclass +class PromotionsModel: + promotionalOffers: Optional[Tuple[PromotionalOffersModel]] = None + upcomingPromotionalOffers: Optional[Tuple[PromotionalOffersModel]] = None + + @classmethod + def from_dict(cls: Type["PromotionsModel"], src: Dict[str, Any]) -> "PromotionsModel": + d = src.copy() + promotional_offers = ( + tuple([PromotionalOffersModel.from_list(y) for y in x]) if (x := d.pop("promotionalOffers", [])) else None + ) + upcoming_promotional_offers = ( + tuple([PromotionalOffersModel.from_list(y) for y in x]) + if (x := d.pop("upcomingPromotionalOffers", [])) + else None + ) + tmp = cls(promotionalOffers=promotional_offers, upcomingPromotionalOffers=upcoming_promotional_offers) + tmp.unmapped = d + return tmp + + @dataclass class CatalogOfferModel: - catalog_ns: Optional[CatalogNamespaceModel] = None + catalogNs: Optional[CatalogNamespaceModel] = None categories: Optional[List[CategoryModel]] = None - custom_attributes: Optional[List[CustomAttributeModel]] = None + customAttributes: Optional[List[CustomAttributeModel]] = None description: Optional[str] = None - effective_date: Optional[datetime] = None - expiry_date: Optional[datetime] = None + effectiveDate: Optional[datetime] = None + expiryDate: Optional[datetime] = None id: Optional[str] = None - is_code_redemption_only: Optional[bool] = None + isCodeRedemptionOnly: Optional[bool] = None items: Optional[List[ItemModel]] = None - key_images: Optional[KeyImagesModel] = None + keyImages: Optional[KeyImagesModel] = None namespace: Optional[str] = None - offer_mappings: Optional[List[PageSandboxModel]] = None - offer_type: Optional[str] = None + offerMappings: Optional[List[PageSandboxModel]] = None + offerType: Optional[str] = None price: Optional[GetPriceResModel] = None - product_slug: Optional[str] = None + productSlug: Optional[str] = None promotions: Optional[PromotionsModel] = None seller: Optional[SellerModel] = None status: Optional[str] = None tags: Optional[List[TagModel]] = None title: Optional[str] = None url: Optional[str] = None - url_slug: Optional[str] = None - viewable_date: Optional[datetime] = None + urlSlug: Optional[str] = None + viewableDate: Optional[datetime] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod @@ -165,31 +233,32 @@ class CatalogOfferModel: expiry_date = parse_date(x) if (x := d.pop("expiryDate", "")) else None key_images = KeyImagesModel.from_list(d.pop("keyImages", [])) price = GetPriceResModel.from_dict(x) if (x := d.pop("price", {})) else None + promotions = PromotionsModel.from_dict(x) if (x := d.pop("promotions", {})) else None viewable_date = parse_date(x) if (x := d.pop("viewableDate", "")) else None tmp = cls( - catalog_ns=d.pop("catalogNs", {}), + catalogNs=d.pop("catalogNs", {}), categories=d.pop("categories", []), - custom_attributes=d.pop("customAttributes", []), + customAttributes=d.pop("customAttributes", []), description=d.pop("description", ""), - effective_date=effective_date, - expiry_date=expiry_date, + effectiveDate=effective_date, + expiryDate=expiry_date, id=d.pop("id", ""), - is_code_redemption_only=d.pop("isCodeRedemptionOnly", None), + isCodeRedemptionOnly=d.pop("isCodeRedemptionOnly", None), items=d.pop("items", []), - key_images=key_images, + keyImages=key_images, namespace=d.pop("namespace", ""), - offer_mappings=d.pop("offerMappings", []), - offer_type=d.pop("offerType", ""), + offerMappings=d.pop("offerMappings", []), + offerType=d.pop("offerType", ""), price=price, - product_slug=d.pop("productSlug", ""), - promotions=d.pop("promotions", {}), + productSlug=d.pop("productSlug", ""), + promotions=promotions, seller=d.pop("seller", {}), status=d.pop("status", ""), tags=d.pop("tags", []), title=d.pop("title", ""), url=d.pop("url", ""), - url_slug=d.pop("urlSlug", ""), - viewable_date=viewable_date, + urlSlug=d.pop("urlSlug", ""), + viewableDate=viewable_date, ) tmp.unmapped = d return tmp @@ -200,8 +269,8 @@ class WishlistItemModel: created: Optional[datetime] = None id: Optional[str] = None namespace: Optional[str] = None - is_first_time: Optional[bool] = None - offer_id: Optional[str] = None + isFirstTime: Optional[bool] = None + offerId: Optional[str] = None order: Optional[Any] = None updated: Optional[datetime] = None offer: Optional[CatalogOfferModel] = None @@ -217,8 +286,8 @@ class WishlistItemModel: created=created, id=d.pop("id", ""), namespace=d.pop("namespace", ""), - is_first_time=d.pop("isFirstTime", None), - offer_id=d.pop("offerId", ""), + isFirstTime=d.pop("isFirstTime", None), + offerId=d.pop("offerId", ""), order=d.pop("order", ""), updated=updated, offer=offer, @@ -238,10 +307,7 @@ class PagingModel: d = src.copy() count = d.pop("count", None) total = d.pop("total", None) - tmp = cls( - count=count, - total=total, - ) + tmp = cls(count=count, total=total) tmp.unmapped = d return tmp @@ -261,26 +327,21 @@ class SearchStoreModel: elem = CatalogOfferModel.from_dict(item) elements.append(elem) paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None - tmp = cls( - elements=elements, - paging=paging, - ) + tmp = cls(elements=elements, paging=paging) tmp.unmapped = d return tmp @dataclass class CatalogModel: - search_store: Optional[SearchStoreModel] = None + searchStore: Optional[SearchStoreModel] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod def from_dict(cls: Type["CatalogModel"], src: Dict[str, Any]) -> "CatalogModel": d = src.copy() search_store = SearchStoreModel.from_dict(x) if (x := d.pop("searchStore", {})) else None - tmp = cls( - search_store=search_store, - ) + tmp = cls(searchStore=search_store) tmp.unmapped = d return tmp @@ -300,10 +361,7 @@ class WishlistItemsModel: elem = WishlistItemModel.from_dict(item) elements.append(elem) paging = PagingModel.from_dict(x) if (x := d.pop("paging", {})) else None - tmp = cls( - elements=elements, - paging=paging, - ) + tmp = cls(elements=elements, paging=paging) tmp.unmapped = d return tmp @@ -316,16 +374,14 @@ class RemoveFromWishlistModel: @classmethod def from_dict(cls: Type["RemoveFromWishlistModel"], src: Dict[str, Any]) -> "RemoveFromWishlistModel": d = src.copy() - tmp = cls( - success=d.pop("success", None), - ) + tmp = cls(success=d.pop("success", None)) tmp.unmapped = d return tmp @dataclass class AddToWishlistModel: - wishlist_item: Optional[WishlistItemModel] = None + wishlistItem: Optional[WishlistItemModel] = None success: Optional[bool] = None unmapped: Dict[str, Any] = field(default_factory=dict) @@ -333,33 +389,26 @@ class AddToWishlistModel: def from_dict(cls: Type["AddToWishlistModel"], src: Dict[str, Any]) -> "AddToWishlistModel": d = src.copy() wishlist_item = WishlistItemModel.from_dict(x) if (x := d.pop("wishlistItem", {})) else None - tmp = cls( - wishlist_item=wishlist_item, - success=d.pop("success", None), - ) + tmp = cls(wishlistItem=wishlist_item, success=d.pop("success", None)) tmp.unmapped = d return tmp @dataclass class WishlistModel: - wishlist_items: Optional[WishlistItemsModel] = None - remove_from_wishlist: Optional[RemoveFromWishlistModel] = None - add_to_wishlist: Optional[AddToWishlistModel] = None + wishlistItems: Optional[WishlistItemsModel] = None + removeFromWishlist: Optional[RemoveFromWishlistModel] = None + addToWishlist: Optional[AddToWishlistModel] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod def from_dict(cls: Type["WishlistModel"], src: Dict[str, Any]) -> "WishlistModel": d = src.copy() wishlist_items = WishlistItemsModel.from_dict(x) if (x := d.pop("wishlistItems", {})) else None - remove_from_wishlist = ( - RemoveFromWishlistModel.from_dict(x) if (x := d.pop("removeFromWishlist", {})) else None - ) + remove_from_wishlist = RemoveFromWishlistModel.from_dict(x) if (x := d.pop("removeFromWishlist", {})) else None add_to_wishlist = AddToWishlistModel.from_dict(x) if (x := d.pop("addToWishlist", {})) else None tmp = cls( - wishlist_items=wishlist_items, - remove_from_wishlist=remove_from_wishlist, - add_to_wishlist=add_to_wishlist, + wishlistItems=wishlist_items, removeFromWishlist=remove_from_wishlist, addToWishlist=add_to_wishlist ) tmp.unmapped = d return tmp diff --git a/rare/components/tabs/store/api/models/utils.py b/rare/components/tabs/store/api/models/utils.py index 92c6ebf2..06f79c67 100644 --- a/rare/components/tabs/store/api/models/utils.py +++ b/rare/components/tabs/store/api/models/utils.py @@ -2,4 +2,4 @@ from datetime import datetime, timezone def parse_date(date: str): - return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc) \ No newline at end of file + return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc) diff --git a/rare/components/tabs/store/landing.py b/rare/components/tabs/store/landing.py index a55e5124..a8582697 100644 --- a/rare/components/tabs/store/landing.py +++ b/rare/components/tabs/store/landing.py @@ -14,26 +14,25 @@ from PyQt5.QtWidgets import ( QFrame, ) +from rare.components.tabs.store.api.models.response import CatalogOfferModel, WishlistItemModel from rare.widgets.flow_layout import FlowLayout from rare.widgets.side_tab import SideTabContents from rare.widgets.sliding_stack import SlidingStackedWidget -from rare.components.tabs.store.api.models.response import CatalogOfferModel, WishlistItemModel -from .api.models.utils import parse_date from .store_api import StoreAPI from .widgets.details import DetailsWidget -from .widgets.items import StoreItemWidget from .widgets.groups import StoreGroup +from .widgets.items import StoreItemWidget logger = logging.getLogger("StoreLanding") class LandingPage(SlidingStackedWidget, SideTabContents): - def __init__(self, api: StoreAPI, parent=None): + def __init__(self, store_api: StoreAPI, parent=None): super(LandingPage, self).__init__(parent=parent) self.implements_scrollarea = True - self.landing_widget = LandingWidget(api, parent=self) + self.landing_widget = LandingWidget(store_api, parent=self) self.landing_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.landing_widget.set_title.connect(self.set_title) self.landing_widget.show_details.connect(self.show_details) @@ -43,7 +42,7 @@ class LandingPage(SlidingStackedWidget, SideTabContents): self.landing_scroll.setFrameStyle(QFrame.NoFrame | QFrame.Plain) self.landing_scroll.setWidget(self.landing_widget) - self.details_widget = DetailsWidget([], api, parent=self) + self.details_widget = DetailsWidget([], store_api, parent=self) self.details_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.details_widget.set_title.connect(self.set_title) self.details_widget.back_clicked.connect(self.show_main) @@ -83,7 +82,7 @@ class LandingWidget(QWidget, SideTabContents): self.discounts_group = StoreGroup(self.tr("Wishlist discounts"), layout=FlowLayout, parent=self) self.discounts_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.games_group = StoreGroup(self.tr("Games"), FlowLayout, self) + self.games_group = StoreGroup(self.tr("Free to play"), FlowLayout, self) self.games_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.games_group.loading(False) self.games_group.setVisible(True) @@ -97,8 +96,8 @@ class LandingWidget(QWidget, SideTabContents): def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) - self.api.get_free_games(self.__add_free) - self.api.get_wishlist(self.__add_discounts) + self.api.get_free(self.__update_free_games) + self.api.get_wishlist(self.__update_wishlist_discounts) return super().showEvent(a0) def hideEvent(self, a0: QHideEvent) -> None: @@ -107,28 +106,20 @@ class LandingWidget(QWidget, SideTabContents): # TODO: Implement tab unloading return super().hideEvent(a0) - def __add_discounts(self, wishlist: List[WishlistItemModel]): + def __update_wishlist_discounts(self, wishlist: List[WishlistItemModel]): for w in self.discounts_group.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly): self.discounts_group.layout().removeWidget(w) w.deleteLater() - discounts = 0 - for game in wishlist: - if not game: - continue - try: - if game.offer.price.total_price["discount"] > 0: - w = StoreItemWidget(self.api.cached_manager, game.offer) - w.show_details.connect(self.show_details) - self.discounts_group.layout().addWidget(w) - discounts += 1 - except Exception as e: - logger.warning(f"{game} {e}") - continue - # self.discounts_group.setVisible(discounts > 0) + for item in wishlist: + if item.offer.price.totalPrice.discount > 0: + w = StoreItemWidget(self.api.cached_manager, item.offer) + w.show_details.connect(self.show_details) + self.discounts_group.layout().addWidget(w) + self.discounts_group.setVisible(bool(wishlist)) self.discounts_group.loading(False) - def __add_free(self, free_games: List[CatalogOfferModel]): + def __update_free_games(self, free_games: List[CatalogOfferModel]): for w in self.free_games_now.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly): self.free_games_now.layout().removeWidget(w) w.deleteLater() @@ -140,56 +131,38 @@ class LandingWidget(QWidget, SideTabContents): date = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) free_now = [] free_next = [] - for game in free_games: + for item in free_games: try: - if ( - game.price.total_price["fmtPrice"]["discountPrice"] == "0" - and game.price.total_price["fmtPrice"]["originalPrice"] - != game.price.total_price["fmtPrice"]["discountPrice"] - ): - free_now.append(game) + if item.price.totalPrice.discountPrice == 0: + free_now.append(item) continue - - if game.title == "Mystery Game": - free_next.append(game) + if item.title == "Mystery Game": + free_next.append(item) continue except KeyError as e: logger.warning(str(e)) - try: - # parse datetime to check if game is next week or now - try: - start_date = parse_date( - game.promotions["upcomingPromotionalOffers"][0]["promotionalOffers"][0]["startDate"] - ) - except Exception: - try: - start_date = parse_date( - game.promotions["promotionalOffers"][0]["promotionalOffers"][0]["startDate"] - ) - except Exception as e: - continue - - except TypeError: - print("type error") - continue + if not item.promotions.promotionalOffers: + start_date = item.promotions.upcomingPromotionalOffers[0].promotionalOffers[0].startDate + else: + start_date = item.promotions.promotionalOffers[0].promotionalOffers[0].startDate if start_date > date: - free_next.append(game) + free_next.append(item) # free games now - self.free_games_now.setVisible(bool(len(free_now))) - for game in free_now: - w = StoreItemWidget(self.api.cached_manager, game) + self.free_games_now.setVisible(bool(free_now)) + for item in free_now: + w = StoreItemWidget(self.api.cached_manager, item) w.show_details.connect(self.show_details) self.free_games_now.layout().addWidget(w) self.free_games_now.loading(False) # free games next week - self.free_games_next.setVisible(bool(len(free_next))) - for game in free_next: - w = StoreItemWidget(self.api.cached_manager, game) - if game.title != "Mystery Game": + self.free_games_next.setVisible(bool(free_next)) + for item in free_next: + w = StoreItemWidget(self.api.cached_manager, item) + if item.title != "Mystery Game": w.show_details.connect(self.show_details) self.free_games_next.layout().addWidget(w) self.free_games_next.loading(False) diff --git a/rare/components/tabs/store/search.py b/rare/components/tabs/store/search.py index 7e5d6d8d..56456b30 100644 --- a/rare/components/tabs/store/search.py +++ b/rare/components/tabs/store/search.py @@ -26,16 +26,16 @@ logger = logging.getLogger("Shop") class SearchPage(SlidingStackedWidget, SideTabContents): - def __init__(self, core, api: StoreAPI, parent=None): + def __init__(self, store_api: StoreAPI, parent=None): super(SearchPage, self).__init__(parent=parent) self.implements_scrollarea = True - self.search_widget = SearchWidget(core, api, parent=self) + self.search_widget = SearchWidget(store_api, parent=self) self.search_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.search_widget.set_title.connect(self.set_title) self.search_widget.show_details.connect(self.show_details) - self.details_widget = DetailsWidget([], api, parent=self) + self.details_widget = DetailsWidget([], store_api, parent=self) self.details_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.details_widget.set_title.connect(self.set_title) self.details_widget.back_clicked.connect(self.show_main) @@ -58,27 +58,25 @@ class SearchPage(SlidingStackedWidget, SideTabContents): class SearchWidget(QWidget, SideTabContents): show_details = pyqtSignal(CatalogOfferModel) - def __init__(self, core: LegendaryCore, api: StoreAPI, parent=None): + def __init__(self, store_api: StoreAPI, parent=None): super(SearchWidget, self).__init__(parent=parent) self.implements_scrollarea = True self.ui = Ui_SearchWidget() self.ui.setupUi(self) self.ui.main_layout.setContentsMargins(0, 0, 3, 0) - self.core = core - self.api_core = api + self.store_api = store_api self.price = "" self.tags = [] self.types = [] self.update_games_allowed = True - self.free_game_widgets = [] self.active_search_request = False self.next_search = "" self.wishlist: List = [] self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search Games")) - self.results_scrollarea = ResultsWidget(self.api_core, self) + self.results_scrollarea = ResultsWidget(self.store_api, self) self.results_scrollarea.show_details.connect(self.show_details) self.ui.left_layout.addWidget(self.search_bar) @@ -188,8 +186,8 @@ class SearchWidget(QWidget, SideTabContents): self.games_group.loading(True) browse_model = SearchStoreQuery( - language=self.core.language_code, - country=self.core.country_code, + language=self.store_api.language_code, + country=self.store_api.country_code, count=20, price_range=self.price, on_sale=self.ui.on_discount.isChecked(), @@ -198,7 +196,7 @@ class SearchWidget(QWidget, SideTabContents): if self.types: browse_model.category = "|".join(self.types) - self.api_core.browse_games(browse_model, self.show_games) + self.store_api.browse_games(browse_model, self.show_games) class CheckBox(QCheckBox): diff --git a/rare/components/tabs/store/store_api.py b/rare/components/tabs/store/store_api.py index 90ac2b7e..0f45d8e8 100644 --- a/rare/components/tabs/store/store_api.py +++ b/rare/components/tabs/store/store_api.py @@ -20,7 +20,7 @@ from .api.models.response import ( CatalogOfferModel, ) -logger = getLogger("ShopAPICore") +logger = getLogger("StoreAPI") graphql_url = "https://graphql.epicgames.com/graphql" @@ -46,7 +46,7 @@ class StoreAPI(QObject): self.browse_active = False self.next_browse_request = tuple(()) - def get_free_games(self, handle_func: callable): + def get_free(self, handle_func: callable): url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions" params = { "locale": self.locale, @@ -59,7 +59,7 @@ class StoreAPI(QObject): def __handle_free_games(data, handle_func): try: response = ResponseModel.from_dict(data) - results: List[CatalogOfferModel] = response.data.catalog.search_store.elements + results: List[CatalogOfferModel] = response.data.catalog.searchStore.elements handle_func(results) except KeyError as e: if DEBUG(): @@ -94,7 +94,7 @@ class StoreAPI(QObject): response = ResponseModel.from_dict(data) if response.errors: logger.error(response.errors) - handle_func(response.data.wishlist.wishlist_items.elements) + handle_func(response.data.wishlist.wishlistItems.elements) except KeyError as e: if DEBUG(): raise e @@ -132,7 +132,7 @@ class StoreAPI(QObject): def __handle_search(data, handler): try: response = ResponseModel.from_dict(data) - handler(response.data.catalog.search_store.elements) + handler(response.data.catalog.searchStore.elements) except KeyError as e: logger.error(str(e)) if DEBUG(): @@ -167,7 +167,7 @@ class StoreAPI(QObject): if not self.next_browse_request: try: response = ResponseModel.from_dict(data) - handle_func(response.data.catalog.search_store.elements) + handle_func(response.data.catalog.searchStore.elements) except KeyError as e: if DEBUG(): raise e @@ -233,7 +233,7 @@ class StoreAPI(QObject): # debug.exec() try: response = ResponseModel.from_dict(data) - data = response.data.wishlist.add_to_wishlist + data = response.data.wishlist.addToWishlist handle_func(data.success) except Exception as e: if DEBUG(): @@ -259,7 +259,7 @@ class StoreAPI(QObject): # debug.exec() try: response = ResponseModel.from_dict(data) - data = response.data.wishlist.remove_from_wishlist + data = response.data.wishlist.removeFromWishlist handle_func(data.success) except Exception as e: if DEBUG(): diff --git a/rare/components/tabs/store/widgets/details.py b/rare/components/tabs/store/widgets/details.py index 98f8eaf0..e0386bc7 100644 --- a/rare/components/tabs/store/widgets/details.py +++ b/rare/components/tabs/store/widgets/details.py @@ -1,5 +1,5 @@ import logging -from typing import List +from typing import List, Dict from PyQt5.QtCore import Qt, QUrl, pyqtSignal from PyQt5.QtGui import QFont, QDesktopServices, QKeyEvent @@ -14,6 +14,7 @@ from PyQt5.QtWidgets import ( from rare.components.tabs.store.api.debug import DebugDialog from rare.components.tabs.store.api.models.diesel import DieselProduct, DieselProductDetail, DieselSystemDetail from rare.components.tabs.store.api.models.response import CatalogOfferModel +from rare.components.tabs.store.store_api import StoreAPI from rare.models.image import ImageSize from rare.ui.components.tabs.store.details import Ui_DetailsWidget from rare.utils.misc import icon @@ -28,7 +29,7 @@ class DetailsWidget(QWidget, SideTabContents): back_clicked: pyqtSignal = pyqtSignal() # TODO Design - def __init__(self, installed_titles: list, api_core, parent=None): + def __init__(self, installed: List, store_api: StoreAPI, parent=None): super(DetailsWidget, self).__init__(parent=parent) self.implements_scrollarea = True @@ -36,13 +37,11 @@ class DetailsWidget(QWidget, SideTabContents): self.ui.setupUi(self) self.ui.main_layout.setContentsMargins(0, 0, 3, 0) - # self.core = LegendaryCoreSingleton() - self.api_core = api_core - self.installed = installed_titles - self.offer: CatalogOfferModel = None - self.data: dict = {} + self.store_api = store_api + self.installed = installed + self.catalog_offer: CatalogOfferModel = None - self.image = LoadingImageWidget(api_core.cached_manager, self) + self.image = LoadingImageWidget(store_api.cached_manager, self) self.image.setFixedSize(ImageSize.Display) self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop) self.ui.left_layout.setAlignment(Qt.AlignTop) @@ -53,7 +52,7 @@ class DetailsWidget(QWidget, SideTabContents): self.in_wishlist = False self.wishlist = [] - self.requirements_tabs: SideTabWidget = SideTabWidget(parent=self.ui.requirements_frame) + self.requirements_tabs = SideTabWidget(parent=self.ui.requirements_frame) self.requirements_tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.ui.requirements_layout.addWidget(self.requirements_tabs) @@ -78,15 +77,17 @@ class DetailsWidget(QWidget, SideTabContents): self.ui.title.setText(offer.title) self.title_str = offer.title self.id_str = offer.id - self.api_core.get_wishlist(self.handle_wishlist_update) + self.store_api.get_wishlist(self.handle_wishlist_update) + # lk: delete tabs in reverse order because indices are updated on deletion while self.requirements_tabs.count(): self.requirements_tabs.widget(0).deleteLater() self.requirements_tabs.removeTab(0) self.requirements_tabs.clear() - slug = offer.product_slug + + slug = offer.productSlug if not slug: - for mapping in offer.offer_mappings: + for mapping in offer.offerMappings: if mapping["pageType"] == "productHome": slug = mapping["pageSlug"] break @@ -114,24 +115,24 @@ class DetailsWidget(QWidget, SideTabContents): # init API request if slug: - self.api_core.get_game_config_cms(offer.product_slug, is_bundle, self.data_received) + self.store_api.get_game_config_cms(offer.productSlug, is_bundle, self.data_received) # else: # self.data_received({}) - self.offer = offer + self.catalog_offer = offer def add_to_wishlist(self): if not self.in_wishlist: - self.api_core.add_to_wishlist( - self.offer.namespace, - self.offer.id, + self.store_api.add_to_wishlist( + self.catalog_offer.namespace, + self.catalog_offer.id, lambda success: self.ui.wishlist_button.setText(self.tr("Remove from wishlist")) if success else self.ui.wishlist_button.setText("Something went wrong") ) else: - self.api_core.remove_from_wishlist( - self.offer.namespace, - self.offer.id, + self.store_api.remove_from_wishlist( + self.catalog_offer.namespace, + self.catalog_offer.id, lambda success: self.ui.wishlist_button.setText(self.tr("Add to wishlist")) if success else self.ui.wishlist_button.setText("Something went wrong"), @@ -146,34 +147,16 @@ class DetailsWidget(QWidget, SideTabContents): except Exception as e: raise e logger.error(str(e)) - self.price.setText("Error") - self.requirements_tabs.setEnabled(False) - for img in self.data.get("keyImages"): - if img["type"] in [ - "DieselStoreFrontWide", - "OfferImageTall", - "VaultClosed", - "ProductLogo", - ]: - self.image.fetchPixmap(img["url"]) - break - self.price.setText("") - self.discount_price.setText("") - self.social_group.setEnabled(False) - self.tags.setText("") - self.dev.setText(self.data.get("seller", {}).get("name", "")) - return - # self.title.setText(self.game.title) - self.ui.price.setFont(QFont()) - price = self.offer.price.total_price["fmtPrice"]["originalPrice"] - discount_price = self.offer.price.total_price["fmtPrice"]["discountPrice"] + self.ui.price.setFont(self.font()) + price = self.catalog_offer.price.totalPrice.fmtPrice["originalPrice"] + discount_price = self.catalog_offer.price.totalPrice.fmtPrice["discountPrice"] if price == "0" or price == 0: self.ui.price.setText(self.tr("Free")) else: self.ui.price.setText(price) if price != discount_price: - font = QFont() + font = self.font() font.setStrikeOut(True) self.ui.price.setFont(font) self.ui.discount_price.setText( @@ -189,18 +172,11 @@ class DetailsWidget(QWidget, SideTabContents): if requirements and requirements.systems: for system in requirements.systems: req_widget = RequirementsWidget(system, self.requirements_tabs) - self.requirements_tabs.addTab(req_widget, system.system_type) - # self.req_group_box.layout().addWidget(req_tabs) - # self.req_group_box.layout().setAlignment(Qt.AlignTop) - # else: - # self.req_group_box.layout().addWidget( - # QLabel(self.tr("Could not get requirements")) - # ) - self.ui.requirements_frame.setVisible(True) + self.requirements_tabs.addTab(req_widget, system.systemType) else: self.ui.requirements_frame.setVisible(False) - key_images = self.offer.key_images + key_images = self.catalog_offer.keyImages img_url = key_images.for_dimensions(self.image.size().width(), self.image.size().height()) self.image.fetchPixmap(img_url.url) @@ -223,7 +199,7 @@ class DetailsWidget(QWidget, SideTabContents): self.ui.social_layout.removeWidget(b) b.deleteLater() - links = product_data.social_links + links = product_data.socialLinks link_count = 0 for name, url in links.items(): if name == "_type": @@ -252,8 +228,7 @@ class DetailsWidget(QWidget, SideTabContents): # self.wishlist.append(game["offer"]["title"]) def button_clicked(self): - return - QDesktopServices.openUrl(QUrl(f"https://www.epicgames.com/store/{self.core.language_code}/p/{self.slug}")) + QDesktopServices.openUrl(QUrl(f"https://www.epicgames.com/store/{self.store_api.language_code}/p/{self.slug}")) def keyPressEvent(self, a0: QKeyEvent): if a0.key() == Qt.Key_Escape: diff --git a/rare/components/tabs/store/widgets/items.py b/rare/components/tabs/store/widgets/items.py index 02b6b500..3fc0363c 100644 --- a/rare/components/tabs/store/widgets/items.py +++ b/rare/components/tabs/store/widgets/items.py @@ -11,7 +11,7 @@ from rare.utils.misc import qta_icon from rare.utils.qt_requests import QtRequests from .image import LoadingImageWidget -logger = logging.getLogger("GameWidgets") +logger = logging.getLogger("StoreWidgets") class ItemWidget(LoadingImageWidget): @@ -46,15 +46,15 @@ class StoreItemWidget(ItemWidget): return self.ui.title_label.setText(game.title) - for attr in game.custom_attributes: + for attr in game.customAttributes: if attr["key"] == "developerName": developer = attr["value"] break else: developer = game.seller["name"] self.ui.developer_label.setText(developer) - price = game.price.total_price["fmtPrice"]["originalPrice"] - discount_price = game.price.total_price["fmtPrice"]["discountPrice"] + price = game.price.totalPrice.fmtPrice["originalPrice"] + discount_price = game.price.totalPrice.fmtPrice["discountPrice"] self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') if price != discount_price: font = self.ui.price_label.font() @@ -64,7 +64,7 @@ class StoreItemWidget(ItemWidget): else: self.ui.discount_label.setVisible(False) - key_images = game.key_images + key_images = game.keyImages self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url) # for img in json_info["keyImages"]: @@ -83,13 +83,13 @@ class ResultsItemWidget(ItemWidget): self.setFixedSize(ImageSize.Display) self.ui.setupUi(self) - key_images = catalog_game.key_images + key_images = catalog_game.keyImages self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url) self.ui.title_label.setText(catalog_game.title) - price = catalog_game.price.total_price["fmtPrice"]["originalPrice"] - discount_price = catalog_game.price.total_price["fmtPrice"]["discountPrice"] + price = catalog_game.price.totalPrice.fmtPrice["originalPrice"] + discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"] self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') if price != discount_price: font = self.ui.price_label.font() @@ -108,14 +108,14 @@ class WishlistItemWidget(ItemWidget): super(WishlistItemWidget, self).__init__(manager, catalog_game, parent=parent) self.setFixedSize(ImageSize.DisplayWide) self.ui.setupUi(self) - for attr in catalog_game.custom_attributes: + for attr in catalog_game.customAttributes: if attr["key"] == "developerName": developer = attr["value"] break else: developer = catalog_game.seller["name"] - original_price = catalog_game.price.total_price["fmtPrice"]["originalPrice"] - discount_price = catalog_game.price.total_price["fmtPrice"]["discountPrice"] + original_price = catalog_game.price.totalPrice.fmtPrice["originalPrice"] + discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"] self.ui.title_label.setText(catalog_game.title) self.ui.developer_label.setText(developer) @@ -127,7 +127,7 @@ class WishlistItemWidget(ItemWidget): self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') else: self.ui.discount_label.setVisible(False) - key_images = catalog_game.key_images + key_images = catalog_game.keyImages self.fetchPixmap( key_images.for_dimensions(self.width(), self.height()).url ) diff --git a/rare/components/tabs/store/wishlist.py b/rare/components/tabs/store/wishlist.py index bedc2ca4..2eb173a9 100644 --- a/rare/components/tabs/store/wishlist.py +++ b/rare/components/tabs/store/wishlist.py @@ -116,13 +116,13 @@ class WishlistWidget(QWidget, SideTabContents): func = lambda x: x.catalog_game.title reverse = self.ui.reverse.isChecked() elif sort == 1: - func = lambda x: x.catalog_game.price.total_price["fmtPrice"]["discountPrice"] + func = lambda x: x.catalog_game.price.totalPrice["fmtPrice"]["discountPrice"] reverse = self.ui.reverse.isChecked() elif sort == 2: func = lambda x: x.catalog_game.seller["name"] reverse = self.ui.reverse.isChecked() elif sort == 3: - func = lambda x: 1 - (x.catalog_game.price.total_price["discountPrice"] / x.catalog_game.price.total_price["originalPrice"]) + func = lambda x: 1 - (x.catalog_game.price.totalPrice["discountPrice"] / x.catalog_game.price.totalPrice["originalPrice"]) reverse = not self.ui.reverse.isChecked() else: func = lambda x: x.catalog_game.title diff --git a/rare/ui/components/tabs/store/details.py b/rare/ui/components/tabs/store/details.py index 5c570cb2..ccb87c86 100644 --- a/rare/ui/components/tabs/store/details.py +++ b/rare/ui/components/tabs/store/details.py @@ -18,6 +18,8 @@ class Ui_DetailsWidget(object): DetailsWidget.setWindowTitle("DetailsWidget") self.main_layout = QtWidgets.QHBoxLayout(DetailsWidget) self.main_layout.setObjectName("main_layout") + self.left_layout = QtWidgets.QVBoxLayout() + self.left_layout.setObjectName("left_layout") self.back_button = QtWidgets.QPushButton(DetailsWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -28,9 +30,7 @@ class Ui_DetailsWidget(object): self.back_button.setIconSize(QtCore.QSize(32, 32)) self.back_button.setFlat(True) self.back_button.setObjectName("back_button") - self.main_layout.addWidget(self.back_button) - self.left_layout = QtWidgets.QVBoxLayout() - self.left_layout.setObjectName("left_layout") + self.left_layout.addWidget(self.back_button) self.main_layout.addLayout(self.left_layout) self.right_layout = QtWidgets.QVBoxLayout() self.right_layout.setObjectName("right_layout") @@ -176,7 +176,7 @@ class Ui_DetailsWidget(object): self.description_label.setObjectName("description_label") self.right_layout.addWidget(self.description_label) self.main_layout.addLayout(self.right_layout) - self.main_layout.setStretch(2, 1) + self.main_layout.setStretch(1, 1) self.retranslateUi(DetailsWidget) diff --git a/rare/ui/components/tabs/store/details.ui b/rare/ui/components/tabs/store/details.ui index 20b21038..b78efa4c 100644 --- a/rare/ui/components/tabs/store/details.ui +++ b/rare/ui/components/tabs/store/details.ui @@ -13,31 +13,32 @@ DetailsWidget - + - - - - 0 - 0 - - - - - - - - 32 - 32 - - - - true - - - - - + + + + + + 0 + 0 + + + + + + + + 32 + 32 + + + + true + + + + diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py index 2a7d8bbd..02a9c5a2 100644 --- a/rare/utils/qt_requests.py +++ b/rare/utils/qt_requests.py @@ -5,7 +5,7 @@ from typing import Callable, Dict, TypeVar, List, Tuple from typing import Union import orjson -from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QUrlQuery, pyqtSlot +from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QUrlQuery, pyqtSlot, QJsonDocument from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QNetworkDiskCache USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36"