diff --git a/rare/components/tabs/store/__init__.py b/rare/components/tabs/store/__init__.py index 817e1acd..35b5b6a2 100644 --- a/rare/components/tabs/store/__init__.py +++ b/rare/components/tabs/store/__init__.py @@ -6,6 +6,7 @@ from rare.widgets.side_tab import SideTabWidget from .game_info import ShopGameInfo from .search_results import SearchResults from .shop_api_core import ShopApiCore +from .api.models.response import CatalogOfferModel from .shop_widget import ShopWidget from .wishlist import WishlistWidget, Wishlist @@ -69,7 +70,7 @@ class StoreTab(SideTabWidget): def update_wishlist(self): self.shop.update_wishlist() - def show_game(self, data): + def show_game(self, data: CatalogOfferModel): self.previous_index = self.currentIndex() self.info.update_game(data) self.setCurrentIndex(self.info_index) diff --git a/rare/components/tabs/store/__main__.py b/rare/components/tabs/store/__main__.py index 29d9acfd..a7366a13 100644 --- a/rare/components/tabs/store/__main__.py +++ b/rare/components/tabs/store/__main__.py @@ -35,4 +35,4 @@ if __name__ == "__main__": window.setWindowTitle(f"{app.applicationName()} - Store") window.resize(QSize(1280, 800)) window.show() - app.exec() \ No newline at end of file + app.exec() diff --git a/rare/components/tabs/store/api/__init__.py b/rare/components/tabs/store/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/components/tabs/store/api/constants/__init__.py b/rare/components/tabs/store/api/constants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/components/tabs/store/api/constants/queries.py b/rare/components/tabs/store/api/constants/queries.py new file mode 100644 index 00000000..5e5d9919 --- /dev/null +++ b/rare/components/tabs/store/api/constants/queries.py @@ -0,0 +1,568 @@ + +FEED_QUERY = ''' +query feedQuery( + $locale: String! + $countryCode: String + $offset: Int + $postsPerPage: Int + $category: String +) { + TransientStream { + myTransientFeed(countryCode: $countryCode, locale: $locale) { + id + activity { + ... on LinkAccountActivity { + type + created_at + platforms + } + ... on SuggestedFriendsActivity { + type + created_at + platform + suggestions { + epicId + epicDisplayName + platformFullName + platformAvatar + } + } + ... on IncomingInvitesActivity { + type + created_at + invites { + epicId + epicDisplayName + } + } + ... on RecentPlayersActivity { + type + created_at + players { + epicId + epicDisplayName + playedGameName + } + } + } + } + } + Blog { + dieselBlogPosts: getPosts( + locale: $locale + offset: $offset + postsPerPage: $postsPerPage + category: $category + ) { + blogList { + _id + author + category + content + urlPattern + slug + sticky + title + date + image + shareImage + trendingImage + url + featured + link + externalLink + } + } + } +} +''' + +REVIEWS_QUERY = ''' +query productReviewsQuery($sku: String!) { + OpenCritic { + productReviews(sku: $sku) { + id + name + openCriticScore + reviewCount + percentRecommended + openCriticUrl + award + topReviews { + publishedDate + externalUrl + snippet + language + score + author + ScoreFormat { + id + description + } + OutletId + outletName + displayScore + } + } + } +} +''' + +MEDIA_QUERY = ''' +query fetchMediaRef($mediaRefId: String!) { + Media { + getMediaRef(mediaRefId: $mediaRefId) { + accountId + outputs { + duration + url + width + height + key + contentType + } + namespace + } + } +} +''' + +ADDONS_QUERY = ''' +query getAddonsByNamespace( + $categories: String! + $count: Int! + $country: String! + $locale: String! + $namespace: String! + $sortBy: String! + $sortDir: String! +) { + Catalog { + catalogOffers( + namespace: $namespace + locale: $locale + params: { + category: $categories + count: $count + country: $country + sortBy: $sortBy + sortDir: $sortDir + } + ) { + elements { + countriesBlacklist + customAttributes { + key + value + } + description + developer + effectiveDate + id + isFeatured + keyImages { + type + url + } + lastModifiedDate + longDescription + namespace + offerType + productSlug + releaseDate + status + technicalDetails + title + urlSlug + } + } + } +} +''' + +CATALOG_QUERY = ''' +query catalogQuery( + $category: String + $count: Int + $country: String! + $keywords: String + $locale: String + $namespace: String! + $sortBy: String + $sortDir: String + $start: Int + $tag: String +) { + Catalog { + catalogOffers( + namespace: $namespace + locale: $locale + params: { + count: $count + country: $country + category: $category + keywords: $keywords + sortBy: $sortBy + sortDir: $sortDir + start: $start + tag: $tag + } + ) { + elements { + isFeatured + collectionOfferIds + title + id + namespace + description + keyImages { + type + url + } + seller { + id + name + } + productSlug + urlSlug + items { + id + namespace + } + customAttributes { + key + value + } + categories { + path + } + price(country: $country) { + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + } + } + } + linkedOfferId + linkedOffer { + effectiveDate + customAttributes { + key + value + } + } + } + paging { + count + total + } + } + } +} +''' + +CATALOG_TAGS_QUERY = ''' +query catalogTags($namespace: String!) { + Catalog { + tags(namespace: $namespace, start: 0, count: 999) { + elements { + aliases + id + name + referenceCount + status + } + } + } +} +''' + +PREREQUISITES_QUERY = ''' +query fetchPrerequisites($offerParams: [OfferParams]) { + Launcher { + prerequisites(offerParams: $offerParams) { + namespace + offerId + missingPrerequisiteItems + satisfiesPrerequisites + } + } +} +''' + +PROMOTIONS_QUERY = ''' +query promotionsQuery( + $namespace: String! + $country: String! + $locale: String! +) { + Catalog { + catalogOffers( + namespace: $namespace + locale: $locale + params: { + category: "freegames" + country: $country + sortBy: "effectiveDate" + sortDir: "asc" + } + ) { + elements { + title + description + id + namespace + categories { + path + } + linkedOfferNs + linkedOfferId + keyImages { + type + url + } + productSlug + promotions { + promotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + upcomingPromotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + } + } + } + } +} +''' + +OFFERS_QUERY = ''' +query catalogQuery( + $productNamespace: String! + $offerId: String! + $locale: String + $country: String! + $includeSubItems: Boolean! +) { + Catalog { + catalogOffer(namespace: $productNamespace, id: $offerId, locale: $locale) { + title + id + namespace + description + effectiveDate + expiryDate + isCodeRedemptionOnly + keyImages { + type + url + } + seller { + id + name + } + productSlug + urlSlug + url + tags { + id + } + items { + id + namespace + } + customAttributes { + key + value + } + categories { + path + } + price(country: $country) { + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + currencyCode + currencyInfo { + decimals + } + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + discountSetting { + discountType + } + } + } + } + } + offerSubItems(namespace: $productNamespace, id: $offerId) + @include(if: $includeSubItems) { + namespace + id + releaseInfo { + appId + platform + } + } + } +} +''' + +SEARCH_STORE_QUERY = ''' +query searchStoreQuery( + $allowCountries: String + $category: String + $count: Int + $country: String! + $keywords: String + $locale: String + $namespace: String + $itemNs: String + $sortBy: String + $sortDir: String + $start: Int + $tag: String + $releaseDate: String + $withPrice: Boolean = false + $withPromotions: Boolean = false +) { + Catalog { + searchStore( + allowCountries: $allowCountries + category: $category + count: $count + country: $country + keywords: $keywords + locale: $locale + namespace: $namespace + itemNs: $itemNs + sortBy: $sortBy + sortDir: $sortDir + releaseDate: $releaseDate + start: $start + tag: $tag + ) { + elements { + title + id + namespace + description + effectiveDate + keyImages { + type + url + } + seller { + id + name + } + productSlug + urlSlug + url + tags { + id + } + items { + id + namespace + } + customAttributes { + key + value + } + categories { + path + } + price(country: $country) @include(if: $withPrice) { + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + currencyCode + currencyInfo { + decimals + } + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + discountSetting { + discountType + } + } + } + } + promotions(category: $category) @include(if: $withPromotions) { + promotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + upcomingPromotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + } + } + paging { + count + total + } + } + } +} +''' \ No newline at end of file diff --git a/rare/components/tabs/store/api/debug.py b/rare/components/tabs/store/api/debug.py new file mode 100644 index 00000000..083fcca2 --- /dev/null +++ b/rare/components/tabs/store/api/debug.py @@ -0,0 +1,29 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeView, QDialog, QVBoxLayout + +from utils.json_formatter import QJsonModel + + +class DebugView(QTreeView): + def __init__(self, data, parent=None): + super(DebugView, self).__init__(parent=parent) + self.setColumnWidth(0, 300) + self.setWordWrap(True) + self.model = QJsonModel(self) + self.setModel(self.model) + self.setContextMenuPolicy(Qt.ActionsContextMenu) + try: + self.model.load(data) + except Exception as e: + pass + self.resizeColumnToContents(0) + + +class DebugDialog(QDialog): + def __init__(self, data, parent=None): + super().__init__(parent=parent) + self.resize(800, 600) + + layout = QVBoxLayout(self) + view = DebugView(data, self) + layout.addWidget(view) diff --git a/rare/components/tabs/store/api/graphql/.graphqlconfig b/rare/components/tabs/store/api/graphql/.graphqlconfig new file mode 100644 index 00000000..54fc8e5f --- /dev/null +++ b/rare/components/tabs/store/api/graphql/.graphqlconfig @@ -0,0 +1,15 @@ +{ + "name": "EGS GraphQL Schema", + "schemaPath": "schema.graphql", + "extensions": { + "endpoints": { + "Default GraphQL Endpoint": { + "url": "http://localhost:8080/graphql", + "headers": { + "user-agent": "JS GraphQL" + }, + "introspect": false + } + } + } +} \ No newline at end of file diff --git a/rare/components/tabs/store/api/graphql/schema.graphql b/rare/components/tabs/store/api/graphql/schema.graphql new file mode 100644 index 00000000..6f3edfda --- /dev/null +++ b/rare/components/tabs/store/api/graphql/schema.graphql @@ -0,0 +1,41 @@ +scalar Date + +type Currency { + decimals: Int + symbol: String +} + +type FormattedPrice { + originalPrice: String + discountPrice: String + intermediatePrice: String +} + +type TotalPrice { + discountPrice: Int + originalPrice: Int + voucherDiscount: Int + discount: Int + currencyCode: String + currencyInfo: Currency + fmtPrice(locale: String): FormattedPrice +} + +type DiscountSetting { + discountType: String +} + +type AppliedRuled { + id: ID + endDate: Date + discountSetting: DiscountSetting +} + +type LineOfferRes { + appliedRules: [AppliedRuled] +} + +type GetPriceRes { + totalPrice: TotalPrice + lineOffers: [LineOfferRes] +} \ No newline at end of file diff --git a/rare/components/tabs/store/api/models/__init__.py b/rare/components/tabs/store/api/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/components/tabs/store/api/models/diesel.py b/rare/components/tabs/store/api/models/diesel.py new file mode 100644 index 00000000..b9629343 --- /dev/null +++ b/rare/components/tabs/store/api/models/diesel.py @@ -0,0 +1,164 @@ +import logging +from dataclasses import dataclass, field +from typing import List, Dict, Any, Type, Optional + +logger = logging.getLogger("DieselModels") + +# lk: Typing overloads for unimplemented types +DieselSocialLinks = Dict + + +@dataclass +class DieselSystemDetailItem: + p_type: Optional[str] = None + minimum: Optional[str] = None + recommended: Optional[str] = None + title: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "DieselSystemDetailItem": + d = src.copy() + tmp = cls( + p_type=d.pop("_type", ""), + minimum=d.pop("minimum", ""), + recommended=d.pop("recommended", ""), + title=d.pop("title", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselSystemDetail: + p_type: Optional[str] = None + details: Optional[List[DieselSystemDetailItem]] = None + system_type: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSystemDetail": + d = src.copy() + _details = d.pop("details", []) + details = [] if _details else None + for item in _details: + detail = DieselSystemDetailItem.from_dict(item) + details.append(detail) + tmp = cls( + p_type=d.pop("_type", ""), + details=details, + system_type=d.pop("systemType", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselSystemDetails: + p_type: Optional[str] = None + languages: Optional[List[str]] = None + rating: Optional[Dict] = None + systems: Optional[List[DieselSystemDetail]] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselSystemDetails"], src: Dict[str, Any]) -> "DieselSystemDetails": + d = src.copy() + _systems = d.pop("systems", []) + systems = [] if _systems else None + for item in _systems: + system = DieselSystemDetail.from_dict(item) + systems.append(system) + tmp = cls( + p_type=d.pop("_type", ""), + languages=d.pop("languages", []), + rating=d.pop("rating", {}), + systems=systems, + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProductAbout: + p_type: Optional[str] = None + desciption: Optional[str] = None + developer_attribution: Optional[str] = None + publisher_attribution: Optional[str] = None + short_description: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselProductAbout"], src: Dict[str, Any]) -> "DieselProductAbout": + d = src.copy() + tmp = cls( + p_type=d.pop("_type", ""), + desciption=d.pop("description", ""), + developer_attribution=d.pop("developerAttribution", ""), + publisher_attribution=d.pop("publisherAttribution", ""), + short_description=d.pop("shortDescription", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProductDetail: + p_type: Optional[str] = None + about: Optional[DieselProductAbout] = None + requirements: Optional[DieselSystemDetails] = None + social_links: Optional[DieselSocialLinks] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselProductDetail": + d = src.copy() + about = DieselProductAbout.from_dict(x) if (x := d.pop("about"), {}) else None + requirements = DieselSystemDetails.from_dict(x) if (x := d.pop("requirements", {})) else None + tmp = cls( + p_type=d.pop("_type", ""), + about=about, + requirements=requirements, + social_links=d.pop("socialLinks", {}), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProduct: + p_id: Optional[str] = None + p_images_: Optional[List[str]] = None + p_locale: Optional[str] = None + p_slug: Optional[str] = None + p_title: Optional[str] = None + p_url_pattern: Optional[str] = None + namespace: Optional[str] = None + pages: Optional[List["DieselProduct"]] = None + data: Optional[DieselProductDetail] = None + product_name: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselProduct"], src: Dict[str, Any]) -> "DieselProduct": + d = src.copy() + _pages = d.pop("pages", []) + pages = [] if _pages else None + for item in _pages: + page = DieselProduct.from_dict(item) + pages.append(page) + data = DieselProductDetail.from_dict(x) if (x := d.pop("data", {})) else None + tmp = cls( + p_id=d.pop("_id", ""), + p_images_=d.pop("_images_", []), + p_locale=d.pop("_locale", ""), + p_slug=d.pop("_slug", ""), + p_title=d.pop("_title", ""), + p_url_pattern=d.pop("_urlPattern", ""), + namespace=d.pop("namespace", ""), + pages=pages, + data=data, + product_name=d.pop("productName", ""), + ) + tmp.unmapped = d + return tmp diff --git a/rare/components/tabs/store/api/models/query.py b/rare/components/tabs/store/api/models/query.py new file mode 100644 index 00000000..925cc500 --- /dev/null +++ b/rare/components/tabs/store/api/models/query.py @@ -0,0 +1,80 @@ +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import List + + +@dataclass +class SearchDateRange: + start_date: datetime = datetime(year=1990, month=1, day=1, tzinfo=timezone.utc) + end_date: datetime = datetime.utcnow() + + def __str__(self): + def fmt_date(date: datetime) -> str: + # lk: The formatting accepted by the GraphQL API is either '%Y-%m-%dT%H:%M:%S.000Z' or '%Y-%m-%dT' + return datetime.strftime(date, '%Y-%m-%dT%H:%M:%S.000Z') + return f"[{fmt_date(self.start_date)},{fmt_date(self.end_date)}]" + + +@dataclass +class SearchStoreQuery: + country: str = "US" + category: str = "games/edition/base|bundles/games|editors|software/edition/base" + count: int = 30 + keywords: str = "" + language: str = "en" + namespace: str = "" + with_mapping: bool = True + item_ns: str = "" + sort_by: str = "releaseDate" + sort_dir: str = "DESC" + start: int = 0 + tag: List[str] = "" + release_date: SearchDateRange = field(default_factory=SearchDateRange) + with_price: bool = True + with_promotions: bool = True + price_range: str = "" + free_game: bool = None + on_sale: bool = None + effective_date: SearchDateRange = field(default_factory=SearchDateRange) + + def __post_init__(self): + self.locale = f"{self.language}-{self.country}" + + def to_dict(self): + payload = { + "allowCountries": self.country, + "category": self.category, + "count": self.count, + "country": self.country, + "keywords": self.keywords, + "locale": self.locale, + "namespace": self.namespace, + "withMapping": self.with_mapping, + "itemNs": self.item_ns, + "sortBy": self.sort_by, + "sortDir": self.sort_dir, + "start": self.start, + "tag": self.tag, + "releaseDate": str(self.release_date), + "withPrice": self.with_price, + "withPromotions": self.with_promotions, + "priceRange": self.price_range, + "freeGame": self.free_game, + "onSale": self.on_sale, + "effectiveDate": str(self.effective_date), + } + # payload.pop("withPromotions") + payload.pop("onSale") + if self.price_range == "free": + payload["freeGame"] = True + payload.pop("priceRange") + elif self.price_range.startswith(""): + payload["priceRange"] = self.price_range.replace("", "") + if self.on_sale: + payload["onSale"] = True + + if self.price_range: + payload["effectiveDate"] = self.effective_date + else: + payload.pop("priceRange") + return payload diff --git a/rare/components/tabs/store/api/models/response.py b/rare/components/tabs/store/api/models/response.py new file mode 100644 index 00000000..182985a1 --- /dev/null +++ b/rare/components/tabs/store/api/models/response.py @@ -0,0 +1,589 @@ +import logging +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import List, Dict, Any, Type, Optional + +logger = logging.getLogger("StoreApiModels") + +# lk: Typing overloads for unimplemented types +DieselSocialLinks = Dict + +CatalogNamespaceModel = Dict +CategoryModel = Dict +CustomAttributeModel = Dict +ItemModel = Dict +SellerModel = Dict +OfferMappingModel = Dict +TagModel = Dict +PromotionsModel = Dict + + +def parse_date(date: str): + return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc) + + +@dataclass +class DieselSystemDetailItem: + p_type: Optional[str] = None + minimum: Optional[str] = None + recommended: Optional[str] = None + title: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "DieselSystemDetailItem": + d = src.copy() + tmp = cls( + p_type=d.pop("_type", ""), + minimum=d.pop("minimum", ""), + recommended=d.pop("recommended", ""), + title=d.pop("title", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselSystemDetail: + p_type: Optional[str] = None + details: Optional[List[DieselSystemDetailItem]] = None + system_type: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSystemDetail": + d = src.copy() + _details = d.pop("details", []) + details = [] if _details else None + for item in _details: + detail = DieselSystemDetailItem.from_dict(item) + details.append(detail) + tmp = cls( + p_type=d.pop("_type", ""), + details=details, + system_type=d.pop("systemType", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselSystemDetails: + p_type: Optional[str] = None + languages: Optional[List[str]] = None + rating: Optional[Dict] = None + systems: Optional[List[DieselSystemDetail]] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselSystemDetails"], src: Dict[str, Any]) -> "DieselSystemDetails": + d = src.copy() + _systems = d.pop("systems", []) + systems = [] if _systems else None + for item in _systems: + system = DieselSystemDetail.from_dict(item) + systems.append(system) + tmp = cls( + p_type=d.pop("_type", ""), + languages=d.pop("languages", []), + rating=d.pop("rating", {}), + systems=systems, + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProductAbout: + p_type: Optional[str] = None + desciption: Optional[str] = None + developer_attribution: Optional[str] = None + publisher_attribution: Optional[str] = None + short_description: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselProductAbout"], src: Dict[str, Any]) -> "DieselProductAbout": + d = src.copy() + tmp = cls( + p_type=d.pop("_type", ""), + desciption=d.pop("description", ""), + developer_attribution=d.pop("developerAttribution", ""), + publisher_attribution=d.pop("publisherAttribution", ""), + short_description=d.pop("shortDescription", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProductDetail: + p_type: Optional[str] = None + about: Optional[DieselProductAbout] = None + requirements: Optional[DieselSystemDetails] = None + social_links: Optional[DieselSocialLinks] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselProductDetail": + d = src.copy() + about = DieselProductAbout.from_dict(x) if (x := d.pop("about"), {}) else None + requirements = DieselSystemDetails.from_dict(x) if (x := d.pop("requirements", {})) else None + tmp = cls( + p_type=d.pop("_type", ""), + about=about, + requirements=requirements, + social_links=d.pop("socialLinks", {}), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProduct: + p_id: Optional[str] = None + p_images_: Optional[List[str]] = None + p_locale: Optional[str] = None + p_slug: Optional[str] = None + p_title: Optional[str] = None + p_url_pattern: Optional[str] = None + namespace: Optional[str] = None + pages: Optional[List["DieselProduct"]] = None + data: Optional[DieselProductDetail] = None + product_name: Optional[str] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DieselProduct"], src: Dict[str, Any]) -> "DieselProduct": + d = src.copy() + _pages = d.pop("pages", []) + pages = [] if _pages else None + for item in _pages: + page = DieselProduct.from_dict(item) + pages.append(page) + data = DieselProductDetail.from_dict(x) if (x := d.pop("data", {})) else None + tmp = cls( + p_id=d.pop("_id", ""), + p_images_=d.pop("_images_", []), + p_locale=d.pop("_locale", ""), + p_slug=d.pop("_slug", ""), + p_title=d.pop("_title", ""), + p_url_pattern=d.pop("_urlPattern", ""), + namespace=d.pop("namespace", ""), + pages=pages, + data=data, + product_name=d.pop("productName", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class ImageUrlModel: + type: Optional[str] = None + url: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + tmp: Dict[str, Any] = {} + tmp.update({}) + if self.type is not None: + tmp["type"] = self.type + if self.url is not None: + tmp["url"] = self.url + return tmp + + @classmethod + def from_dict(cls: Type["ImageUrlModel"], src: Dict[str, Any]) -> "ImageUrlModel": + d = src.copy() + type = d.pop("type", None) + url = d.pop("url", None) + tmp = cls( + type=type, + url=url, + ) + return tmp + + +@dataclass +class KeyImagesModel: + key_images: Optional[List[ImageUrlModel]] = None + + def __getitem__(self, item): + return self.key_images[item] + + def __bool__(self): + return bool(self.key_images) + + def to_list(self) -> List[Dict[str, Any]]: + items: Optional[List[Dict[str, Any]]] = None + if self.key_images is not None: + items = [] + for image_url in self.key_images: + item = image_url.to_dict() + items.append(item) + return items + + @classmethod + def from_list(cls: Type["KeyImagesModel"], src: List[Dict]): + d = src.copy() + key_images = [] + for item in d: + image_url = ImageUrlModel.from_dict(item) + key_images.append(image_url) + tmp = cls(key_images) + 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)) + 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)) + return wide_images + + def for_dimensions(self, w: int, h: int) -> ImageUrlModel: + try: + if w > h: + model = self.available_wide()[0] + else: + model = self.available_tall()[0] + _ = model.url + except Exception as e: + logger.error(e) + logger.error(self.to_list()) + else: + return model + + +TotalPriceModel = Dict +FmtPriceModel = Dict +LineOffersModel = Dict + + +@dataclass +class PriceModel: + total_price: Optional[TotalPriceModel] = None + fmt_price: Optional[FmtPriceModel] = None + line_offers: Optional[LineOffersModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["PriceModel"], src: Dict[str, Any]) -> "PriceModel": + d = src.copy() + tmp = cls( + total_price=d.pop("totalPrice", {}), + fmt_price=d.pop("fmtPrice", {}), + line_offers=d.pop("lineOffers", {}), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class CatalogOfferModel: + catalog_ns: Optional[CatalogNamespaceModel] = None + categories: Optional[List[CategoryModel]] = None + custom_attributes: Optional[List[CustomAttributeModel]] = None + description: Optional[str] = None + effective_date: Optional[datetime] = None + expiry_date: Optional[datetime] = None + id: Optional[str] = None + is_code_redemption_only: Optional[bool] = None + items: Optional[List[ItemModel]] = None + key_images: Optional[KeyImagesModel] = None + namespace: Optional[str] = None + offer_mappings: Optional[List[OfferMappingModel]] = None + offer_type: Optional[str] = None + price: Optional[PriceModel] = None + product_slug: 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 + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["CatalogOfferModel"], src: Dict[str, Any]) -> "CatalogOfferModel": + d = src.copy() + effective_date = parse_date(x) if (x := d.pop("effectiveDate", "")) else None + expiry_date = parse_date(x) if (x := d.pop("expiryDate", "")) else None + key_images = KeyImagesModel.from_list(d.pop("keyImages", [])) + price = PriceModel.from_dict(x) if (x := d.pop("price", {})) else None + viewable_date = parse_date(x) if (x := d.pop("viewableDate", "")) else None + tmp = cls( + catalog_ns=d.pop("catalogNs", {}), + categories=d.pop("categories", []), + custom_attributes=d.pop("customAttributes", []), + description=d.pop("description", ""), + effective_date=effective_date, + expiry_date=expiry_date, + id=d.pop("id", ""), + is_code_redemption_only=d.pop("isCodeRedemptionOnly", None), + items=d.pop("items", []), + key_images=key_images, + namespace=d.pop("namespace", ""), + offer_mappings=d.pop("offerMappings", []), + offer_type=d.pop("offerType", ""), + price=price, + product_slug=d.pop("productSlug", ""), + promotions=d.pop("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, + ) + tmp.unmapped = d + return tmp + + +@dataclass +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 + order: Optional[Any] = None + updated: Optional[datetime] = None + offer: Optional[CatalogOfferModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["WishlistItemModel"], src: Dict[str, Any]) -> "WishlistItemModel": + d = src.copy() + created = parse_date(x) if (x := d.pop("created", "")) else None + offer = CatalogOfferModel.from_dict(x) if (x := d.pop("offer", {})) else None + updated = parse_date(x) if (x := d.pop("updated", "")) else None + tmp = cls( + created=created, + id=d.pop("id", ""), + namespace=d.pop("namespace", ""), + is_first_time=d.pop("isFirstTime", None), + offer_id=d.pop("offerId", ""), + order=d.pop("order", ""), + updated=updated, + offer=offer, + ) + tmp.unmapped = d + return tmp + + +@dataclass +class PagingModel: + count: Optional[int] = None + total: Optional[int] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["PagingModel"], src: Dict[str, Any]) -> "PagingModel": + d = src.copy() + count = d.pop("count", None) + total = d.pop("total", None) + tmp = cls( + count=count, + total=total, + ) + tmp.unmapped = d + return tmp + + +@dataclass +class SearchStoreModel: + elements: Optional[List[CatalogOfferModel]] = None + paging: Optional[PagingModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["SearchStoreModel"], src: Dict[str, Any]) -> "SearchStoreModel": + d = src.copy() + _elements = d.pop("elements", []) + elements = [] if _elements else None + for item in _elements: + 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.unmapped = d + return tmp + + +@dataclass +class CatalogModel: + search_store: 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.unmapped = d + return tmp + + +@dataclass +class WishlistItemsModel: + elements: Optional[List[WishlistItemModel]] = None + paging: Optional[PagingModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["WishlistItemsModel"], src: Dict[str, Any]) -> "WishlistItemsModel": + d = src.copy() + _elements = d.pop("elements", []) + elements = [] if _elements else None + for item in _elements: + 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.unmapped = d + return tmp + + +@dataclass +class RemoveFromWishlistModel: + success: Optional[bool] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["RemoveFromWishlistModel"], src: Dict[str, Any]) -> "RemoveFromWishlistModel": + d = src.copy() + tmp = cls( + success=d.pop("success", None), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class AddToWishlistModel: + wishlist_item: Optional[WishlistItemModel] = None + success: Optional[bool] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + 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.unmapped = d + return tmp + + +@dataclass +class WishlistModel: + wishlist_items: Optional[WishlistItemsModel] = None + remove_from_wishlist: Optional[RemoveFromWishlistModel] = None + add_to_wishlist: 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 + ) + 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, + ) + tmp.unmapped = d + return tmp + + +ProductModel = Dict + + +@dataclass +class DataModel: + product: Optional[ProductModel] = None + catalog: Optional[CatalogModel] = None + wishlist: Optional[WishlistModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DataModel"], src: Dict[str, Any]) -> "DataModel": + d = src.copy() + catalog = CatalogModel.from_dict(x) if (x := d.pop("Catalog", {})) else None + wishlist = WishlistModel.from_dict(x) if (x := d.pop("Wishlist", {})) else None + tmp = cls(product=d.pop("Product", {}), catalog=catalog, wishlist=wishlist) + tmp.unmapped = d + return tmp + + +@dataclass +class ErrorModel: + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["ErrorModel"], src: Dict[str, Any]) -> "ErrorModel": + d = src.copy() + tmp = cls() + tmp.unmapped = d + return tmp + + +@dataclass +class ExtensionsModel: + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["ExtensionsModel"], src: Dict[str, Any]) -> "ExtensionsModel": + d = src.copy() + tmp = cls() + tmp.unmapped = d + return tmp + + +@dataclass +class ResponseModel: + data: Optional[DataModel] = None + errors: Optional[List[ErrorModel]] = None + extensions: Optional[ExtensionsModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["ResponseModel"], src: Dict[str, Any]) -> "ResponseModel": + d = src.copy() + data = DataModel.from_dict(x) if (x := d.pop("data", {})) else None + _errors = d.pop("errors", []) + errors = [] if _errors else None + for item in _errors: + error = ErrorModel.from_dict(item) + errors.append(error) + extensions = ExtensionsModel.from_dict(x) if (x := d.pop("extensions", {})) else None + tmp = cls(data=data, errors=errors, extensions=extensions) + 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 new file mode 100644 index 00000000..92c6ebf2 --- /dev/null +++ b/rare/components/tabs/store/api/models/utils.py @@ -0,0 +1,5 @@ +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 diff --git a/rare/components/tabs/store/constants.py b/rare/components/tabs/store/constants.py index e254dbd3..8a839b93 100644 --- a/rare/components/tabs/store/constants.py +++ b/rare/components/tabs/store/constants.py @@ -44,8 +44,187 @@ class Constants(QObject): ] -game_query = """ -query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, $keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, $sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, $effectiveDate: String) { +__Image = ''' +type +url +alt +''' + +__StorePageMapping = ''' +cmsSlug +offerId +prePurchaseOfferId +''' + +__PageSandboxModel = ''' +pageSlug +pageType +productId +sandboxId +createdDate +updatedDate +deletedDate +mappings { + %s +} +''' % (__StorePageMapping) + +__CatalogNamespace = ''' +parent +displayName +store +home: mappings(pageType: "productHome") { + %s +} +addons: mappings(pageType: "addon--cms-hybrid") { + %s +} +offers: mappings(pageType: "offer") { + %s +} +''' % (__PageSandboxModel, __PageSandboxModel, __PageSandboxModel) + +__CatalogItem = ''' +id +namespace +''' + +__GetPriceRes = ''' + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + currencyCode + currencyInfo { + decimals + symbol + } + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + discountSetting { + discountType + } + } + } +''' + +__Promotions = ''' +promotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } +} +upcomingPromotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } +} +''' + +__CatalogOffer = ''' +title +id +namespace +offerType +expiryDate +status +isCodeRedemptionOnly +description +effectiveDate +keyImages { + %(image)s +} +currentPrice +seller { + id + name +} +productSlug +urlSlug +url +tags { + id + name + groupName +} +items { + %(catalog_item)s +} +customAttributes { + key + value +} +categories { + path +} +catalogNs @include(if: $withMapping) { + %(catalog_namespace)s +} +offerMappings @include(if: $withMapping) { + %(page_sandbox_model)s +} +price(country: $country) @include(if: $withPrice) { + %(get_price_res)s +} +promotions(category: $category) @include(if: $withPromotions) { + %(promotions)s +} +''' % { + "image": __Image, + "catalog_item": __CatalogItem, + "catalog_namespace": __CatalogNamespace, + "page_sandbox_model": __PageSandboxModel, + "get_price_res": __GetPriceRes, + "promotions": __Promotions, +} + +__Pagination = ''' +count +total +''' + +SEARCH_STORE_QUERY = ''' +query searchStoreQuery( + $allowCountries: String + $category: String + $count: Int + $country: String! + $keywords: String + $locale: String + $namespace: String + $withMapping: Boolean = false + $itemNs: String + $sortBy: String + $sortDir: String + $start: Int + $tag: String + $releaseDate: String + $withPrice: Boolean = false + $withPromotions: Boolean = false + $priceRange: String + $freeGame: Boolean + $onSale: Boolean + $effectiveDate: String +) { Catalog { searchStore( allowCountries: $allowCountries @@ -67,452 +246,209 @@ query searchStoreQuery($allowCountries: String, $category: String, $count: Int, effectiveDate: $effectiveDate ) { elements { - title - id - namespace - description - effectiveDate - keyImages { - type - url - } - currentPrice - seller { - id - name - } - productSlug - urlSlug - url - tags { - id - } - items { - id - namespace - } - customAttributes { - key - value - } - categories { - path - } - catalogNs @include(if: $withMapping) { - mappings(pageType: "productHome") { - pageSlug - pageType - } - } - offerMappings @include(if: $withMapping) { - pageSlug - pageType - } - price(country: $country) @include(if: $withPrice) { - totalPrice { - discountPrice - originalPrice - voucherDiscount - discount - currencyCode - currencyInfo { - decimals - } - fmtPrice(locale: $locale) { - originalPrice - discountPrice - intermediatePrice - } - } - lineOffers { - appliedRules { - id - endDate - discountSetting { - discountType - } - } - } - } - promotions(category: $category) @include(if: $withPromotions) { - promotionalOffers { - promotionalOffers { - startDate - endDate - discountSetting { - discountType - discountPercentage - } - } - } - upcomingPromotionalOffers { - promotionalOffers { - startDate - endDate - discountSetting { - discountType - discountPercentage - } - } - } - } + %s } paging { - count - total + %s } } } } -""" +''' % (__CatalogOffer, __Pagination) -search_query = """ -query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, $keywords: String, $locale: String, $namespace: String, $itemNs: String, $sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, $effectiveDate: String) { - Catalog { - searchStore( - allowCountries: $allowCountries - category: $category - count: $count - country: $country - keywords: $keywords - locale: $locale - namespace: $namespace - itemNs: $itemNs - sortBy: $sortBy - sortDir: $sortDir - releaseDate: $releaseDate - start: $start - tag: $tag - priceRange: $priceRange - freeGame: $freeGame - onSale: $onSale - effectiveDate: $effectiveDate - ) { - elements { - title - id - namespace - description - effectiveDate - keyImages { - type - url - } - currentPrice - seller { - id - name - } - productSlug - urlSlug - url - tags { - id - } - items { - id - namespace - } - customAttributes { - key - value - } - categories { - path - } - catalogNs { - mappings(pageType: "productHome") { - pageSlug - pageType - } - } - offerMappings { - pageSlug - pageType - } - price(country: $country) @include(if: $withPrice) { - totalPrice { - discountPrice - originalPrice - voucherDiscount - discount - currencyCode - currencyInfo { - decimals - } - fmtPrice(locale: $locale) { - originalPrice - discountPrice - intermediatePrice - } - } - lineOffers { - appliedRules { - id - endDate - discountSetting { - discountType - } - } - } - } - promotions(category: $category) @include(if: $withPromotions) { - promotionalOffers { - promotionalOffers { - startDate - endDate - discountSetting { - discountType - discountPercentage - } - } - } - upcomingPromotionalOffers { - promotionalOffers { - startDate - endDate - discountSetting { - discountType - discountPercentage - } - } - } - } - } - paging { - count - total - } - } - } +__WISHLIST_ITEM = ''' +id +order +created +offerId +updated +namespace +isFirstTime +offer(locale: $locale) { + %s } -""" +''' % __CatalogOffer -wishlist_query = """ -query wishlistQuery($country:String!, $locale:String) { +WISHLIST_QUERY = ''' +query wishlistQuery( + $country: String! + $locale: String + $category: String + $withMapping: Boolean = false + $withPrice: Boolean = false + $withPromotions: Boolean = false +) { Wishlist { wishlistItems { elements { - id - order - created - offerId - updated - namespace - offer(locale: $locale) { - productSlug - urlSlug - title - id - namespace - offerType - expiryDate - status - isCodeRedemptionOnly - description - effectiveDate - keyImages { - type - url - } - seller { - id - name - } - productSlug - urlSlug - items { - id - namespace - } - customAttributes { - key - value - } - catalogNs { - mappings(pageType: "productHome") { - pageSlug - pageType + %s + } + } + } +} +''' % __WISHLIST_ITEM + +WISHLIST_ADD_QUERY = ''' +mutation addWishlistMutation( + $namespace: String! + $offerId: String! + $country: String! + $locale: String + $category: String + $withMapping: Boolean = false + $withPrice: Boolean = false + $withPromotions: Boolean = false +) { + Wishlist { + addToWishlist( + namespace: $namespace + offerId: $offerId + ) { + wishlistItem { + %s + } + success + } + } +} +''' % __WISHLIST_ITEM + +WISHLIST_REMOVE_QUERY = ''' +mutation removeFromWishlistMutation( + $namespace: String! + $offerId: String! + $operation: RemoveOperation! +) { + Wishlist { + removeFromWishlist( + namespace: $namespace + offerId: $offerId + operation: $operation + ) { + success + } + } +} +''' + +COUPONS_QUERY = ''' +query getCoupons( + $currencyCountry: String! + $identityId: String! + $locale: String +) { + CodeRedemption { + coupons( + currencyCountry: $currencyCountry + identityId: $identityId + includeSalesEventInfo: true + ) { + code + codeStatus + codeType + consumptionMetadata { + amountDisplay { + amount + currency + placement + symbol + } + minSalesPriceDisplay { + amount + currency + placement + symbol + } + } + endDate + namespace + salesEvent(locale: $locale) { + eventName + eventSlug + voucherImages { + type + url + } + voucherLink + } + startDate + } + } +} +''' + +STORE_CONFIG_QUERY = ''' +query getStoreConfig( + $includeCriticReviews: Boolean = false + $locale: String! + $sandboxId: String! + $templateId: String +) { + Product { + sandbox(sandboxId: $sandboxId) { + configuration(locale: $locale, templateId: $templateId) { + ... on StoreConfiguration { + configs { + shortDescription + criticReviews @include(if: $includeCriticReviews) { + openCritic } - } - offerMappings { - pageSlug - pageType - } - categories { - path - } - price(country: $country) { - totalPrice { - discountPrice - originalPrice - voucherDiscount - discount - fmtPrice(locale: $locale) { - originalPrice - discountPrice - intermediatePrice + socialLinks { + platform + url + } + supportedAudio + supportedText + tags(locale: $locale) { + id + name + groupName + } + technicalRequirements { + macos { + minimum + recommended + title } - currencyCode - currencyInfo { - decimals - symbol + windows { + minimum + recommended + title } } - lineOffers { - appliedRules { - id - endDate + } + } + ... on HomeConfiguration { + configs { + keyImages { + ... on KeyImage { + type + url + alt } } + longDescription } } } } } } -""" - -add_to_wishlist_query = """ -mutation addWishlistMutation($namespace: String!, $offerId: String!, $country:String!, $locale:String) { - Wishlist { - addToWishlist(namespace: $namespace, offerId: $offerId) { - wishlistItem { - id, - order, - created, - offerId, - updated, - namespace, - isFirstTime - offer { - productSlug - urlSlug - title - id - namespace - offerType - expiryDate - status - isCodeRedemptionOnly - description - effectiveDate - keyImages { - type - url - } - seller { - id - name - } - productSlug - urlSlug - items { - id - namespace - } - customAttributes { - key - value - } - catalogNs { - mappings(pageType: "productHome") { - pageSlug - pageType - } - } - offerMappings { - pageSlug - pageType - } - categories { - path - } - price(country: $country) { - totalPrice { - discountPrice - originalPrice - voucherDiscount - discount - fmtPrice(locale: $locale) { - originalPrice - discountPrice - intermediatePrice - } - currencyCode - currencyInfo { - decimals - symbol - } - } - lineOffers { - appliedRules { - id - endDate - } - } - } - } - - } - success - } - } -} -""" - -remove_from_wishlist_query = """ -mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) { - Wishlist { - removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) { - success - } - } -} -""" - -coupon_query = """ -query getCoupons($currencyCountry: String!, $identityId: String!, $locale: String) { - CodeRedemption { - coupons(currencyCountry: $currencyCountry, identityId: $identityId, includeSalesEventInfo: true) { - code - codeStatus - codeType - consumptionMetadata { - amountDisplay { - amount - currency - placement - symbol - } - minSalesPriceDisplay { - amount - currency - placement - symbol - } - } - endDate - namespace - salesEvent(locale: $locale) { - eventName - eventSlug - voucherImages { - type - url - } - voucherLink - } - startDate - } - } -} -""" +''' -# if __name__ == "__main__": -# from sgqlc import introspection, codegen -# -# coupon = codegen.operation.parse_graphql(coupon_query) -# codegen.schema. -# print(coupon.) +def compress_query(query: str) -> str: + return query.replace(" ", "").replace("\n", " ") + + +game_query = compress_query(SEARCH_STORE_QUERY) +search_query = compress_query(SEARCH_STORE_QUERY) +wishlist_query = compress_query(WISHLIST_QUERY) +wishlist_add_query = compress_query(WISHLIST_ADD_QUERY) +wishlist_remove_query = compress_query(WISHLIST_REMOVE_QUERY) +coupons_query = compress_query(COUPONS_QUERY) +store_config_query = compress_query(STORE_CONFIG_QUERY) + + +if __name__ == "__main__": + print(SEARCH_STORE_QUERY) diff --git a/rare/components/tabs/store/game_info.py b/rare/components/tabs/store/game_info.py index 2d399b17..19a27524 100644 --- a/rare/components/tabs/store/game_info.py +++ b/rare/components/tabs/store/game_info.py @@ -1,4 +1,6 @@ import logging +from pprint import pprint +from typing import List from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QFont, QDesktopServices, QFontMetrics @@ -10,13 +12,14 @@ from PyQt5.QtWidgets import ( QSizePolicy, ) -from rare.components.tabs.store.shop_models import ShopGame +from rare.components.tabs.store.api.models.response import CatalogOfferModel, DieselProduct, DieselProductDetail from rare.shared import LegendaryCoreSingleton from rare.shared.image_manager import ImageSize from rare.ui.components.tabs.store.shop_game_info import Ui_ShopInfo from rare.utils.misc import icon from rare.widgets.side_tab import SideTabWidget, SideTabContents from rare.widgets.elide_label import ElideLabel +from .api.debug import DebugDialog from .image_widget import ShopImageWidget logger = logging.getLogger("ShopInfo") @@ -37,45 +40,46 @@ class ShopGameInfo(QWidget, SideTabContents): self.image.setFixedSize(ImageSize.Normal) self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop) - self.game: ShopGame = None + self.offer: CatalogOfferModel = None self.data: dict = {} self.ui.wishlist_button.clicked.connect(self.add_to_wishlist) + self.ui.wishlist_button.setVisible(True) self.in_wishlist = False self.wishlist = [] - self.requirements_tabs: SideTabWidget = SideTabWidget(parent=self.ui.requirements_group) + self.requirements_tabs: SideTabWidget = SideTabWidget(parent=self.ui.requirements_frame) self.requirements_tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.ui.requirements_layout.addWidget(self.requirements_tabs) self.setDisabled(True) - def handle_wishlist_update(self, data): - if data and data[0] == "error": + def handle_wishlist_update(self, wishlist: List[CatalogOfferModel]): + if wishlist and wishlist[0] == "error": return - self.wishlist = [i["offer"]["title"] for i in data] - if self.title_str in self.wishlist: + self.wishlist = [game.id for game in wishlist] + if self.id_str in self.wishlist: self.in_wishlist = True - self.ui.wishlist_button.setVisible(True) self.ui.wishlist_button.setText(self.tr("Remove from Wishlist")) else: self.in_wishlist = False - self.ui.wishlist_button.setVisible(False) - def update_game(self, data: dict): - self.set_title.emit(data["title"]) - self.ui.title.setText(data["title"]) - self.title_str = data["title"] - self.id_str = data["id"] + def update_game(self, offer: CatalogOfferModel): + debug = DebugDialog(offer.__dict__, None) + debug.exec() + self.set_title.emit(offer.title) + 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) - # lk: delete tabs in inverse order because indices are updated on deletion + # 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 = data["productSlug"] + slug = offer.product_slug if not slug: - for mapping in data["offerMappings"]: + for mapping in offer.offer_mappings: if mapping["pageType"] == "productHome": slug = mapping["pageSlug"] break @@ -86,7 +90,7 @@ class ShopGameInfo(QWidget, SideTabContents): slug = slug.replace("/home", "") self.slug = slug - if data["namespace"] in self.installed: + if offer.namespace in self.installed: self.ui.open_store_button.setText(self.tr("Show Game on Epic Page")) self.ui.owned_label.setVisible(True) else: @@ -94,39 +98,44 @@ class ShopGameInfo(QWidget, SideTabContents): self.ui.owned_label.setVisible(False) self.ui.price.setText(self.tr("Loading")) - self.ui.wishlist_button.setVisible(False) # self.title.setText(self.tr("Loading")) # self.image.setPixmap(QPixmap()) - self.data = data is_bundle = False - for i in data["categories"]: + for i in offer.categories: if "bundles" in i.get("path", ""): is_bundle = True # init API request if slug: - self.api_core.get_game(slug, is_bundle, self.data_received) + self.api_core.get_game(offer.product_slug, is_bundle, self.data_received) # else: # self.data_received({}) + self.offer = offer def add_to_wishlist(self): if not self.in_wishlist: - return - # self.api_core.add_to_wishlist(self.game.namespace, self.game.offer_id, - # lambda success: self.wishlist_button.setText(self.tr("Remove from wishlist")) - # if success else self.wishlist_button.setText("Something goes wrong")) + self.api_core.add_to_wishlist( + self.offer.namespace, + self.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.game.namespace, - self.game.offer_id, - lambda success: self.ui.wishlist_button.setVisible(False) + self.offer.namespace, + self.offer.id, + lambda success: self.ui.wishlist_button.setText(self.tr("Add to wishlist")) if success - else self.ui.wishlist_button.setText("Something goes wrong"), + else self.ui.wishlist_button.setText("Something went wrong"), ) - def data_received(self, game): + def data_received(self, product: DieselProduct): try: - self.game = ShopGame.from_json(game, self.data) + if product.pages: + product_data: DieselProductDetail = product.pages[0].data + else: + product_data: DieselProductDetail = product.data except Exception as e: raise e logger.error(str(e)) @@ -150,17 +159,19 @@ class ShopGameInfo(QWidget, SideTabContents): # self.title.setText(self.game.title) self.ui.price.setFont(QFont()) - if self.game.price == "0" or self.game.price == 0: + price = self.offer.price.total_price["fmtPrice"]["originalPrice"] + discount_price = self.offer.price.total_price["fmtPrice"]["discountPrice"] + if price == "0" or price == 0: self.ui.price.setText(self.tr("Free")) else: - self.ui.price.setText(self.game.price) - if self.game.price != self.game.discount_price: + self.ui.price.setText(price) + if price != discount_price: font = QFont() font.setStrikeOut(True) self.ui.price.setFont(font) self.ui.discount_price.setText( - self.game.discount_price - if self.game.discount_price != "0" + discount_price + if discount_price != "0" else self.tr("Free") ) self.ui.discount_price.setVisible(True) @@ -171,8 +182,9 @@ class ShopGameInfo(QWidget, SideTabContents): bold_font.setBold(True) fm = QFontMetrics(self.font()) - if self.game.reqs: - for system in self.game.reqs: + requirements = product_data.requirements + if requirements and requirements.systems: + for system in requirements.systems: req_widget = QWidget(self.requirements_tabs) req_layout = QGridLayout(req_widget) req_widget.layout().setAlignment(Qt.AlignTop) @@ -185,53 +197,57 @@ class ShopGameInfo(QWidget, SideTabContents): req_layout.addWidget(rec_label, 0, 2) req_layout.setColumnStretch(1, 2) req_layout.setColumnStretch(2, 2) - for i, (key, value) in enumerate(self.game.reqs.get(system, {}).items()): - req_layout.addWidget(QLabel(key, parent=req_widget), i + 1, 0) - min_label = ElideLabel(value[0], parent=req_widget) + for i, detail in enumerate(system.details): + req_layout.addWidget(QLabel(detail.title, parent=req_widget), i + 1, 0) + min_label = ElideLabel(detail.minimum, parent=req_widget) req_layout.addWidget(min_label, i + 1, 1) - rec_label = ElideLabel(value[1], parent=req_widget) + rec_label = ElideLabel(detail.recommended, parent=req_widget) req_layout.addWidget(rec_label, i + 1, 2) - self.requirements_tabs.addTab(req_widget, system) - # 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.requirements_tabs.setEnabled(True) - if self.game.image_urls.front_tall: - img_url = self.game.image_urls.front_tall - elif self.game.image_urls.offer_image_tall: - img_url = self.game.image_urls.offer_image_tall - elif self.game.image_urls.product_logo: - img_url = self.game.image_urls.product_logo + 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) else: - img_url = "" - self.image.fetchPixmap(img_url) + self.ui.requirements_frame.setVisible(False) + + key_images = self.offer.key_images + img_url = key_images.for_dimensions(self.image.size().width(), self.image.size().height()) + self.image.fetchPixmap(img_url.url) # self.image_stack.setCurrentIndex(0) - try: - if isinstance(self.game.developer, list): - self.ui.dev.setText(", ".join(self.game.developer)) - else: - self.ui.dev.setText(self.game.developer) - except KeyError: - pass - self.ui.tags.setText(", ".join(self.game.tags)) + about = product_data.about + self.ui.description_label.setText(about.desciption) + self.ui.dev.setText(about.developer_attribution) + # try: + # if isinstance(aboudeveloper, list): + # self.ui.dev.setText(", ".join(self.game.developer)) + # else: + # self.ui.dev.setText(self.game.developer) + # except KeyError: + # pass + tags = product_data.unmapped["meta"].get("tags", []) + self.ui.tags.setText(", ".join(tags)) # clear Layout for b in self.ui.social_group.findChildren(SocialButton, options=Qt.FindDirectChildrenOnly): self.ui.social_layout.removeWidget(b) b.deleteLater() + links = product_data.social_links link_count = 0 - for name, url in self.game.links: - - if name.lower() == "homepage": + for name, url in links.items(): + if name == "_type": + continue + name = name.replace("link", "").lower() + if name == "homepage": icn = icon("mdi.web", "fa.search", scale_factor=1.5) else: try: - icn = icon(f"mdi.{name.lower()}", f"fa.{name.lower()}", scale_factor=1.5) + icn = icon(f"mdi.{name}", f"fa.{name}", scale_factor=1.5) except Exception as e: logger.error(str(e)) continue @@ -244,10 +260,10 @@ class ShopGameInfo(QWidget, SideTabContents): self.setEnabled(True) - def add_wishlist_items(self, wishlist): - wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"] - for game in wishlist: - self.wishlist.append(game["offer"]["title"]) + # def add_wishlist_items(self, wishlist: List[CatalogGameModel]): + # wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"] + # for game in wishlist: + # self.wishlist.append(game["offer"]["title"]) def button_clicked(self): return diff --git a/rare/components/tabs/store/game_widgets.py b/rare/components/tabs/store/game_widgets.py index 09ba8873..9b453969 100644 --- a/rare/components/tabs/store/game_widgets.py +++ b/rare/components/tabs/store/game_widgets.py @@ -3,42 +3,44 @@ import logging from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtGui import QMouseEvent from PyQt5.QtWidgets import QPushButton +from orjson import orjson -from rare.components.tabs.store.shop_models import ImageUrlModel +from rare.components.tabs.store.api.models.response import CatalogOfferModel from rare.shared.image_manager import ImageSize from rare.utils.misc import qta_icon from rare.utils.qt_requests import QtRequestManager +from .api.debug import DebugDialog from .image_widget import ShopImageWidget logger = logging.getLogger("GameWidgets") class GameWidget(ShopImageWidget): - show_info = pyqtSignal(dict) + show_info = pyqtSignal(CatalogOfferModel) - def __init__(self, manager: QtRequestManager, json_info=None, parent=None): + def __init__(self, manager: QtRequestManager, catalog_game: CatalogOfferModel = None, parent=None): super(GameWidget, self).__init__(manager, parent=parent) self.setFixedSize(ImageSize.Wide) self.ui.setupUi(self) - self.json_info = json_info - if json_info: - self.init_ui(json_info) + self.catalog_game = catalog_game + if catalog_game: + self.init_ui(catalog_game) - def init_ui(self, json_info): - if not json_info: + def init_ui(self, game: CatalogOfferModel): + if not game: self.ui.title_label.setText(self.tr("An error occurred")) return - self.ui.title_label.setText(json_info.get("title")) - for attr in json_info["customAttributes"]: + self.ui.title_label.setText(game.title) + for attr in game.custom_attributes: if attr["key"] == "developerName": developer = attr["value"] break else: - developer = json_info["seller"]["name"] + developer = game.seller["name"] self.ui.developer_label.setText(developer) - price = json_info["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - discount_price = json_info["price"]["totalPrice"]["fmtPrice"]["discountPrice"] + price = game.price.total_price["fmtPrice"]["originalPrice"] + discount_price = game.price.total_price["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() @@ -48,43 +50,48 @@ class GameWidget(ShopImageWidget): else: self.ui.discount_label.setVisible(False) - for c in r'<>?":|\/*': - json_info["title"] = json_info["title"].replace(c, "") + key_images = game.key_images + self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url) - for img in json_info["keyImages"]: - if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo",]: - if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game": - continue - self.fetchPixmap(img["url"]) - break - else: - logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) + # for img in json_info["keyImages"]: + # if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"]: + # if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game": + # continue + # self.fetchPixmap(img["url"]) + # break + # else: + # logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) def mousePressEvent(self, a0: QMouseEvent) -> None: if a0.button() == Qt.LeftButton: a0.accept() - self.show_info.emit(self.json_info) + self.show_info.emit(self.catalog_game) + if a0.button() == Qt.RightButton: + a0.accept() + print(self.catalog_game.__dict__) + dialog = DebugDialog(self.catalog_game.__dict__, self) + dialog.show() class WishlistWidget(ShopImageWidget): - open_game = pyqtSignal(dict) - delete_from_wishlist = pyqtSignal(dict) + open_game = pyqtSignal(CatalogOfferModel) + delete_from_wishlist = pyqtSignal(CatalogOfferModel) - def __init__(self, manager: QtRequestManager, game: dict, parent=None): + def __init__(self, manager: QtRequestManager, catalog_game: CatalogOfferModel, parent=None): super(WishlistWidget, self).__init__(manager, parent=parent) self.setFixedSize(ImageSize.Wide) self.ui.setupUi(self) - self.game = game - for attr in game["customAttributes"]: + self.game = catalog_game + for attr in catalog_game.custom_attributes: if attr["key"] == "developerName": developer = attr["value"] break else: - developer = game["seller"]["name"] - original_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - discount_price = game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] + developer = catalog_game.seller["name"] + original_price = catalog_game.price.total_price["fmtPrice"]["originalPrice"] + discount_price = catalog_game.price.total_price["fmtPrice"]["discountPrice"] - self.ui.title_label.setText(game.get("title")) + self.ui.title_label.setText(catalog_game.title) self.ui.developer_label.setText(developer) self.ui.price_label.setText(f'{original_price if original_price != "0" else self.tr("Free")}') if original_price != discount_price: @@ -94,11 +101,10 @@ class WishlistWidget(ShopImageWidget): self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') else: self.ui.discount_label.setVisible(False) - image_model = ImageUrlModel.from_json(game["keyImages"]) - url = image_model.front_wide - if not url: - url = image_model.offer_image_wide - self.fetchPixmap(url) + key_images = catalog_game.key_images + self.fetchPixmap( + key_images.for_dimensions(self.width(), self.height()).url + ) self.delete_button = QPushButton(self) self.delete_button.setIcon(icon("mdi.delete", color="white")) @@ -113,5 +119,7 @@ class WishlistWidget(ShopImageWidget): a0.accept() self.open_game.emit(self.game) # right - elif a0.button() == Qt.RightButton: - pass # self.showMenu(e) + if a0.button() == Qt.RightButton: + a0.accept() + dialog = DebugDialog(self.game.__dict__, self) + dialog.show() diff --git a/rare/components/tabs/store/search_results.py b/rare/components/tabs/store/search_results.py index 9860781e..24ec3fd1 100644 --- a/rare/components/tabs/store/search_results.py +++ b/rare/components/tabs/store/search_results.py @@ -13,9 +13,12 @@ from rare.widgets.flow_layout import FlowLayout from rare.widgets.side_tab import SideTabContents from .image_widget import ShopImageWidget +from .api.debug import DebugDialog +from .api.models.response import CatalogOfferModel + class SearchResults(QScrollArea, SideTabContents): - show_info = pyqtSignal(dict) + show_info = pyqtSignal(CatalogOfferModel) def __init__(self, api_core, parent=None): super(SearchResults, self).__init__(parent=parent) @@ -59,22 +62,20 @@ class SearchResults(QScrollArea, SideTabContents): class SearchResultItem(ShopImageWidget): - show_info = pyqtSignal(dict) + show_info = pyqtSignal(CatalogOfferModel) - def __init__(self, manager: QtRequestManager, result: dict, parent=None): + def __init__(self, manager: QtRequestManager, catalog_game: CatalogOfferModel, parent=None): super(SearchResultItem, self).__init__(manager, parent=parent) self.setFixedSize(ImageSize.Normal) self.ui.setupUi(self) - for img in result["keyImages"]: - if img["type"] in ["DieselStoreFrontTall", "OfferImageTall", "Thumbnail", "ProductLogo"]: - self.fetchPixmap(img["url"]) - break - else: - print("No image found") - self.ui.title_label.setText(result["title"]) - price = result["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - discount_price = result["price"]["totalPrice"]["fmtPrice"]["discountPrice"] + key_images = catalog_game.key_images + 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"] self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') if price != discount_price: font = self.ui.price_label.font() @@ -84,9 +85,14 @@ class SearchResultItem(ShopImageWidget): else: self.ui.discount_label.setVisible(False) - self.res = result + self.catalog_game = catalog_game def mousePressEvent(self, a0: QMouseEvent) -> None: if a0.button() == Qt.LeftButton: a0.accept() - self.show_info.emit(self.res) + self.show_info.emit(self.catalog_game) + if a0.button() == Qt.RightButton: + a0.accept() + dialog = DebugDialog(self.catalog_game.__dict__, self) + dialog.show() + diff --git a/rare/components/tabs/store/shop_api_core.py b/rare/components/tabs/store/shop_api_core.py index 5d411b04..662df6d0 100644 --- a/rare/components/tabs/store/shop_api_core.py +++ b/rare/components/tabs/store/shop_api_core.py @@ -1,21 +1,33 @@ from logging import getLogger +from typing import List, Callable from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtWidgets import QApplication +from rare.components.tabs.store.api.debug import DebugDialog from rare.components.tabs.store.constants import ( wishlist_query, search_query, - add_to_wishlist_query, - remove_from_wishlist_query, + wishlist_add_query, + wishlist_remove_query, ) from rare.components.tabs.store.shop_models import BrowseModel from rare.utils.paths import cache_dir from rare.utils.qt_requests import QtRequests +from .api.models.query import SearchStoreQuery +from .api.models.response import ( + DieselProduct, + ResponseModel, + CatalogOfferModel, +) logger = getLogger("ShopAPICore") graphql_url = "https://graphql.epicgames.com/graphql" +DEBUG: Callable[[], bool] = lambda: "--debug" in QApplication.arguments() + + class ShopApiCore(QObject): update_wishlist = pyqtSignal() @@ -25,6 +37,7 @@ class ShopApiCore(QObject): self.language_code: str = language self.country_code: str = country self.locale = f"{self.language_code}-{self.country_code}" + self.locale = "en-US" self.manager = QtRequests(parent=self) self.authed_manager = QtRequests(token=token, parent=self) self.cached_manager = QtRequests(cache=str(cache_dir().joinpath("store")), parent=self) @@ -39,54 +52,67 @@ class ShopApiCore(QObject): "country": self.country_code, "allowCountries": self.country_code, } - self.manager.get(url, lambda data: self._handle_free_games(data, handle_func), params=params) + self.manager.get(url, lambda data: self.__handle_free_games(data, handle_func), params=params) - def _handle_free_games(self, data, handle_func): + @staticmethod + def __handle_free_games(data, handle_func): try: - results: dict = data["data"]["Catalog"]["searchStore"]["elements"] - except KeyError: + response = ResponseModel.from_dict(data) + results: List[CatalogOfferModel] = response.data.catalog.search_store.elements + handle_func(results) + except KeyError as e: + if DEBUG(): + raise e logger.error("Free games Api request failed") handle_func(["error", "Key error"]) return except Exception as e: + if DEBUG(): + raise e logger.error(f"Free games Api request failed: {e}") handle_func(["error", e]) return - handle_func(results) def get_wishlist(self, handle_func): self.authed_manager.post( graphql_url, - lambda data: self._handle_wishlist(data, handle_func), + lambda data: self.__handle_wishlist(data, handle_func), { "query": wishlist_query, "variables": { "country": self.country_code, "locale": self.locale, + "withPrice": True, }, }, ) - def _handle_wishlist(self, data, handle_func): + @staticmethod + def __handle_wishlist(data, handle_func): try: - results: list = data["data"]["Wishlist"]["wishlistItems"]["elements"] - except KeyError: + response = ResponseModel.from_dict(data) + if response.errors: + logger.error(response.errors) + handle_func(response.data.wishlist.wishlist_items.elements) + except KeyError as e: + if DEBUG(): + raise e logger.error("Free games Api request failed") handle_func(["error", "Key error"]) return except Exception as e: + if DEBUG(): + raise e logger.error(f"Free games Api request failed: {e}") handle_func(["error", e]) return - handle_func(results) - - def search_game(self, name, handle_func): + def search_game(self, name, handler): payload = { "query": search_query, "variables": { "category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 10, + "count": 20, "country": self.country_code, "keywords": name, "locale": self.locale, @@ -99,42 +125,56 @@ class ShopApiCore(QObject): }, } - self.manager.post(graphql_url, lambda data: self._handle_search(data, handle_func), payload) + self.manager.post(graphql_url, lambda data: self.__handle_search(data, handler), payload) - def _handle_search(self, data, handle_func): + @staticmethod + def __handle_search(data, handler): try: - handle_func(data["data"]["Catalog"]["searchStore"]["elements"]) + response = ResponseModel.from_dict(data) + handler(response.data.catalog.search_store.elements) except KeyError as e: logger.error(str(e)) - handle_func([]) + if DEBUG(): + raise e + handler([]) except Exception as e: logger.error(f"Search Api request failed: {e}") - handle_func([]) + if DEBUG(): + raise e + handler([]) return - def browse_games(self, browse_model: BrowseModel, handle_func): + def browse_games(self, browse_model: SearchStoreQuery, handle_func): if self.browse_active: self.next_browse_request = (browse_model, handle_func) return self.browse_active = True payload = { "query": search_query, - "variables": browse_model.__dict__ + "variables": browse_model.to_dict() } - self.manager.post(graphql_url, lambda data: self._handle_browse_games(data, handle_func), payload) + debug = DebugDialog(payload["variables"], None) + debug.exec() + self.manager.post(graphql_url, lambda data: self.__handle_browse_games(data, handle_func), payload) - def _handle_browse_games(self, data, handle_func): + def __handle_browse_games(self, data, handle_func): + debug = DebugDialog(data, None) + debug.exec() self.browse_active = False if data is None: data = {} if not self.next_browse_request: - try: - handle_func(data["data"]["Catalog"]["searchStore"]["elements"]) + response = ResponseModel.from_dict(data) + handle_func(response.data.catalog.search_store.elements) except KeyError as e: + if DEBUG(): + raise e logger.error(str(e)) handle_func([]) except Exception as e: + if DEBUG(): + raise e logger.error(f"Browse games Api request failed: {e}") handle_func([]) return @@ -143,62 +183,72 @@ class ShopApiCore(QObject): self.next_browse_request = tuple(()) def get_game(self, slug: str, is_bundle: bool, handle_func): - url = f"https://store-content.ak.epicgames.com/api/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" - self.manager.get(url, lambda data: self._handle_get_game(data, handle_func)) + url = "https://store-content.ak.epicgames.com/api" + url += f"/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" + self.manager.get(url, lambda data: self.__handle_get_game(data, handle_func)) - def _handle_get_game(self, data, handle_func): + @staticmethod + def __handle_get_game(data, handle_func): + debug = DebugDialog(data, None) + debug.exec() try: - handle_func(data) + product = DieselProduct.from_dict(data) + handle_func(product) except Exception as e: - raise e + if DEBUG(): + raise e logger.error(str(e)) # handle_func({}) # needs a captcha def add_to_wishlist(self, namespace, offer_id, handle_func: callable): payload = { + "query": wishlist_add_query, "variables": { "offerId": offer_id, "namespace": namespace, "country": self.country_code, "locale": self.locale, }, - "query": add_to_wishlist_query, } self.authed_manager.post(graphql_url, lambda data: self._handle_add_to_wishlist(data, handle_func), payload) def _handle_add_to_wishlist(self, data, handle_func): + debug = DebugDialog(data, None) + debug.exec() try: - data = data["data"]["Wishlist"]["addToWishlist"] - if data["success"]: - handle_func(True) - else: - handle_func(False) + response = ResponseModel.from_dict(data) + data = response.data.wishlist.add_to_wishlist + handle_func(data.success) except Exception as e: + if DEBUG(): + raise e logger.error(str(e)) handle_func(False) self.update_wishlist.emit() def remove_from_wishlist(self, namespace, offer_id, handle_func: callable): payload = { + "query": wishlist_remove_query, "variables": { "offerId": offer_id, "namespace": namespace, "operation": "REMOVE", }, - "query": remove_from_wishlist_query, } self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, handle_func), payload) def _handle_remove_from_wishlist(self, data, handle_func): + debug = DebugDialog(data, None) + debug.exec() try: - data = data["data"]["Wishlist"]["removeFromWishlist"] - if data["success"]: - handle_func(True) - else: - handle_func(False) + response = ResponseModel.from_dict(data) + data = response.data.wishlist.remove_from_wishlist + handle_func(data.success) except Exception as e: + if DEBUG(): + raise e logger.error(str(e)) handle_func(False) self.update_wishlist.emit() diff --git a/rare/components/tabs/store/shop_models.py b/rare/components/tabs/store/shop_models.py deleted file mode 100644 index f124c317..00000000 --- a/rare/components/tabs/store/shop_models.py +++ /dev/null @@ -1,195 +0,0 @@ -import datetime -from dataclasses import dataclass -from typing import List, Dict -import epicstore_api.queries as egs_query - - -class ImageUrlModel: - def __init__( - self, - front_tall: str = "", - offer_image_tall: str = "", - thumbnail: str = "", - front_wide: str = "", - offer_image_wide: str = "", - product_logo: str = "", - ): - self.front_tall = front_tall - self.offer_image_tall = offer_image_tall - self.thumbnail = thumbnail - self.front_wide = front_wide - self.offer_image_wide = offer_image_wide - self.product_logo = product_logo - - @classmethod - def from_json(cls, json_data: list): - tmp = cls() - for item in json_data: - if item["type"] == "Thumbnail": - tmp.thumbnail = item["url"] - elif item["type"] == "DieselStoreFrontTall": - tmp.front_tall = item["url"] - elif item["type"] == "DieselStoreFrontWide": - tmp.front_wide = item["url"] - elif item["type"] == "OfferImageTall": - tmp.offer_image_tall = item["url"] - elif item["type"] == "OfferImageWide": - tmp.offer_image_wide = item["url"] - elif item["type"] == "ProductLogo": - tmp.product_logo = item["url"] - return tmp - - -class ShopGame: - # TODO: Copyrights etc - def __init__( - self, - title: str = "", - id: str = "", - image_urls: ImageUrlModel = None, - social_links: Dict = None, - langs: Dict = None, - reqs: Dict = None, - publisher: str = "", - developer: str = "", - original_price: str = "", - discount_price: str = "", - tags: List = None, - namespace: str = "", - offer_id: str = "", - ): - self.title = title - self.id = id - self.image_urls = image_urls - self.links = [] - if social_links: - for item in social_links: - if item.startswith("link"): - self.links.append( - tuple((item.replace("link", ""), social_links[item])) - ) - else: - self.links = [] - self.languages = langs if langs is not None else {} - self.reqs = reqs if reqs is not None else {} - self.publisher = publisher - self.developer = developer - self.price = original_price - self.discount_price = discount_price - self.tags = tags if tags is not None else [] - self.namespace = namespace - self.offer_id = offer_id - - @classmethod - def from_json(cls, api_data: dict, search_data: dict): - if isinstance(api_data, list): - for product in api_data: - if product["_title"] == "home": - api_data = product - break - if "pages" in api_data.keys(): - for page in api_data["pages"]: - if page["_slug"] == "home": - api_data = page - break - tmp = cls() - if search_data: - tmp.title = search_data.get("title", "Fail") - tmp.id = search_data.get("id") - tmp.image_urls = ImageUrlModel.from_json(search_data["keyImages"]) - if not tmp.developer: - for i in search_data["customAttributes"]: - if i["key"] == "developerName": - tmp.developer = i["value"] - tmp.price = search_data["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - tmp.discount_price = search_data["price"]["totalPrice"]["fmtPrice"][ - "discountPrice" - ] - tmp.namespace = search_data["namespace"] - tmp.offer_id = search_data["id"] - - if api_data: - links = api_data["data"]["socialLinks"] - tmp.links = [] - for item in links: - if item.startswith("link"): - tmp.links.append(tuple((item.replace("link", ""), links[item]))) - tmp.available_voice_langs = api_data["data"]["requirements"].get( - "languages", "Failed" - ) - tmp.reqs = {} - for i, system in enumerate(api_data["data"]["requirements"].get("systems", [])): - try: - tmp.reqs[system["systemType"]] = {} - except KeyError: - continue - for req in system["details"]: - try: - tmp.reqs[system["systemType"]][req["title"]] = ( - req["minimum"], - req["recommended"], - ) - except KeyError: - pass - tmp.publisher = api_data["data"]["meta"].get("publisher", "") - tmp.developer = api_data["data"]["meta"].get("developer", "") - tmp.tags = [ - i.replace("_", " ").capitalize() - for i in api_data["data"]["meta"].get("tags", []) - ] - - return tmp - - -@dataclass -class BrowseModel: - category: str = "games/edition/base|bundles/games|editors|software/edition/base" - count: int = 30 - language_code: str = "en" - country_code: str = "US" - keywords: str = "" - sortDir: str = "DESC" - start: int = 0 - tag: str = "" - withMapping: bool = True - withPrice: bool = True - date: str = ( - f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%H:%M:%S')}.420Z]" - ) - price: str = "" - onSale: bool = False - - def __post_init__(self): - self.locale = f"{self.language_code}-{self.country_code}" - - @property - def __dict__(self): - payload = { - "count": self.count, - "category": self.category, - "allowCountries": self.country_code, - "namespace": "", - "sortBy": "releaseDate", - "sortDir": self.sortDir, - "start": self.start, - "keywords": self.keywords, - "tag": self.tag, - "priceRange": self.price, - "releaseDate": self.date, - "withPrice": self.withPrice, - "locale": self.locale, - "country": self.country_code, - } - if self.price == "free": - payload["freeGame"] = True - payload.pop("priceRange") - elif self.price.startswith(""): - payload["priceRange"] = self.price.replace("", "") - if self.onSale: - payload["onSale"] = True - - if self.price: - payload["effectiveDate"] = self.date - else: - payload.pop("priceRange") - return payload diff --git a/rare/components/tabs/store/shop_widget.py b/rare/components/tabs/store/shop_widget.py index f8cf9a85..d22b51eb 100644 --- a/rare/components/tabs/store/shop_widget.py +++ b/rare/components/tabs/store/shop_widget.py @@ -11,17 +11,18 @@ from PyQt5.QtWidgets import ( QHBoxLayout, QWidget, QSizePolicy, QStackedLayout, ) - from legendary.core import LegendaryCore + from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.utils.extra_widgets import ButtonLineEdit from rare.widgets.flow_layout import FlowLayout from rare.widgets.side_tab import SideTabContents +from .api.models.query import SearchStoreQuery +from .api.models.response import CatalogOfferModel, WishlistItemModel from .constants import Constants from .game_widgets import GameWidget from .image_widget import WaitingSpinner from .shop_api_core import ShopApiCore -from .shop_models import BrowseModel logger = logging.getLogger("Shop") @@ -102,8 +103,8 @@ class ShopWidget(QWidget, SideTabContents): def update_wishlist(self): self.api_core.get_wishlist(self.add_wishlist_items) - def add_wishlist_items(self, wishlist): - for w in self.discounts_flow.findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly): + def add_wishlist_items(self, wishlist: List[WishlistItemModel]): + for w in self.discounts_flow.findChildren(QWidget, options=Qt.FindDirectChildrenOnly): self.discounts_flow.layout().removeWidget(w) w.deleteLater() @@ -124,8 +125,8 @@ class ShopWidget(QWidget, SideTabContents): if not game: continue try: - if game["offer"]["price"]["totalPrice"]["discount"] > 0: - w = GameWidget(self.api_core.cached_manager, game["offer"]) + if game.offer.price.total_price["discount"] > 0: + w = GameWidget(self.api_core.cached_manager, game.offer) w.show_info.connect(self.show_game) self.discounts_flow.layout().addWidget(w) discounts += 1 @@ -137,7 +138,7 @@ class ShopWidget(QWidget, SideTabContents): # FIXME: FlowLayout doesn't update on adding widget self.discounts_flow.layout().update() - def add_free_games(self, free_games: list): + def add_free_games(self, free_games: List[CatalogOfferModel]): for w in self.ui.free_container.layout().findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly): self.ui.free_container.layout().removeWidget(w) w.deleteLater() @@ -174,14 +175,14 @@ class ShopWidget(QWidget, SideTabContents): for game in free_games: try: if ( - game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] == "0" - and game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - != game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] + game.price.total_price["fmtPrice"]["discountPrice"] == "0" + and game.price.total_price["fmtPrice"]["originalPrice"] + != game.price.total_price["fmtPrice"]["discountPrice"] ): free_games_now.append(game) continue - if game["title"] == "Mystery Game": + if game.title == "Mystery Game": coming_free_games.append(game) continue except KeyError as e: @@ -191,7 +192,7 @@ class ShopWidget(QWidget, SideTabContents): # parse datetime to check if game is next week or now try: start_date = datetime.datetime.strptime( - game["promotions"]["upcomingPromotionalOffers"][0][ + game.promotions["upcomingPromotionalOffers"][0][ "promotionalOffers" ][0]["startDate"], "%Y-%m-%dT%H:%M:%S.%fZ", @@ -199,7 +200,7 @@ class ShopWidget(QWidget, SideTabContents): except Exception: try: start_date = datetime.datetime.strptime( - game["promotions"]["promotionalOffers"][0][ + game.promotions["promotionalOffers"][0][ "promotionalOffers" ][0]["startDate"], "%Y-%m-%dT%H:%M:%S.%fZ", @@ -230,7 +231,7 @@ class ShopWidget(QWidget, SideTabContents): # free games next week for free_game in coming_free_games: w = GameWidget(self.api_core.cached_manager, free_game) - if free_game["title"] != "Mystery Game": + if free_game.title != "Mystery Game": w.show_info.connect(self.show_game) self.free_games_next.layout().addWidget(w) # self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300)) @@ -350,12 +351,12 @@ class ShopWidget(QWidget, SideTabContents): self.games_layout.setCurrentWidget(self.games_spinner) - browse_model = BrowseModel( - language_code=self.core.language_code, - country_code=self.core.country_code, + browse_model = SearchStoreQuery( + language=self.core.language_code, + country=self.core.country_code, count=20, - price=self.price, - onSale=self.ui.on_discount.isChecked(), + price_range=self.price, + on_sale=self.ui.on_discount.isChecked(), ) browse_model.tag = "|".join(self.tags) @@ -364,14 +365,14 @@ class ShopWidget(QWidget, SideTabContents): self.api_core.browse_games(browse_model, self.show_games) def show_games(self, data): - for w in self.games_flow.layout().findChildren(GameWidget, options=Qt.FindDirectChildrenOnly): + for w in self.games_flow.findChildren(QWidget, options=Qt.FindDirectChildrenOnly): self.games_flow.layout().removeWidget(w) w.deleteLater() if data: for game in data: w = GameWidget(self.api_core.cached_manager, game) - w.show_info.connect(self.show_game.emit) + w.show_info.connect(self.show_game) self.games_flow.layout().addWidget(w) else: self.games_flow.layout().addWidget( diff --git a/rare/components/tabs/store/wishlist.py b/rare/components/tabs/store/wishlist.py index 0da63f44..b8fe7277 100644 --- a/rare/components/tabs/store/wishlist.py +++ b/rare/components/tabs/store/wishlist.py @@ -1,3 +1,5 @@ +from typing import List + from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtWidgets import QMessageBox, QWidget @@ -7,10 +9,11 @@ from rare.widgets.side_tab import SideTabContents from rare.widgets.flow_layout import FlowLayout from .shop_api_core import ShopApiCore from .game_widgets import WishlistWidget +from .api.models.response import WishlistItemModel, CatalogOfferModel class Wishlist(QWidget, SideTabContents): - show_game_info = pyqtSignal(dict) + show_game_info = pyqtSignal(CatalogOfferModel) update_wishlist_signal = pyqtSignal() def __init__(self, api_core: ShopApiCore, parent=None): @@ -38,10 +41,10 @@ class Wishlist(QWidget, SideTabContents): self.setEnabled(False) self.api_core.get_wishlist(self.set_wishlist) - def delete_from_wishlist(self, game): + def delete_from_wishlist(self, game: CatalogOfferModel): self.api_core.remove_from_wishlist( - game["namespace"], - game["id"], + game.namespace, + game.id, lambda success: self.update_wishlist() if success else QMessageBox.warning( @@ -73,27 +76,26 @@ class Wishlist(QWidget, SideTabContents): self.ui.list_container.layout().removeWidget(w) if sort == 0: - func = lambda x: x.game["title"] + func = lambda x: x.game.title reverse = self.ui.reverse.isChecked() elif sort == 1: - func = lambda x: x.game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] + func = lambda x: x.game.price.total_price["fmtPrice"]["discountPrice"] reverse = self.ui.reverse.isChecked() elif sort == 2: - func = lambda x: x.game["seller"]["name"] + func = lambda x: x.game.seller["name"] reverse = self.ui.reverse.isChecked() elif sort == 3: - func = lambda x: 1 - (x.game["price"]["totalPrice"]["discountPrice"] / x.game["price"]["totalPrice"]["originalPrice"]) + func = lambda x: 1 - (x.game.price.total_price["discountPrice"] / x.game.price.total_price["originalPrice"]) reverse = not self.ui.reverse.isChecked() else: - func = lambda x: x.game["title"] + func = lambda x: x.game.title reverse = self.ui.reverse.isChecked() widgets = sorted(widgets, key=func, reverse=reverse) for w in widgets: self.ui.list_container.layout().addWidget(w) - - def set_wishlist(self, wishlist=None, sort=0): + def set_wishlist(self, wishlist: List[WishlistItemModel] = None, sort=0): if wishlist and wishlist[0] == "error": return @@ -111,8 +113,8 @@ class Wishlist(QWidget, SideTabContents): self.ui.no_games_label.setVisible(False) for game in wishlist: - w = WishlistWidget(self.api_core.cached_manager, game["offer"], self.ui.list_container) - w.open_game.connect(self.show_game_info.emit) + w = WishlistWidget(self.api_core.cached_manager, game.offer, self.ui.list_container) + w.open_game.connect(self.show_game_info) w.delete_from_wishlist.connect(self.delete_from_wishlist) self.widgets.append(w) self.list_layout.addWidget(w) diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index f0c8b720..e827d9d5 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_ShopInfo(object): def setupUi(self, ShopInfo): ShopInfo.setObjectName("ShopInfo") - ShopInfo.resize(747, 442) + ShopInfo.resize(443, 347) ShopInfo.setWindowTitle("ShopGameInfo") self.main_layout = QtWidgets.QHBoxLayout(ShopInfo) self.main_layout.setObjectName("main_layout") @@ -146,19 +146,43 @@ class Ui_ShopInfo(object): self.button_layout.addWidget(self.wishlist_button) self.info_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.buttons_widget) self.right_layout.addLayout(self.info_layout) - self.requirements_group = QtWidgets.QFrame(ShopInfo) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + self.requirements_frame = QtWidgets.QFrame(ShopInfo) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.requirements_group.sizePolicy().hasHeightForWidth()) - self.requirements_group.setSizePolicy(sizePolicy) - self.requirements_group.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.requirements_group.setFrameShadow(QtWidgets.QFrame.Sunken) - self.requirements_group.setObjectName("requirements_group") - self.requirements_layout = QtWidgets.QHBoxLayout(self.requirements_group) + sizePolicy.setHeightForWidth(self.requirements_frame.sizePolicy().hasHeightForWidth()) + self.requirements_frame.setSizePolicy(sizePolicy) + self.requirements_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.requirements_frame.setFrameShadow(QtWidgets.QFrame.Sunken) + self.requirements_frame.setObjectName("requirements_frame") + self.requirements_layout = QtWidgets.QHBoxLayout(self.requirements_frame) self.requirements_layout.setContentsMargins(0, 0, 0, 0) self.requirements_layout.setObjectName("requirements_layout") - self.right_layout.addWidget(self.requirements_group) + self.right_layout.addWidget(self.requirements_frame) + self.description_group = QtWidgets.QGroupBox(ShopInfo) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.description_group.sizePolicy().hasHeightForWidth()) + self.description_group.setSizePolicy(sizePolicy) + self.description_group.setFlat(False) + self.description_group.setObjectName("description_group") + self.description_layout = QtWidgets.QVBoxLayout(self.description_group) + self.description_layout.setObjectName("description_layout") + self.description_label = QtWidgets.QLabel(self.description_group) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.description_label.sizePolicy().hasHeightForWidth()) + self.description_label.setSizePolicy(sizePolicy) + self.description_label.setText("error") + self.description_label.setTextFormat(QtCore.Qt.MarkdownText) + self.description_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.description_label.setWordWrap(True) + self.description_label.setOpenExternalLinks(True) + self.description_label.setObjectName("description_label") + self.description_layout.addWidget(self.description_label) + self.right_layout.addWidget(self.description_group) self.main_layout.addLayout(self.right_layout) self.main_layout.setStretch(1, 1) @@ -176,6 +200,7 @@ class Ui_ShopInfo(object): self.actions_label.setText(_translate("ShopInfo", "Actions")) self.open_store_button.setText(_translate("ShopInfo", "Buy in Epic Games Store")) self.wishlist_button.setText(_translate("ShopInfo", "Add to wishlist")) + self.description_group.setTitle(_translate("ShopInfo", "Description")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index 802e6fd2..9813c0cb 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -6,8 +6,8 @@ 0 0 - 747 - 442 + 443 + 347 @@ -309,9 +309,9 @@ - + - + 0 0 @@ -338,6 +338,49 @@ + + + + + 0 + 0 + + + + Description + + + false + + + + + + + 0 + 0 + + + + error + + + Qt::MarkdownText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + + + + diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py index ebd1003f..2a7d8bbd 100644 --- a/rare/utils/qt_requests.py +++ b/rare/utils/qt_requests.py @@ -66,7 +66,7 @@ class QtRequests(QObject): def __post(self, item: RequestQueueItem): request = self.__prepare_request(item) - payload = orjson.dumps(item.payload) # pylint: disable=maybe-no-member + payload = orjson.dumps(item.payload) reply = self.manager.post(request, payload) reply.errorOccurred.connect(self.__on_error) self.__active_requests[reply] = item diff --git a/requirements-dev.txt b/requirements-dev.txt index 96eb2180..d2647125 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,3 +7,4 @@ nuitka ordered-set PyQt5-stubs qstylizer + diff --git a/requirements-full.txt b/requirements-full.txt index 55b8e26f..a2754f33 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -13,3 +13,4 @@ pythonnet>=3.0.0rc4; platform_system == "Windows" cefpython3; platform_system == "Windows" pywebview[cef]; platform_system == "Windows" pypresence + diff --git a/requirements.txt b/requirements.txt index 4a9abb8f..a83fe85c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system orjson vdf; platform_system == "Linux" or platform_system == "FreeBSD" pywin32; platform_system == "Windows" +