1
0
Fork 0
mirror of synced 2024-05-23 14:00:05 +12:00

Store: Exploratory changes for GraphQL API

This commit is contained in:
loathingKernel 2023-04-10 14:03:45 +03:00
parent f6396f488a
commit 91af16b76d
No known key found for this signature in database
GPG key ID: CE0C72D0B53821FD
27 changed files with 2230 additions and 843 deletions

View file

@ -6,6 +6,7 @@ from rare.widgets.side_tab import SideTabWidget
from .game_info import ShopGameInfo from .game_info import ShopGameInfo
from .search_results import SearchResults from .search_results import SearchResults
from .shop_api_core import ShopApiCore from .shop_api_core import ShopApiCore
from .api.models.response import CatalogOfferModel
from .shop_widget import ShopWidget from .shop_widget import ShopWidget
from .wishlist import WishlistWidget, Wishlist from .wishlist import WishlistWidget, Wishlist
@ -69,7 +70,7 @@ class StoreTab(SideTabWidget):
def update_wishlist(self): def update_wishlist(self):
self.shop.update_wishlist() self.shop.update_wishlist()
def show_game(self, data): def show_game(self, data: CatalogOfferModel):
self.previous_index = self.currentIndex() self.previous_index = self.currentIndex()
self.info.update_game(data) self.info.update_game(data)
self.setCurrentIndex(self.info_index) self.setCurrentIndex(self.info_index)

View file

@ -35,4 +35,4 @@ if __name__ == "__main__":
window.setWindowTitle(f"{app.applicationName()} - Store") window.setWindowTitle(f"{app.applicationName()} - Store")
window.resize(QSize(1280, 800)) window.resize(QSize(1280, 800))
window.show() window.show()
app.exec() app.exec()

View file

@ -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
}
}
}
}
'''

View file

@ -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)

View file

@ -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
}
}
}
}

View file

@ -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]
}

View file

@ -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

View file

@ -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("<price>"):
payload["priceRange"] = self.price_range.replace("<price>", "")
if self.on_sale:
payload["onSale"] = True
if self.price_range:
payload["effectiveDate"] = self.effective_date
else:
payload.pop("priceRange")
return payload

View file

@ -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

View file

@ -0,0 +1,5 @@
from datetime import datetime, timezone
def parse_date(date: str):
return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc)

View file

@ -44,8 +44,187 @@ class Constants(QObject):
] ]
game_query = """ __Image = '''
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) { 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 { Catalog {
searchStore( searchStore(
allowCountries: $allowCountries allowCountries: $allowCountries
@ -67,452 +246,209 @@ query searchStoreQuery($allowCountries: String, $category: String, $count: Int,
effectiveDate: $effectiveDate effectiveDate: $effectiveDate
) { ) {
elements { elements {
title %s
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
}
}
}
}
} }
paging { paging {
count %s
total
} }
} }
} }
} }
""" ''' % (__CatalogOffer, __Pagination)
search_query = """ __WISHLIST_ITEM = '''
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) { id
Catalog { order
searchStore( created
allowCountries: $allowCountries offerId
category: $category updated
count: $count namespace
country: $country isFirstTime
keywords: $keywords offer(locale: $locale) {
locale: $locale %s
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
}
}
}
} }
""" ''' % __CatalogOffer
wishlist_query = """ WISHLIST_QUERY = '''
query wishlistQuery($country:String!, $locale:String) { query wishlistQuery(
$country: String!
$locale: String
$category: String
$withMapping: Boolean = false
$withPrice: Boolean = false
$withPromotions: Boolean = false
) {
Wishlist { Wishlist {
wishlistItems { wishlistItems {
elements { elements {
id %s
order }
created }
offerId }
updated }
namespace ''' % __WISHLIST_ITEM
offer(locale: $locale) {
productSlug WISHLIST_ADD_QUERY = '''
urlSlug mutation addWishlistMutation(
title $namespace: String!
id $offerId: String!
namespace $country: String!
offerType $locale: String
expiryDate $category: String
status $withMapping: Boolean = false
isCodeRedemptionOnly $withPrice: Boolean = false
description $withPromotions: Boolean = false
effectiveDate ) {
keyImages { Wishlist {
type addToWishlist(
url namespace: $namespace
} offerId: $offerId
seller { ) {
id wishlistItem {
name %s
} }
productSlug success
urlSlug }
items { }
id }
namespace ''' % __WISHLIST_ITEM
}
customAttributes { WISHLIST_REMOVE_QUERY = '''
key mutation removeFromWishlistMutation(
value $namespace: String!
} $offerId: String!
catalogNs { $operation: RemoveOperation!
mappings(pageType: "productHome") { ) {
pageSlug Wishlist {
pageType 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
} }
} socialLinks {
offerMappings { platform
pageSlug url
pageType }
} supportedAudio
categories { supportedText
path tags(locale: $locale) {
} id
price(country: $country) { name
totalPrice { groupName
discountPrice }
originalPrice technicalRequirements {
voucherDiscount macos {
discount minimum
fmtPrice(locale: $locale) { recommended
originalPrice title
discountPrice
intermediatePrice
} }
currencyCode windows {
currencyInfo { minimum
decimals recommended
symbol title
} }
} }
lineOffers { }
appliedRules { }
id ... on HomeConfiguration {
endDate 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__": def compress_query(query: str) -> str:
# from sgqlc import introspection, codegen return query.replace(" ", "").replace("\n", " ")
#
# coupon = codegen.operation.parse_graphql(coupon_query)
# codegen.schema. game_query = compress_query(SEARCH_STORE_QUERY)
# print(coupon.) 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)

View file

@ -1,4 +1,6 @@
import logging import logging
from pprint import pprint
from typing import List
from PyQt5.QtCore import Qt, QUrl from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QFont, QDesktopServices, QFontMetrics from PyQt5.QtGui import QFont, QDesktopServices, QFontMetrics
@ -10,13 +12,14 @@ from PyQt5.QtWidgets import (
QSizePolicy, 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 import LegendaryCoreSingleton
from rare.shared.image_manager import ImageSize from rare.shared.image_manager import ImageSize
from rare.ui.components.tabs.store.shop_game_info import Ui_ShopInfo from rare.ui.components.tabs.store.shop_game_info import Ui_ShopInfo
from rare.utils.misc import icon from rare.utils.misc import icon
from rare.widgets.side_tab import SideTabWidget, SideTabContents from rare.widgets.side_tab import SideTabWidget, SideTabContents
from rare.widgets.elide_label import ElideLabel from rare.widgets.elide_label import ElideLabel
from .api.debug import DebugDialog
from .image_widget import ShopImageWidget from .image_widget import ShopImageWidget
logger = logging.getLogger("ShopInfo") logger = logging.getLogger("ShopInfo")
@ -37,45 +40,46 @@ class ShopGameInfo(QWidget, SideTabContents):
self.image.setFixedSize(ImageSize.Normal) self.image.setFixedSize(ImageSize.Normal)
self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop) self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop)
self.game: ShopGame = None self.offer: CatalogOfferModel = None
self.data: dict = {} self.data: dict = {}
self.ui.wishlist_button.clicked.connect(self.add_to_wishlist) self.ui.wishlist_button.clicked.connect(self.add_to_wishlist)
self.ui.wishlist_button.setVisible(True)
self.in_wishlist = False self.in_wishlist = False
self.wishlist = [] 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.requirements_tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.ui.requirements_layout.addWidget(self.requirements_tabs) self.ui.requirements_layout.addWidget(self.requirements_tabs)
self.setDisabled(True) self.setDisabled(True)
def handle_wishlist_update(self, data): def handle_wishlist_update(self, wishlist: List[CatalogOfferModel]):
if data and data[0] == "error": if wishlist and wishlist[0] == "error":
return return
self.wishlist = [i["offer"]["title"] for i in data] self.wishlist = [game.id for game in wishlist]
if self.title_str in self.wishlist: if self.id_str in self.wishlist:
self.in_wishlist = True self.in_wishlist = True
self.ui.wishlist_button.setVisible(True)
self.ui.wishlist_button.setText(self.tr("Remove from Wishlist")) self.ui.wishlist_button.setText(self.tr("Remove from Wishlist"))
else: else:
self.in_wishlist = False self.in_wishlist = False
self.ui.wishlist_button.setVisible(False)
def update_game(self, data: dict): def update_game(self, offer: CatalogOfferModel):
self.set_title.emit(data["title"]) debug = DebugDialog(offer.__dict__, None)
self.ui.title.setText(data["title"]) debug.exec()
self.title_str = data["title"] self.set_title.emit(offer.title)
self.id_str = data["id"] self.ui.title.setText(offer.title)
self.title_str = offer.title
self.id_str = offer.id
self.api_core.get_wishlist(self.handle_wishlist_update) self.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(): while self.requirements_tabs.count():
self.requirements_tabs.widget(0).deleteLater() self.requirements_tabs.widget(0).deleteLater()
self.requirements_tabs.removeTab(0) self.requirements_tabs.removeTab(0)
self.requirements_tabs.clear() self.requirements_tabs.clear()
slug = data["productSlug"] slug = offer.product_slug
if not slug: if not slug:
for mapping in data["offerMappings"]: for mapping in offer.offer_mappings:
if mapping["pageType"] == "productHome": if mapping["pageType"] == "productHome":
slug = mapping["pageSlug"] slug = mapping["pageSlug"]
break break
@ -86,7 +90,7 @@ class ShopGameInfo(QWidget, SideTabContents):
slug = slug.replace("/home", "") slug = slug.replace("/home", "")
self.slug = slug 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.open_store_button.setText(self.tr("Show Game on Epic Page"))
self.ui.owned_label.setVisible(True) self.ui.owned_label.setVisible(True)
else: else:
@ -94,39 +98,44 @@ class ShopGameInfo(QWidget, SideTabContents):
self.ui.owned_label.setVisible(False) self.ui.owned_label.setVisible(False)
self.ui.price.setText(self.tr("Loading")) self.ui.price.setText(self.tr("Loading"))
self.ui.wishlist_button.setVisible(False)
# self.title.setText(self.tr("Loading")) # self.title.setText(self.tr("Loading"))
# self.image.setPixmap(QPixmap()) # self.image.setPixmap(QPixmap())
self.data = data
is_bundle = False is_bundle = False
for i in data["categories"]: for i in offer.categories:
if "bundles" in i.get("path", ""): if "bundles" in i.get("path", ""):
is_bundle = True is_bundle = True
# init API request # init API request
if slug: 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: # else:
# self.data_received({}) # self.data_received({})
self.offer = offer
def add_to_wishlist(self): def add_to_wishlist(self):
if not self.in_wishlist: if not self.in_wishlist:
return self.api_core.add_to_wishlist(
# self.api_core.add_to_wishlist(self.game.namespace, self.game.offer_id, self.offer.namespace,
# lambda success: self.wishlist_button.setText(self.tr("Remove from wishlist")) self.offer.id,
# if success else self.wishlist_button.setText("Something goes wrong")) lambda success: self.ui.wishlist_button.setText(self.tr("Remove from wishlist"))
if success
else self.ui.wishlist_button.setText("Something went wrong")
)
else: else:
self.api_core.remove_from_wishlist( self.api_core.remove_from_wishlist(
self.game.namespace, self.offer.namespace,
self.game.offer_id, self.offer.id,
lambda success: self.ui.wishlist_button.setVisible(False) lambda success: self.ui.wishlist_button.setText(self.tr("Add to wishlist"))
if success 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: 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: except Exception as e:
raise e raise e
logger.error(str(e)) logger.error(str(e))
@ -150,17 +159,19 @@ class ShopGameInfo(QWidget, SideTabContents):
# self.title.setText(self.game.title) # self.title.setText(self.game.title)
self.ui.price.setFont(QFont()) 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")) self.ui.price.setText(self.tr("Free"))
else: else:
self.ui.price.setText(self.game.price) self.ui.price.setText(price)
if self.game.price != self.game.discount_price: if price != discount_price:
font = QFont() font = QFont()
font.setStrikeOut(True) font.setStrikeOut(True)
self.ui.price.setFont(font) self.ui.price.setFont(font)
self.ui.discount_price.setText( self.ui.discount_price.setText(
self.game.discount_price discount_price
if self.game.discount_price != "0" if discount_price != "0"
else self.tr("Free") else self.tr("Free")
) )
self.ui.discount_price.setVisible(True) self.ui.discount_price.setVisible(True)
@ -171,8 +182,9 @@ class ShopGameInfo(QWidget, SideTabContents):
bold_font.setBold(True) bold_font.setBold(True)
fm = QFontMetrics(self.font()) fm = QFontMetrics(self.font())
if self.game.reqs: requirements = product_data.requirements
for system in self.game.reqs: if requirements and requirements.systems:
for system in requirements.systems:
req_widget = QWidget(self.requirements_tabs) req_widget = QWidget(self.requirements_tabs)
req_layout = QGridLayout(req_widget) req_layout = QGridLayout(req_widget)
req_widget.layout().setAlignment(Qt.AlignTop) req_widget.layout().setAlignment(Qt.AlignTop)
@ -185,53 +197,57 @@ class ShopGameInfo(QWidget, SideTabContents):
req_layout.addWidget(rec_label, 0, 2) req_layout.addWidget(rec_label, 0, 2)
req_layout.setColumnStretch(1, 2) req_layout.setColumnStretch(1, 2)
req_layout.setColumnStretch(2, 2) req_layout.setColumnStretch(2, 2)
for i, (key, value) in enumerate(self.game.reqs.get(system, {}).items()): for i, detail in enumerate(system.details):
req_layout.addWidget(QLabel(key, parent=req_widget), i + 1, 0) req_layout.addWidget(QLabel(detail.title, parent=req_widget), i + 1, 0)
min_label = ElideLabel(value[0], parent=req_widget) min_label = ElideLabel(detail.minimum, parent=req_widget)
req_layout.addWidget(min_label, i + 1, 1) 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) req_layout.addWidget(rec_label, i + 1, 2)
self.requirements_tabs.addTab(req_widget, system) self.requirements_tabs.addTab(req_widget, system.system_type)
# self.req_group_box.layout().addWidget(req_tabs) # self.req_group_box.layout().addWidget(req_tabs)
# self.req_group_box.layout().setAlignment(Qt.AlignTop) # self.req_group_box.layout().setAlignment(Qt.AlignTop)
# else: # else:
# self.req_group_box.layout().addWidget( # self.req_group_box.layout().addWidget(
# QLabel(self.tr("Could not get requirements")) # QLabel(self.tr("Could not get requirements"))
# ) # )
self.requirements_tabs.setEnabled(True) self.ui.requirements_frame.setVisible(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
else: else:
img_url = "" self.ui.requirements_frame.setVisible(False)
self.image.fetchPixmap(img_url)
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) # self.image_stack.setCurrentIndex(0)
try: about = product_data.about
if isinstance(self.game.developer, list): self.ui.description_label.setText(about.desciption)
self.ui.dev.setText(", ".join(self.game.developer)) self.ui.dev.setText(about.developer_attribution)
else: # try:
self.ui.dev.setText(self.game.developer) # if isinstance(aboudeveloper, list):
except KeyError: # self.ui.dev.setText(", ".join(self.game.developer))
pass # else:
self.ui.tags.setText(", ".join(self.game.tags)) # 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 # clear Layout
for b in self.ui.social_group.findChildren(SocialButton, options=Qt.FindDirectChildrenOnly): for b in self.ui.social_group.findChildren(SocialButton, options=Qt.FindDirectChildrenOnly):
self.ui.social_layout.removeWidget(b) self.ui.social_layout.removeWidget(b)
b.deleteLater() b.deleteLater()
links = product_data.social_links
link_count = 0 link_count = 0
for name, url in self.game.links: for name, url in links.items():
if name == "_type":
if name.lower() == "homepage": continue
name = name.replace("link", "").lower()
if name == "homepage":
icn = icon("mdi.web", "fa.search", scale_factor=1.5) icn = icon("mdi.web", "fa.search", scale_factor=1.5)
else: else:
try: 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: except Exception as e:
logger.error(str(e)) logger.error(str(e))
continue continue
@ -244,10 +260,10 @@ class ShopGameInfo(QWidget, SideTabContents):
self.setEnabled(True) self.setEnabled(True)
def add_wishlist_items(self, wishlist): # def add_wishlist_items(self, wishlist: List[CatalogGameModel]):
wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"] # wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"]
for game in wishlist: # for game in wishlist:
self.wishlist.append(game["offer"]["title"]) # self.wishlist.append(game["offer"]["title"])
def button_clicked(self): def button_clicked(self):
return return

View file

@ -3,42 +3,44 @@ import logging
from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QMouseEvent from PyQt5.QtGui import QMouseEvent
from PyQt5.QtWidgets import QPushButton 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.shared.image_manager import ImageSize
from rare.utils.misc import qta_icon from rare.utils.misc import qta_icon
from rare.utils.qt_requests import QtRequestManager from rare.utils.qt_requests import QtRequestManager
from .api.debug import DebugDialog
from .image_widget import ShopImageWidget from .image_widget import ShopImageWidget
logger = logging.getLogger("GameWidgets") logger = logging.getLogger("GameWidgets")
class GameWidget(ShopImageWidget): 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) super(GameWidget, self).__init__(manager, parent=parent)
self.setFixedSize(ImageSize.Wide) self.setFixedSize(ImageSize.Wide)
self.ui.setupUi(self) self.ui.setupUi(self)
self.json_info = json_info self.catalog_game = catalog_game
if json_info: if catalog_game:
self.init_ui(json_info) self.init_ui(catalog_game)
def init_ui(self, json_info): def init_ui(self, game: CatalogOfferModel):
if not json_info: if not game:
self.ui.title_label.setText(self.tr("An error occurred")) self.ui.title_label.setText(self.tr("An error occurred"))
return return
self.ui.title_label.setText(json_info.get("title")) self.ui.title_label.setText(game.title)
for attr in json_info["customAttributes"]: for attr in game.custom_attributes:
if attr["key"] == "developerName": if attr["key"] == "developerName":
developer = attr["value"] developer = attr["value"]
break break
else: else:
developer = json_info["seller"]["name"] developer = game.seller["name"]
self.ui.developer_label.setText(developer) self.ui.developer_label.setText(developer)
price = json_info["price"]["totalPrice"]["fmtPrice"]["originalPrice"] price = game.price.total_price["fmtPrice"]["originalPrice"]
discount_price = json_info["price"]["totalPrice"]["fmtPrice"]["discountPrice"] discount_price = game.price.total_price["fmtPrice"]["discountPrice"]
self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}')
if price != discount_price: if price != discount_price:
font = self.ui.price_label.font() font = self.ui.price_label.font()
@ -48,43 +50,48 @@ class GameWidget(ShopImageWidget):
else: else:
self.ui.discount_label.setVisible(False) self.ui.discount_label.setVisible(False)
for c in r'<>?":|\/*': key_images = game.key_images
json_info["title"] = json_info["title"].replace(c, "") self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url)
for img in json_info["keyImages"]: # for img in json_info["keyImages"]:
if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo",]: # if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"]:
if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game": # if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game":
continue # continue
self.fetchPixmap(img["url"]) # self.fetchPixmap(img["url"])
break # break
else: # else:
logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) # logger.info(", ".join([img["type"] for img in json_info["keyImages"]]))
def mousePressEvent(self, a0: QMouseEvent) -> None: def mousePressEvent(self, a0: QMouseEvent) -> None:
if a0.button() == Qt.LeftButton: if a0.button() == Qt.LeftButton:
a0.accept() 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): class WishlistWidget(ShopImageWidget):
open_game = pyqtSignal(dict) open_game = pyqtSignal(CatalogOfferModel)
delete_from_wishlist = pyqtSignal(dict) 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) super(WishlistWidget, self).__init__(manager, parent=parent)
self.setFixedSize(ImageSize.Wide) self.setFixedSize(ImageSize.Wide)
self.ui.setupUi(self) self.ui.setupUi(self)
self.game = game self.game = catalog_game
for attr in game["customAttributes"]: for attr in catalog_game.custom_attributes:
if attr["key"] == "developerName": if attr["key"] == "developerName":
developer = attr["value"] developer = attr["value"]
break break
else: else:
developer = game["seller"]["name"] developer = catalog_game.seller["name"]
original_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] original_price = catalog_game.price.total_price["fmtPrice"]["originalPrice"]
discount_price = game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] 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.developer_label.setText(developer)
self.ui.price_label.setText(f'{original_price if original_price != "0" else self.tr("Free")}') self.ui.price_label.setText(f'{original_price if original_price != "0" else self.tr("Free")}')
if original_price != discount_price: 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")}') self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}')
else: else:
self.ui.discount_label.setVisible(False) self.ui.discount_label.setVisible(False)
image_model = ImageUrlModel.from_json(game["keyImages"]) key_images = catalog_game.key_images
url = image_model.front_wide self.fetchPixmap(
if not url: key_images.for_dimensions(self.width(), self.height()).url
url = image_model.offer_image_wide )
self.fetchPixmap(url)
self.delete_button = QPushButton(self) self.delete_button = QPushButton(self)
self.delete_button.setIcon(icon("mdi.delete", color="white")) self.delete_button.setIcon(icon("mdi.delete", color="white"))
@ -113,5 +119,7 @@ class WishlistWidget(ShopImageWidget):
a0.accept() a0.accept()
self.open_game.emit(self.game) self.open_game.emit(self.game)
# right # right
elif a0.button() == Qt.RightButton: if a0.button() == Qt.RightButton:
pass # self.showMenu(e) a0.accept()
dialog = DebugDialog(self.game.__dict__, self)
dialog.show()

View file

@ -13,9 +13,12 @@ from rare.widgets.flow_layout import FlowLayout
from rare.widgets.side_tab import SideTabContents from rare.widgets.side_tab import SideTabContents
from .image_widget import ShopImageWidget from .image_widget import ShopImageWidget
from .api.debug import DebugDialog
from .api.models.response import CatalogOfferModel
class SearchResults(QScrollArea, SideTabContents): class SearchResults(QScrollArea, SideTabContents):
show_info = pyqtSignal(dict) show_info = pyqtSignal(CatalogOfferModel)
def __init__(self, api_core, parent=None): def __init__(self, api_core, parent=None):
super(SearchResults, self).__init__(parent=parent) super(SearchResults, self).__init__(parent=parent)
@ -59,22 +62,20 @@ class SearchResults(QScrollArea, SideTabContents):
class SearchResultItem(ShopImageWidget): 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) super(SearchResultItem, self).__init__(manager, parent=parent)
self.setFixedSize(ImageSize.Normal) self.setFixedSize(ImageSize.Normal)
self.ui.setupUi(self) 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"]) key_images = catalog_game.key_images
price = result["price"]["totalPrice"]["fmtPrice"]["originalPrice"] self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url)
discount_price = result["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
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")}') self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}')
if price != discount_price: if price != discount_price:
font = self.ui.price_label.font() font = self.ui.price_label.font()
@ -84,9 +85,14 @@ class SearchResultItem(ShopImageWidget):
else: else:
self.ui.discount_label.setVisible(False) self.ui.discount_label.setVisible(False)
self.res = result self.catalog_game = catalog_game
def mousePressEvent(self, a0: QMouseEvent) -> None: def mousePressEvent(self, a0: QMouseEvent) -> None:
if a0.button() == Qt.LeftButton: if a0.button() == Qt.LeftButton:
a0.accept() 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()

View file

@ -1,21 +1,33 @@
from logging import getLogger from logging import getLogger
from typing import List, Callable
from PyQt5.QtCore import pyqtSignal, QObject 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 ( from rare.components.tabs.store.constants import (
wishlist_query, wishlist_query,
search_query, search_query,
add_to_wishlist_query, wishlist_add_query,
remove_from_wishlist_query, wishlist_remove_query,
) )
from rare.components.tabs.store.shop_models import BrowseModel from rare.components.tabs.store.shop_models import BrowseModel
from rare.utils.paths import cache_dir from rare.utils.paths import cache_dir
from rare.utils.qt_requests import QtRequests from rare.utils.qt_requests import QtRequests
from .api.models.query import SearchStoreQuery
from .api.models.response import (
DieselProduct,
ResponseModel,
CatalogOfferModel,
)
logger = getLogger("ShopAPICore") logger = getLogger("ShopAPICore")
graphql_url = "https://graphql.epicgames.com/graphql" graphql_url = "https://graphql.epicgames.com/graphql"
DEBUG: Callable[[], bool] = lambda: "--debug" in QApplication.arguments()
class ShopApiCore(QObject): class ShopApiCore(QObject):
update_wishlist = pyqtSignal() update_wishlist = pyqtSignal()
@ -25,6 +37,7 @@ class ShopApiCore(QObject):
self.language_code: str = language self.language_code: str = language
self.country_code: str = country self.country_code: str = country
self.locale = f"{self.language_code}-{self.country_code}" self.locale = f"{self.language_code}-{self.country_code}"
self.locale = "en-US"
self.manager = QtRequests(parent=self) self.manager = QtRequests(parent=self)
self.authed_manager = QtRequests(token=token, parent=self) self.authed_manager = QtRequests(token=token, parent=self)
self.cached_manager = QtRequests(cache=str(cache_dir().joinpath("store")), 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, "country": self.country_code,
"allowCountries": 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: try:
results: dict = data["data"]["Catalog"]["searchStore"]["elements"] response = ResponseModel.from_dict(data)
except KeyError: 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") logger.error("Free games Api request failed")
handle_func(["error", "Key error"]) handle_func(["error", "Key error"])
return return
except Exception as e: except Exception as e:
if DEBUG():
raise e
logger.error(f"Free games Api request failed: {e}") logger.error(f"Free games Api request failed: {e}")
handle_func(["error", e]) handle_func(["error", e])
return return
handle_func(results)
def get_wishlist(self, handle_func): def get_wishlist(self, handle_func):
self.authed_manager.post( self.authed_manager.post(
graphql_url, graphql_url,
lambda data: self._handle_wishlist(data, handle_func), lambda data: self.__handle_wishlist(data, handle_func),
{ {
"query": wishlist_query, "query": wishlist_query,
"variables": { "variables": {
"country": self.country_code, "country": self.country_code,
"locale": self.locale, "locale": self.locale,
"withPrice": True,
}, },
}, },
) )
def _handle_wishlist(self, data, handle_func): @staticmethod
def __handle_wishlist(data, handle_func):
try: try:
results: list = data["data"]["Wishlist"]["wishlistItems"]["elements"] response = ResponseModel.from_dict(data)
except KeyError: 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") logger.error("Free games Api request failed")
handle_func(["error", "Key error"]) handle_func(["error", "Key error"])
return return
except Exception as e: except Exception as e:
if DEBUG():
raise e
logger.error(f"Free games Api request failed: {e}") logger.error(f"Free games Api request failed: {e}")
handle_func(["error", e]) handle_func(["error", e])
return return
handle_func(results) def search_game(self, name, handler):
def search_game(self, name, handle_func):
payload = { payload = {
"query": search_query, "query": search_query,
"variables": { "variables": {
"category": "games/edition/base|bundles/games|editors|software/edition/base", "category": "games/edition/base|bundles/games|editors|software/edition/base",
"count": 10, "count": 20,
"country": self.country_code, "country": self.country_code,
"keywords": name, "keywords": name,
"locale": self.locale, "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: try:
handle_func(data["data"]["Catalog"]["searchStore"]["elements"]) response = ResponseModel.from_dict(data)
handler(response.data.catalog.search_store.elements)
except KeyError as e: except KeyError as e:
logger.error(str(e)) logger.error(str(e))
handle_func([]) if DEBUG():
raise e
handler([])
except Exception as e: except Exception as e:
logger.error(f"Search Api request failed: {e}") logger.error(f"Search Api request failed: {e}")
handle_func([]) if DEBUG():
raise e
handler([])
return return
def browse_games(self, browse_model: BrowseModel, handle_func): def browse_games(self, browse_model: SearchStoreQuery, handle_func):
if self.browse_active: if self.browse_active:
self.next_browse_request = (browse_model, handle_func) self.next_browse_request = (browse_model, handle_func)
return return
self.browse_active = True self.browse_active = True
payload = { payload = {
"query": search_query, "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 self.browse_active = False
if data is None: if data is None:
data = {} data = {}
if not self.next_browse_request: if not self.next_browse_request:
try: 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: except KeyError as e:
if DEBUG():
raise e
logger.error(str(e)) logger.error(str(e))
handle_func([]) handle_func([])
except Exception as e: except Exception as e:
if DEBUG():
raise e
logger.error(f"Browse games Api request failed: {e}") logger.error(f"Browse games Api request failed: {e}")
handle_func([]) handle_func([])
return return
@ -143,62 +183,72 @@ class ShopApiCore(QObject):
self.next_browse_request = tuple(()) self.next_browse_request = tuple(())
def get_game(self, slug: str, is_bundle: bool, handle_func): 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}" url = "https://store-content.ak.epicgames.com/api"
self.manager.get(url, lambda data: self._handle_get_game(data, handle_func)) 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: try:
handle_func(data) product = DieselProduct.from_dict(data)
handle_func(product)
except Exception as e: except Exception as e:
raise e if DEBUG():
raise e
logger.error(str(e)) logger.error(str(e))
# handle_func({}) # handle_func({})
# needs a captcha # needs a captcha
def add_to_wishlist(self, namespace, offer_id, handle_func: callable): def add_to_wishlist(self, namespace, offer_id, handle_func: callable):
payload = { payload = {
"query": wishlist_add_query,
"variables": { "variables": {
"offerId": offer_id, "offerId": offer_id,
"namespace": namespace, "namespace": namespace,
"country": self.country_code, "country": self.country_code,
"locale": self.locale, "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) 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): def _handle_add_to_wishlist(self, data, handle_func):
debug = DebugDialog(data, None)
debug.exec()
try: try:
data = data["data"]["Wishlist"]["addToWishlist"] response = ResponseModel.from_dict(data)
if data["success"]: data = response.data.wishlist.add_to_wishlist
handle_func(True) handle_func(data.success)
else:
handle_func(False)
except Exception as e: except Exception as e:
if DEBUG():
raise e
logger.error(str(e)) logger.error(str(e))
handle_func(False) handle_func(False)
self.update_wishlist.emit() self.update_wishlist.emit()
def remove_from_wishlist(self, namespace, offer_id, handle_func: callable): def remove_from_wishlist(self, namespace, offer_id, handle_func: callable):
payload = { payload = {
"query": wishlist_remove_query,
"variables": { "variables": {
"offerId": offer_id, "offerId": offer_id,
"namespace": namespace, "namespace": namespace,
"operation": "REMOVE", "operation": "REMOVE",
}, },
"query": remove_from_wishlist_query,
} }
self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, handle_func), self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, handle_func),
payload) payload)
def _handle_remove_from_wishlist(self, data, handle_func): def _handle_remove_from_wishlist(self, data, handle_func):
debug = DebugDialog(data, None)
debug.exec()
try: try:
data = data["data"]["Wishlist"]["removeFromWishlist"] response = ResponseModel.from_dict(data)
if data["success"]: data = response.data.wishlist.remove_from_wishlist
handle_func(True) handle_func(data.success)
else:
handle_func(False)
except Exception as e: except Exception as e:
if DEBUG():
raise e
logger.error(str(e)) logger.error(str(e))
handle_func(False) handle_func(False)
self.update_wishlist.emit() self.update_wishlist.emit()

View file

@ -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("<price>"):
payload["priceRange"] = self.price.replace("<price>", "")
if self.onSale:
payload["onSale"] = True
if self.price:
payload["effectiveDate"] = self.date
else:
payload.pop("priceRange")
return payload

View file

@ -11,17 +11,18 @@ from PyQt5.QtWidgets import (
QHBoxLayout, QHBoxLayout,
QWidget, QSizePolicy, QStackedLayout, QWidget, QSizePolicy, QStackedLayout,
) )
from legendary.core import LegendaryCore from legendary.core import LegendaryCore
from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.ui.components.tabs.store.store import Ui_ShopWidget
from rare.utils.extra_widgets import ButtonLineEdit from rare.utils.extra_widgets import ButtonLineEdit
from rare.widgets.flow_layout import FlowLayout from rare.widgets.flow_layout import FlowLayout
from rare.widgets.side_tab import SideTabContents 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 .constants import Constants
from .game_widgets import GameWidget from .game_widgets import GameWidget
from .image_widget import WaitingSpinner from .image_widget import WaitingSpinner
from .shop_api_core import ShopApiCore from .shop_api_core import ShopApiCore
from .shop_models import BrowseModel
logger = logging.getLogger("Shop") logger = logging.getLogger("Shop")
@ -102,8 +103,8 @@ class ShopWidget(QWidget, SideTabContents):
def update_wishlist(self): def update_wishlist(self):
self.api_core.get_wishlist(self.add_wishlist_items) self.api_core.get_wishlist(self.add_wishlist_items)
def add_wishlist_items(self, wishlist): def add_wishlist_items(self, wishlist: List[WishlistItemModel]):
for w in self.discounts_flow.findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly): for w in self.discounts_flow.findChildren(QWidget, options=Qt.FindDirectChildrenOnly):
self.discounts_flow.layout().removeWidget(w) self.discounts_flow.layout().removeWidget(w)
w.deleteLater() w.deleteLater()
@ -124,8 +125,8 @@ class ShopWidget(QWidget, SideTabContents):
if not game: if not game:
continue continue
try: try:
if game["offer"]["price"]["totalPrice"]["discount"] > 0: if game.offer.price.total_price["discount"] > 0:
w = GameWidget(self.api_core.cached_manager, game["offer"]) w = GameWidget(self.api_core.cached_manager, game.offer)
w.show_info.connect(self.show_game) w.show_info.connect(self.show_game)
self.discounts_flow.layout().addWidget(w) self.discounts_flow.layout().addWidget(w)
discounts += 1 discounts += 1
@ -137,7 +138,7 @@ class ShopWidget(QWidget, SideTabContents):
# FIXME: FlowLayout doesn't update on adding widget # FIXME: FlowLayout doesn't update on adding widget
self.discounts_flow.layout().update() 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): for w in self.ui.free_container.layout().findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly):
self.ui.free_container.layout().removeWidget(w) self.ui.free_container.layout().removeWidget(w)
w.deleteLater() w.deleteLater()
@ -174,14 +175,14 @@ class ShopWidget(QWidget, SideTabContents):
for game in free_games: for game in free_games:
try: try:
if ( if (
game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] == "0" game.price.total_price["fmtPrice"]["discountPrice"] == "0"
and game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] and game.price.total_price["fmtPrice"]["originalPrice"]
!= game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] != game.price.total_price["fmtPrice"]["discountPrice"]
): ):
free_games_now.append(game) free_games_now.append(game)
continue continue
if game["title"] == "Mystery Game": if game.title == "Mystery Game":
coming_free_games.append(game) coming_free_games.append(game)
continue continue
except KeyError as e: except KeyError as e:
@ -191,7 +192,7 @@ class ShopWidget(QWidget, SideTabContents):
# parse datetime to check if game is next week or now # parse datetime to check if game is next week or now
try: try:
start_date = datetime.datetime.strptime( start_date = datetime.datetime.strptime(
game["promotions"]["upcomingPromotionalOffers"][0][ game.promotions["upcomingPromotionalOffers"][0][
"promotionalOffers" "promotionalOffers"
][0]["startDate"], ][0]["startDate"],
"%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%S.%fZ",
@ -199,7 +200,7 @@ class ShopWidget(QWidget, SideTabContents):
except Exception: except Exception:
try: try:
start_date = datetime.datetime.strptime( start_date = datetime.datetime.strptime(
game["promotions"]["promotionalOffers"][0][ game.promotions["promotionalOffers"][0][
"promotionalOffers" "promotionalOffers"
][0]["startDate"], ][0]["startDate"],
"%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%S.%fZ",
@ -230,7 +231,7 @@ class ShopWidget(QWidget, SideTabContents):
# free games next week # free games next week
for free_game in coming_free_games: for free_game in coming_free_games:
w = GameWidget(self.api_core.cached_manager, free_game) 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) w.show_info.connect(self.show_game)
self.free_games_next.layout().addWidget(w) self.free_games_next.layout().addWidget(w)
# self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300)) # 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) self.games_layout.setCurrentWidget(self.games_spinner)
browse_model = BrowseModel( browse_model = SearchStoreQuery(
language_code=self.core.language_code, language=self.core.language_code,
country_code=self.core.country_code, country=self.core.country_code,
count=20, count=20,
price=self.price, price_range=self.price,
onSale=self.ui.on_discount.isChecked(), on_sale=self.ui.on_discount.isChecked(),
) )
browse_model.tag = "|".join(self.tags) browse_model.tag = "|".join(self.tags)
@ -364,14 +365,14 @@ class ShopWidget(QWidget, SideTabContents):
self.api_core.browse_games(browse_model, self.show_games) self.api_core.browse_games(browse_model, self.show_games)
def show_games(self, data): 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) self.games_flow.layout().removeWidget(w)
w.deleteLater() w.deleteLater()
if data: if data:
for game in data: for game in data:
w = GameWidget(self.api_core.cached_manager, game) 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) self.games_flow.layout().addWidget(w)
else: else:
self.games_flow.layout().addWidget( self.games_flow.layout().addWidget(

View file

@ -1,3 +1,5 @@
from typing import List
from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtWidgets import QMessageBox, QWidget 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 rare.widgets.flow_layout import FlowLayout
from .shop_api_core import ShopApiCore from .shop_api_core import ShopApiCore
from .game_widgets import WishlistWidget from .game_widgets import WishlistWidget
from .api.models.response import WishlistItemModel, CatalogOfferModel
class Wishlist(QWidget, SideTabContents): class Wishlist(QWidget, SideTabContents):
show_game_info = pyqtSignal(dict) show_game_info = pyqtSignal(CatalogOfferModel)
update_wishlist_signal = pyqtSignal() update_wishlist_signal = pyqtSignal()
def __init__(self, api_core: ShopApiCore, parent=None): def __init__(self, api_core: ShopApiCore, parent=None):
@ -38,10 +41,10 @@ class Wishlist(QWidget, SideTabContents):
self.setEnabled(False) self.setEnabled(False)
self.api_core.get_wishlist(self.set_wishlist) 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( self.api_core.remove_from_wishlist(
game["namespace"], game.namespace,
game["id"], game.id,
lambda success: self.update_wishlist() lambda success: self.update_wishlist()
if success if success
else QMessageBox.warning( else QMessageBox.warning(
@ -73,27 +76,26 @@ class Wishlist(QWidget, SideTabContents):
self.ui.list_container.layout().removeWidget(w) self.ui.list_container.layout().removeWidget(w)
if sort == 0: if sort == 0:
func = lambda x: x.game["title"] func = lambda x: x.game.title
reverse = self.ui.reverse.isChecked() reverse = self.ui.reverse.isChecked()
elif sort == 1: 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() reverse = self.ui.reverse.isChecked()
elif sort == 2: elif sort == 2:
func = lambda x: x.game["seller"]["name"] func = lambda x: x.game.seller["name"]
reverse = self.ui.reverse.isChecked() reverse = self.ui.reverse.isChecked()
elif sort == 3: 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() reverse = not self.ui.reverse.isChecked()
else: else:
func = lambda x: x.game["title"] func = lambda x: x.game.title
reverse = self.ui.reverse.isChecked() reverse = self.ui.reverse.isChecked()
widgets = sorted(widgets, key=func, reverse=reverse) widgets = sorted(widgets, key=func, reverse=reverse)
for w in widgets: for w in widgets:
self.ui.list_container.layout().addWidget(w) self.ui.list_container.layout().addWidget(w)
def set_wishlist(self, wishlist: List[WishlistItemModel] = None, sort=0):
def set_wishlist(self, wishlist=None, sort=0):
if wishlist and wishlist[0] == "error": if wishlist and wishlist[0] == "error":
return return
@ -111,8 +113,8 @@ class Wishlist(QWidget, SideTabContents):
self.ui.no_games_label.setVisible(False) self.ui.no_games_label.setVisible(False)
for game in wishlist: for game in wishlist:
w = WishlistWidget(self.api_core.cached_manager, game["offer"], self.ui.list_container) w = WishlistWidget(self.api_core.cached_manager, game.offer, self.ui.list_container)
w.open_game.connect(self.show_game_info.emit) w.open_game.connect(self.show_game_info)
w.delete_from_wishlist.connect(self.delete_from_wishlist) w.delete_from_wishlist.connect(self.delete_from_wishlist)
self.widgets.append(w) self.widgets.append(w)
self.list_layout.addWidget(w) self.list_layout.addWidget(w)

View file

@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ShopInfo(object): class Ui_ShopInfo(object):
def setupUi(self, ShopInfo): def setupUi(self, ShopInfo):
ShopInfo.setObjectName("ShopInfo") ShopInfo.setObjectName("ShopInfo")
ShopInfo.resize(747, 442) ShopInfo.resize(443, 347)
ShopInfo.setWindowTitle("ShopGameInfo") ShopInfo.setWindowTitle("ShopGameInfo")
self.main_layout = QtWidgets.QHBoxLayout(ShopInfo) self.main_layout = QtWidgets.QHBoxLayout(ShopInfo)
self.main_layout.setObjectName("main_layout") self.main_layout.setObjectName("main_layout")
@ -146,19 +146,43 @@ class Ui_ShopInfo(object):
self.button_layout.addWidget(self.wishlist_button) self.button_layout.addWidget(self.wishlist_button)
self.info_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.buttons_widget) self.info_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.buttons_widget)
self.right_layout.addLayout(self.info_layout) self.right_layout.addLayout(self.info_layout)
self.requirements_group = QtWidgets.QFrame(ShopInfo) self.requirements_frame = QtWidgets.QFrame(ShopInfo)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.requirements_group.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.requirements_frame.sizePolicy().hasHeightForWidth())
self.requirements_group.setSizePolicy(sizePolicy) self.requirements_frame.setSizePolicy(sizePolicy)
self.requirements_group.setFrameShape(QtWidgets.QFrame.StyledPanel) self.requirements_frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.requirements_group.setFrameShadow(QtWidgets.QFrame.Sunken) self.requirements_frame.setFrameShadow(QtWidgets.QFrame.Sunken)
self.requirements_group.setObjectName("requirements_group") self.requirements_frame.setObjectName("requirements_frame")
self.requirements_layout = QtWidgets.QHBoxLayout(self.requirements_group) self.requirements_layout = QtWidgets.QHBoxLayout(self.requirements_frame)
self.requirements_layout.setContentsMargins(0, 0, 0, 0) self.requirements_layout.setContentsMargins(0, 0, 0, 0)
self.requirements_layout.setObjectName("requirements_layout") 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.addLayout(self.right_layout)
self.main_layout.setStretch(1, 1) self.main_layout.setStretch(1, 1)
@ -176,6 +200,7 @@ class Ui_ShopInfo(object):
self.actions_label.setText(_translate("ShopInfo", "Actions")) self.actions_label.setText(_translate("ShopInfo", "Actions"))
self.open_store_button.setText(_translate("ShopInfo", "Buy in Epic Games Store")) self.open_store_button.setText(_translate("ShopInfo", "Buy in Epic Games Store"))
self.wishlist_button.setText(_translate("ShopInfo", "Add to wishlist")) self.wishlist_button.setText(_translate("ShopInfo", "Add to wishlist"))
self.description_group.setTitle(_translate("ShopInfo", "Description"))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>747</width> <width>443</width>
<height>442</height> <height>347</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -309,9 +309,9 @@
</layout> </layout>
</item> </item>
<item> <item>
<widget class="QFrame" name="requirements_group"> <widget class="QFrame" name="requirements_frame">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
@ -338,6 +338,49 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="description_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Description</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="description_layout">
<item>
<widget class="QLabel" name="description_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string notr="true">error</string>
</property>
<property name="textFormat">
<enum>Qt::MarkdownText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>

View file

@ -66,7 +66,7 @@ class QtRequests(QObject):
def __post(self, item: RequestQueueItem): def __post(self, item: RequestQueueItem):
request = self.__prepare_request(item) 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 = self.manager.post(request, payload)
reply.errorOccurred.connect(self.__on_error) reply.errorOccurred.connect(self.__on_error)
self.__active_requests[reply] = item self.__active_requests[reply] = item

View file

@ -7,3 +7,4 @@ nuitka
ordered-set ordered-set
PyQt5-stubs PyQt5-stubs
qstylizer qstylizer

View file

@ -13,3 +13,4 @@ pythonnet>=3.0.0rc4; platform_system == "Windows"
cefpython3; platform_system == "Windows" cefpython3; platform_system == "Windows"
pywebview[cef]; platform_system == "Windows" pywebview[cef]; platform_system == "Windows"
pypresence pypresence

View file

@ -7,3 +7,4 @@ legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system
orjson orjson
vdf; platform_system == "Linux" or platform_system == "FreeBSD" vdf; platform_system == "Linux" or platform_system == "FreeBSD"
pywin32; platform_system == "Windows" pywin32; platform_system == "Windows"