diff --git a/rare/components/tabs/__init__.py b/rare/components/tabs/__init__.py index 688d0ea7..a8025a02 100644 --- a/rare/components/tabs/__init__.py +++ b/rare/components/tabs/__init__.py @@ -8,7 +8,7 @@ from .downloads import DownloadsTab from .games import GamesTab from .settings import SettingsTab from .settings.debug import DebugSettings -from .store import Shop +from .store import StoreTab from .tab_widgets import MainTabBar, TabButtonWidget @@ -39,7 +39,7 @@ class MainTabWidget(QTabWidget): self.setTabEnabled(self.downloads_index, not self.args.offline) if not self.args.offline: - self.store_tab = Shop(self.core) + self.store_tab = StoreTab(self.core, parent=self) self.store_index = self.addTab(self.store_tab, self.tr("Store (Beta)")) self.setTabEnabled(self.store_index, not self.args.offline) diff --git a/rare/components/tabs/store/__init__.py b/rare/components/tabs/store/__init__.py index 1c9493fe..c011ae45 100644 --- a/rare/components/tabs/store/__init__.py +++ b/rare/components/tabs/store/__init__.py @@ -1,61 +1,42 @@ from PyQt5.QtGui import QShowEvent, QHideEvent -from PyQt5.QtWidgets import QStackedWidget, QTabWidget from legendary.core import LegendaryCore -from rare.shared.rare_core import RareCore -from rare.utils.paths import cache_dir -from .game_info import ShopGameInfo -from .search_results import SearchResults -from .shop_api_core import ShopApiCore -from .shop_widget import ShopWidget -from .wishlist import WishlistWidget, Wishlist +from rare.widgets.side_tab import SideTabWidget +from .api.models.response import CatalogOfferModel +from .landing import LandingWidget, LandingPage +from .search import SearchPage +from .store_api import StoreAPI +from .widgets.details import DetailsWidget +from .wishlist import WishlistPage -class Shop(QStackedWidget): - init = False +class StoreTab(SideTabWidget): + + def __init__(self, core: LegendaryCore, parent=None): + super(StoreTab, self).__init__(parent=parent) + self.init = False - def __init__(self, core: LegendaryCore): - super(Shop, self).__init__() self.core = core - self.rcore = RareCore.instance() - self.api_core = ShopApiCore( + # self.rcore = RareCore.instance() + self.api = StoreAPI( self.core.egs.session.headers["Authorization"], self.core.language_code, self.core.country_code, + [] # [i.asset_infos["Windows"].namespace for i in self.rcore.game_list if bool(i.asset_infos)] ) - self.shop = ShopWidget(cache_dir(), self.core, self.api_core) - self.wishlist_widget = Wishlist(self.api_core) + self.landing = LandingPage(self.api, parent=self) + self.landing_index = self.addTab(self.landing, self.tr("Store")) - self.store_tabs = QTabWidget(parent=self) - self.store_tabs.addTab(self.shop, self.tr("Games")) - self.store_tabs.addTab(self.wishlist_widget, self.tr("Wishlist")) + self.search = SearchPage(self.api, parent=self) + self.search_index = self.addTab(self.search, self.tr("Search")) - self.addWidget(self.store_tabs) - - self.search_results = SearchResults(self.api_core) - self.addWidget(self.search_results) - self.search_results.show_info.connect(self.show_game_info) - self.info = ShopGameInfo( - [i.asset_infos["Windows"].namespace for i in self.rcore.game_list if bool(i.asset_infos)], - self.api_core, - ) - self.addWidget(self.info) - self.info.back_button.clicked.connect(lambda: self.setCurrentIndex(0)) - - self.search_results.back_button.clicked.connect(lambda: self.setCurrentIndex(0)) - self.shop.show_info.connect(self.show_search_results) - - self.wishlist_widget.show_game_info.connect(self.show_game_info) - self.shop.show_game.connect(self.show_game_info) - self.api_core.update_wishlist.connect(self.update_wishlist) - self.wishlist_widget.update_wishlist_signal.connect(self.update_wishlist) + self.wishlist = WishlistPage(self.api, parent=self) + self.wishlist_index = self.addTab(self.wishlist, self.tr("Wishlist")) def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous() or self.init: return super().showEvent(a0) - self.shop.load() - self.wishlist_widget.update_wishlist() self.init = True return super().showEvent(a0) @@ -64,14 +45,3 @@ class Shop(QStackedWidget): return super().hideEvent(a0) # TODO: Implement store unloading return super().hideEvent(a0) - - def update_wishlist(self): - self.shop.update_wishlist() - - def show_game_info(self, data): - self.info.update_game(data) - self.setCurrentIndex(2) - - def show_search_results(self, text: str): - self.search_results.load_results(text) - self.setCurrentIndex(1) diff --git a/rare/components/tabs/store/__main__.py b/rare/components/tabs/store/__main__.py new file mode 100644 index 00000000..c14f5874 --- /dev/null +++ b/rare/components/tabs/store/__main__.py @@ -0,0 +1,39 @@ +import sys + +from PyQt5.QtCore import QSize +from PyQt5.QtWidgets import QDialog, QApplication, QVBoxLayout +from legendary.core import LegendaryCore + +from . import StoreTab + + +class StoreWindow(QDialog): + def __init__(self): + super().__init__() + + self.core = LegendaryCore() + self.core.login() + self.store_tab = StoreTab(self.core, self) + + layout = QVBoxLayout(self) + layout.addWidget(self.store_tab) + + self.store_tab.show() + + +if __name__ == "__main__": + import rare.resources.static_css + # import rare.resources.stylesheets.RareStyle + from rare.utils.misc import set_style_sheet + + app = QApplication(sys.argv) + app.setApplicationName("Rare") + app.setOrganizationName("Rare") + + set_style_sheet("") + set_style_sheet("RareStyle") + window = StoreWindow() + window.setWindowTitle(f"{app.applicationName()} - Store") + window.resize(QSize(1280, 800)) + window.show() + app.exec() diff --git a/rare/components/tabs/store/api/__init__.py b/rare/components/tabs/store/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/components/tabs/store/api/constants/__init__.py b/rare/components/tabs/store/api/constants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/components/tabs/store/api/constants/queries.py b/rare/components/tabs/store/api/constants/queries.py new file mode 100644 index 00000000..5e5d9919 --- /dev/null +++ b/rare/components/tabs/store/api/constants/queries.py @@ -0,0 +1,568 @@ + +FEED_QUERY = ''' +query feedQuery( + $locale: String! + $countryCode: String + $offset: Int + $postsPerPage: Int + $category: String +) { + TransientStream { + myTransientFeed(countryCode: $countryCode, locale: $locale) { + id + activity { + ... on LinkAccountActivity { + type + created_at + platforms + } + ... on SuggestedFriendsActivity { + type + created_at + platform + suggestions { + epicId + epicDisplayName + platformFullName + platformAvatar + } + } + ... on IncomingInvitesActivity { + type + created_at + invites { + epicId + epicDisplayName + } + } + ... on RecentPlayersActivity { + type + created_at + players { + epicId + epicDisplayName + playedGameName + } + } + } + } + } + Blog { + dieselBlogPosts: getPosts( + locale: $locale + offset: $offset + postsPerPage: $postsPerPage + category: $category + ) { + blogList { + _id + author + category + content + urlPattern + slug + sticky + title + date + image + shareImage + trendingImage + url + featured + link + externalLink + } + } + } +} +''' + +REVIEWS_QUERY = ''' +query productReviewsQuery($sku: String!) { + OpenCritic { + productReviews(sku: $sku) { + id + name + openCriticScore + reviewCount + percentRecommended + openCriticUrl + award + topReviews { + publishedDate + externalUrl + snippet + language + score + author + ScoreFormat { + id + description + } + OutletId + outletName + displayScore + } + } + } +} +''' + +MEDIA_QUERY = ''' +query fetchMediaRef($mediaRefId: String!) { + Media { + getMediaRef(mediaRefId: $mediaRefId) { + accountId + outputs { + duration + url + width + height + key + contentType + } + namespace + } + } +} +''' + +ADDONS_QUERY = ''' +query getAddonsByNamespace( + $categories: String! + $count: Int! + $country: String! + $locale: String! + $namespace: String! + $sortBy: String! + $sortDir: String! +) { + Catalog { + catalogOffers( + namespace: $namespace + locale: $locale + params: { + category: $categories + count: $count + country: $country + sortBy: $sortBy + sortDir: $sortDir + } + ) { + elements { + countriesBlacklist + customAttributes { + key + value + } + description + developer + effectiveDate + id + isFeatured + keyImages { + type + url + } + lastModifiedDate + longDescription + namespace + offerType + productSlug + releaseDate + status + technicalDetails + title + urlSlug + } + } + } +} +''' + +CATALOG_QUERY = ''' +query catalogQuery( + $category: String + $count: Int + $country: String! + $keywords: String + $locale: String + $namespace: String! + $sortBy: String + $sortDir: String + $start: Int + $tag: String +) { + Catalog { + catalogOffers( + namespace: $namespace + locale: $locale + params: { + count: $count + country: $country + category: $category + keywords: $keywords + sortBy: $sortBy + sortDir: $sortDir + start: $start + tag: $tag + } + ) { + elements { + isFeatured + collectionOfferIds + title + id + namespace + description + keyImages { + type + url + } + seller { + id + name + } + productSlug + urlSlug + items { + id + namespace + } + customAttributes { + key + value + } + categories { + path + } + price(country: $country) { + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + } + } + } + linkedOfferId + linkedOffer { + effectiveDate + customAttributes { + key + value + } + } + } + paging { + count + total + } + } + } +} +''' + +CATALOG_TAGS_QUERY = ''' +query catalogTags($namespace: String!) { + Catalog { + tags(namespace: $namespace, start: 0, count: 999) { + elements { + aliases + id + name + referenceCount + status + } + } + } +} +''' + +PREREQUISITES_QUERY = ''' +query fetchPrerequisites($offerParams: [OfferParams]) { + Launcher { + prerequisites(offerParams: $offerParams) { + namespace + offerId + missingPrerequisiteItems + satisfiesPrerequisites + } + } +} +''' + +PROMOTIONS_QUERY = ''' +query promotionsQuery( + $namespace: String! + $country: String! + $locale: String! +) { + Catalog { + catalogOffers( + namespace: $namespace + locale: $locale + params: { + category: "freegames" + country: $country + sortBy: "effectiveDate" + sortDir: "asc" + } + ) { + elements { + title + description + id + namespace + categories { + path + } + linkedOfferNs + linkedOfferId + keyImages { + type + url + } + productSlug + promotions { + promotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + upcomingPromotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + } + } + } + } +} +''' + +OFFERS_QUERY = ''' +query catalogQuery( + $productNamespace: String! + $offerId: String! + $locale: String + $country: String! + $includeSubItems: Boolean! +) { + Catalog { + catalogOffer(namespace: $productNamespace, id: $offerId, locale: $locale) { + title + id + namespace + description + effectiveDate + expiryDate + isCodeRedemptionOnly + keyImages { + type + url + } + seller { + id + name + } + productSlug + urlSlug + url + tags { + id + } + items { + id + namespace + } + customAttributes { + key + value + } + categories { + path + } + price(country: $country) { + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + currencyCode + currencyInfo { + decimals + } + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + discountSetting { + discountType + } + } + } + } + } + offerSubItems(namespace: $productNamespace, id: $offerId) + @include(if: $includeSubItems) { + namespace + id + releaseInfo { + appId + platform + } + } + } +} +''' + +SEARCH_STORE_QUERY = ''' +query searchStoreQuery( + $allowCountries: String + $category: String + $count: Int + $country: String! + $keywords: String + $locale: String + $namespace: String + $itemNs: String + $sortBy: String + $sortDir: String + $start: Int + $tag: String + $releaseDate: String + $withPrice: Boolean = false + $withPromotions: Boolean = false +) { + Catalog { + searchStore( + allowCountries: $allowCountries + category: $category + count: $count + country: $country + keywords: $keywords + locale: $locale + namespace: $namespace + itemNs: $itemNs + sortBy: $sortBy + sortDir: $sortDir + releaseDate: $releaseDate + start: $start + tag: $tag + ) { + elements { + title + id + namespace + description + effectiveDate + keyImages { + type + url + } + seller { + id + name + } + productSlug + urlSlug + url + tags { + id + } + items { + id + namespace + } + customAttributes { + key + value + } + categories { + path + } + price(country: $country) @include(if: $withPrice) { + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + currencyCode + currencyInfo { + decimals + } + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + discountSetting { + discountType + } + } + } + } + promotions(category: $category) @include(if: $withPromotions) { + promotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + upcomingPromotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } + } + } + } + paging { + count + total + } + } + } +} +''' \ No newline at end of file diff --git a/rare/components/tabs/store/api/debug.py b/rare/components/tabs/store/api/debug.py new file mode 100644 index 00000000..24a20016 --- /dev/null +++ b/rare/components/tabs/store/api/debug.py @@ -0,0 +1,29 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QTreeView, QDialog, QVBoxLayout + +from rare.utils.json_formatter import QJsonModel + + +class DebugView(QTreeView): + def __init__(self, data, parent=None): + super(DebugView, self).__init__(parent=parent) + self.setColumnWidth(0, 300) + self.setWordWrap(True) + self.model = QJsonModel(self) + self.setModel(self.model) + self.setContextMenuPolicy(Qt.ActionsContextMenu) + try: + self.model.load(data) + except Exception as e: + pass + self.resizeColumnToContents(0) + + +class DebugDialog(QDialog): + def __init__(self, data, parent=None): + super().__init__(parent=parent) + self.resize(800, 600) + + layout = QVBoxLayout(self) + view = DebugView(data, self) + layout.addWidget(view) diff --git a/rare/components/tabs/store/api/graphql/.graphqlconfig b/rare/components/tabs/store/api/graphql/.graphqlconfig new file mode 100644 index 00000000..54fc8e5f --- /dev/null +++ b/rare/components/tabs/store/api/graphql/.graphqlconfig @@ -0,0 +1,15 @@ +{ + "name": "EGS GraphQL Schema", + "schemaPath": "schema.graphql", + "extensions": { + "endpoints": { + "Default GraphQL Endpoint": { + "url": "http://localhost:8080/graphql", + "headers": { + "user-agent": "JS GraphQL" + }, + "introspect": false + } + } + } +} \ No newline at end of file diff --git a/rare/components/tabs/store/api/graphql/schema.graphql b/rare/components/tabs/store/api/graphql/schema.graphql new file mode 100644 index 00000000..6ccfe7a7 --- /dev/null +++ b/rare/components/tabs/store/api/graphql/schema.graphql @@ -0,0 +1,76 @@ +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 AppliedRules { + id: ID + endDate: Date + discountSetting: DiscountSetting +} + +type LineOfferRes { + appliedRules: [AppliedRules] +} + +type GetPriceRes { + totalPrice: TotalPrice + lineOffers: [LineOfferRes] +} + +type Image { + type: String + url: String + alt: String +} + +type StorePageMapping { + cmsSlug: String + offerId: ID + prePurchaseOfferId: ID +} + +type PageSandboxModel { + pageSlug: String + pageType: String + productId: ID + sandboxId: ID + createdDate: Date + updatedDate: Date + deletedDate: Date + mappings: [StorePageMapping] +} + +type CatalogNamespace { + parent: ID + displayName: String + store: String + mappings: [PageSandboxModel] +} + +type CatalogItem { + id: ID + namespace: ID +} \ No newline at end of file diff --git a/rare/components/tabs/store/api/models/__init__.py b/rare/components/tabs/store/api/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/components/tabs/store/api/models/diesel.py b/rare/components/tabs/store/api/models/diesel.py new file mode 100644 index 00000000..20a4f07a --- /dev/null +++ b/rare/components/tabs/store/api/models/diesel.py @@ -0,0 +1,164 @@ +import logging +from dataclasses import dataclass, field +from typing import List, Dict, Any, Type, Optional + +logger = logging.getLogger("DieselModels") + +# lk: Typing overloads for unimplemented types +DieselSocialLinks = Dict + + +@dataclass +class DieselSystemDetailItem: + _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( + _type=d.pop("_type", ""), + minimum=d.pop("minimum", ""), + recommended=d.pop("recommended", ""), + title=d.pop("title", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselSystemDetail: + _type: Optional[str] = None + details: Optional[List[DieselSystemDetailItem]] = None + systemType: 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( + _type=d.pop("_type", ""), + details=details, + systemType=d.pop("systemType", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselSystemDetails: + _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( + _type=d.pop("_type", ""), + languages=d.pop("languages", []), + rating=d.pop("rating", {}), + systems=systems, + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProductAbout: + _type: Optional[str] = None + desciption: Optional[str] = None + developerAttribution: Optional[str] = None + publisherAttribution: Optional[str] = None + shortDescription: 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( + _type=d.pop("_type", ""), + desciption=d.pop("description", ""), + developerAttribution=d.pop("developerAttribution", ""), + publisherAttribution=d.pop("publisherAttribution", ""), + shortDescription=d.pop("shortDescription", ""), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProductDetail: + _type: Optional[str] = None + about: Optional[DieselProductAbout] = None + requirements: Optional[DieselSystemDetails] = None + socialLinks: 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( + _type=d.pop("_type", ""), + about=about, + requirements=requirements, + socialLinks=d.pop("socialLinks", {}), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class DieselProduct: + _id: Optional[str] = None + _images_: Optional[List[str]] = None + _locale: Optional[str] = None + _slug: Optional[str] = None + _title: Optional[str] = None + _urlPattern: Optional[str] = None + namespace: Optional[str] = None + pages: Optional[List["DieselProduct"]] = None + data: Optional[DieselProductDetail] = None + productName: 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( + _id=d.pop("_id", ""), + _images_=d.pop("_images_", []), + _locale=d.pop("_locale", ""), + _slug=d.pop("_slug", ""), + _title=d.pop("_title", ""), + _urlPattern=d.pop("_urlPattern", ""), + namespace=d.pop("namespace", ""), + pages=pages, + data=data, + productName=d.pop("productName", ""), + ) + tmp.unmapped = d + return tmp diff --git a/rare/components/tabs/store/api/models/query.py b/rare/components/tabs/store/api/models/query.py new file mode 100644 index 00000000..f3f21ca8 --- /dev/null +++ b/rare/components/tabs/store/api/models/query.py @@ -0,0 +1,80 @@ +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import List + + +@dataclass +class SearchDateRange: + start_date: datetime = datetime(year=1990, month=1, day=1, tzinfo=timezone.utc) + end_date: datetime = datetime.utcnow().replace(tzinfo=timezone.utc) + + 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-%d' + return datetime.strftime(date, '%Y-%m-%dT%H:%M:%S.000Z') + return f"[{fmt_date(self.start_date)},{fmt_date(self.end_date)}]" + + +@dataclass +class SearchStoreQuery: + country: str = "US" + category: str = "games/edition/base|bundles/games|editors|software/edition/base" + count: int = 30 + keywords: str = "" + language: str = "en" + namespace: str = "" + with_mapping: bool = True + item_ns: str = "" + sort_by: str = "releaseDate" + sort_dir: str = "DESC" + start: int = 0 + tag: List[str] = "" + release_date: SearchDateRange = field(default_factory=SearchDateRange) + with_price: bool = True + with_promotions: bool = True + price_range: str = "" + free_game: bool = None + on_sale: bool = None + effective_date: SearchDateRange = field(default_factory=SearchDateRange) + + def __post_init__(self): + self.locale = f"{self.language}-{self.country}" + + def to_dict(self): + payload = { + "allowCountries": self.country, + "category": self.category, + "count": self.count, + "country": self.country, + "keywords": self.keywords, + "locale": self.locale, + "namespace": self.namespace, + "withMapping": self.with_mapping, + "itemNs": self.item_ns, + "sortBy": self.sort_by, + "sortDir": self.sort_dir, + "start": self.start, + "tag": self.tag, + "releaseDate": str(self.release_date), + "withPrice": self.with_price, + "withPromotions": self.with_promotions, + "priceRange": self.price_range, + "freeGame": self.free_game, + "onSale": self.on_sale, + "effectiveDate": str(self.effective_date), + } + # payload.pop("withPromotions") + payload.pop("onSale") + if self.price_range == "free": + payload["freeGame"] = True + payload.pop("priceRange") + elif self.price_range.startswith(""): + payload["priceRange"] = self.price_range.replace("", "") + if self.on_sale: + payload["onSale"] = True + + if self.price_range: + payload["effectiveDate"] = self.effective_date + else: + payload.pop("priceRange") + return payload diff --git a/rare/components/tabs/store/api/models/response.py b/rare/components/tabs/store/api/models/response.py new file mode 100644 index 00000000..23332d51 --- /dev/null +++ b/rare/components/tabs/store/api/models/response.py @@ -0,0 +1,480 @@ +import logging +from dataclasses import dataclass, field +from datetime import datetime +from typing import List, Dict, Any, Type, Optional, Tuple + +from .utils import parse_date + +logger = logging.getLogger("StoreApiModels") + +# lk: Typing overloads for unimplemented types +DieselSocialLinks = Dict + +CatalogNamespaceModel = Dict +CategoryModel = Dict +CustomAttributeModel = Dict +ItemModel = Dict +SellerModel = Dict +PageSandboxModel = Dict +TagModel = Dict + + +@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 + tall_types = ("DieselStoreFrontTall", "OfferImageTall", "Thumbnail", "ProductLogo", "DieselGameBoxLogo") + wide_types = ("DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo") + + 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_images = filter(lambda img: img.type in KeyImagesModel.tall_types, self.key_images) + tall_images = sorted(tall_images, key=lambda x: KeyImagesModel.tall_types.index(x.type)) + return tall_images + + def available_wide(self) -> List[ImageUrlModel]: + wide_images = filter(lambda img: img.type in KeyImagesModel.wide_types, self.key_images) + wide_images = sorted(wide_images, key=lambda x: KeyImagesModel.wide_types.index(x.type)) + return wide_images + + def for_dimensions(self, w: int, h: int) -> ImageUrlModel: + 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 + + +CurrencyModel = Dict +FormattedPriceModel = Dict +LineOffersModel = Dict + + +@dataclass +class TotalPriceModel: + discountPrice: Optional[int] = None + originalPrice: Optional[int] = None + voucherDiscount: Optional[int] = None + discount: Optional[int] = None + currencyCode: Optional[str] = None + currencyInfo: Optional[CurrencyModel] = None + fmtPrice: Optional[FormattedPriceModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["TotalPriceModel"], src: Dict[str, Any]) -> "TotalPriceModel": + d = src.copy() + tmp = cls( + discountPrice=d.pop("discountPrice", None), + originalPrice=d.pop("originalPrice", None), + voucherDiscount=d.pop("voucherDiscount", None), + discount=d.pop("discount", None), + currencyCode=d.pop("currencyCode", None), + currencyInfo=d.pop("currrencyInfo", {}), + fmtPrice=d.pop("fmtPrice", {}), + ) + tmp.unmapped = d + return tmp + + +@dataclass +class GetPriceResModel: + totalPrice: Optional[TotalPriceModel] = None + lineOffers: Optional[LineOffersModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["GetPriceResModel"], src: Dict[str, Any]) -> "GetPriceResModel": + d = src.copy() + total_price = TotalPriceModel.from_dict(x) if (x := d.pop("totalPrice", {})) else None + tmp = cls(totalPrice=total_price, lineOffers=d.pop("lineOffers", {})) + tmp.unmapped = d + return tmp + + +DiscountSettingModel = Dict + + +@dataclass +class PromotionalOfferModel: + startDate: Optional[datetime] = None + endDate: Optional[datetime] = None + discountSetting: Optional[DiscountSettingModel] = None + + @classmethod + def from_dict(cls: Type["PromotionalOfferModel"], src: Dict[str, Any]) -> "PromotionalOfferModel": + d = src.copy() + start_date = parse_date(x) if (x := d.pop("startDate", "")) else None + end_date = parse_date(x) if (x := d.pop("endDate", "")) else None + tmp = cls(startDate=start_date, endDate=end_date, discountSetting=d.pop("discountSetting", {})) + tmp.unmapped = d + return tmp + + +@dataclass +class PromotionalOffersModel: + promotionalOffers: Optional[Tuple[PromotionalOfferModel]] = None + + @classmethod + def from_list(cls: Type["PromotionalOffersModel"], src: Dict[str, List]) -> "PromotionalOffersModel": + d = src.copy() + promotional_offers = ( + tuple([PromotionalOfferModel.from_dict(y) for y in x]) if (x := d.pop("promotionalOffers", [])) else None + ) + tmp = cls(promotionalOffers=promotional_offers) + tmp.unmapped = d + return tmp + + +@dataclass +class PromotionsModel: + promotionalOffers: Optional[Tuple[PromotionalOffersModel]] = None + upcomingPromotionalOffers: Optional[Tuple[PromotionalOffersModel]] = None + + @classmethod + def from_dict(cls: Type["PromotionsModel"], src: Dict[str, Any]) -> "PromotionsModel": + d = src.copy() + promotional_offers = ( + tuple([PromotionalOffersModel.from_list(y) for y in x]) if (x := d.pop("promotionalOffers", [])) else None + ) + upcoming_promotional_offers = ( + tuple([PromotionalOffersModel.from_list(y) for y in x]) + if (x := d.pop("upcomingPromotionalOffers", [])) + else None + ) + tmp = cls(promotionalOffers=promotional_offers, upcomingPromotionalOffers=upcoming_promotional_offers) + tmp.unmapped = d + return tmp + + +@dataclass +class CatalogOfferModel: + catalogNs: Optional[CatalogNamespaceModel] = None + categories: Optional[List[CategoryModel]] = None + customAttributes: Optional[List[CustomAttributeModel]] = None + description: Optional[str] = None + effectiveDate: Optional[datetime] = None + expiryDate: Optional[datetime] = None + id: Optional[str] = None + isCodeRedemptionOnly: Optional[bool] = None + items: Optional[List[ItemModel]] = None + keyImages: Optional[KeyImagesModel] = None + namespace: Optional[str] = None + offerMappings: Optional[List[PageSandboxModel]] = None + offerType: Optional[str] = None + price: Optional[GetPriceResModel] = None + productSlug: Optional[str] = None + promotions: Optional[PromotionsModel] = None + seller: Optional[SellerModel] = None + status: Optional[str] = None + tags: Optional[List[TagModel]] = None + title: Optional[str] = None + url: Optional[str] = None + urlSlug: Optional[str] = None + viewableDate: 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 = GetPriceResModel.from_dict(x) if (x := d.pop("price", {})) else None + promotions = PromotionsModel.from_dict(x) if (x := d.pop("promotions", {})) else None + viewable_date = parse_date(x) if (x := d.pop("viewableDate", "")) else None + tmp = cls( + catalogNs=d.pop("catalogNs", {}), + categories=d.pop("categories", []), + customAttributes=d.pop("customAttributes", []), + description=d.pop("description", ""), + effectiveDate=effective_date, + expiryDate=expiry_date, + id=d.pop("id", ""), + isCodeRedemptionOnly=d.pop("isCodeRedemptionOnly", None), + items=d.pop("items", []), + keyImages=key_images, + namespace=d.pop("namespace", ""), + offerMappings=d.pop("offerMappings", []), + offerType=d.pop("offerType", ""), + price=price, + productSlug=d.pop("productSlug", ""), + promotions=promotions, + seller=d.pop("seller", {}), + status=d.pop("status", ""), + tags=d.pop("tags", []), + title=d.pop("title", ""), + url=d.pop("url", ""), + urlSlug=d.pop("urlSlug", ""), + viewableDate=viewable_date, + ) + tmp.unmapped = d + return tmp + + +@dataclass +class WishlistItemModel: + created: Optional[datetime] = None + id: Optional[str] = None + namespace: Optional[str] = None + isFirstTime: Optional[bool] = None + offerId: 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", ""), + isFirstTime=d.pop("isFirstTime", None), + offerId=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: + searchStore: Optional[SearchStoreModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["CatalogModel"], src: Dict[str, Any]) -> "CatalogModel": + d = src.copy() + search_store = SearchStoreModel.from_dict(x) if (x := d.pop("searchStore", {})) else None + tmp = cls(searchStore=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: + wishlistItem: 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(wishlistItem=wishlist_item, success=d.pop("success", None)) + tmp.unmapped = d + return tmp + + +@dataclass +class WishlistModel: + wishlistItems: Optional[WishlistItemsModel] = None + removeFromWishlist: Optional[RemoveFromWishlistModel] = None + addToWishlist: Optional[AddToWishlistModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["WishlistModel"], src: Dict[str, Any]) -> "WishlistModel": + d = src.copy() + wishlist_items = WishlistItemsModel.from_dict(x) if (x := d.pop("wishlistItems", {})) else None + remove_from_wishlist = RemoveFromWishlistModel.from_dict(x) if (x := d.pop("removeFromWishlist", {})) else None + add_to_wishlist = AddToWishlistModel.from_dict(x) if (x := d.pop("addToWishlist", {})) else None + tmp = cls( + wishlistItems=wishlist_items, removeFromWishlist=remove_from_wishlist, addToWishlist=add_to_wishlist + ) + tmp.unmapped = d + return tmp + + +ProductModel = Dict + + +@dataclass +class DataModel: + product: Optional[ProductModel] = None + catalog: Optional[CatalogModel] = None + wishlist: Optional[WishlistModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["DataModel"], src: Dict[str, Any]) -> "DataModel": + d = src.copy() + catalog = CatalogModel.from_dict(x) if (x := d.pop("Catalog", {})) else None + wishlist = WishlistModel.from_dict(x) if (x := d.pop("Wishlist", {})) else None + tmp = cls(product=d.pop("Product", {}), catalog=catalog, wishlist=wishlist) + tmp.unmapped = d + return tmp + + +@dataclass +class ErrorModel: + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["ErrorModel"], src: Dict[str, Any]) -> "ErrorModel": + d = src.copy() + tmp = cls() + tmp.unmapped = d + return tmp + + +@dataclass +class ExtensionsModel: + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["ExtensionsModel"], src: Dict[str, Any]) -> "ExtensionsModel": + d = src.copy() + tmp = cls() + tmp.unmapped = d + return tmp + + +@dataclass +class ResponseModel: + data: Optional[DataModel] = None + errors: Optional[List[ErrorModel]] = None + extensions: Optional[ExtensionsModel] = None + unmapped: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_dict(cls: Type["ResponseModel"], src: Dict[str, Any]) -> "ResponseModel": + d = src.copy() + data = DataModel.from_dict(x) if (x := d.pop("data", {})) else None + _errors = d.pop("errors", []) + errors = [] if _errors else None + for item in _errors: + error = ErrorModel.from_dict(item) + errors.append(error) + extensions = ExtensionsModel.from_dict(x) if (x := d.pop("extensions", {})) else None + tmp = cls(data=data, errors=errors, extensions=extensions) + tmp.unmapped = d + return tmp diff --git a/rare/components/tabs/store/api/models/utils.py b/rare/components/tabs/store/api/models/utils.py new file mode 100644 index 00000000..06f79c67 --- /dev/null +++ b/rare/components/tabs/store/api/models/utils.py @@ -0,0 +1,5 @@ +from datetime import datetime, timezone + + +def parse_date(date: str): + return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc) diff --git a/rare/components/tabs/store/constants.py b/rare/components/tabs/store/constants.py index 246729ef..8a839b93 100644 --- a/rare/components/tabs/store/constants.py +++ b/rare/components/tabs/store/constants.py @@ -44,74 +44,411 @@ class Constants(QObject): ] -game_query = ( - "query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, " - "$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, " - "$sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean " - "= false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, " - "$effectiveDate: String) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n " - "category: $category\n count: $count\n country: $country\n keywords: $keywords\n " - "locale: $locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n " - "sortDir: $sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n " - "priceRange: $priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: " - "$effectiveDate\n ) {\n elements {\n title\n id\n namespace\n " - "description\n effectiveDate\n keyImages {\n type\n url\n }\n " - " currentPrice\n seller {\n id\n name\n }\n productSlug\n " - " urlSlug\n url\n tags {\n id\n }\n items {\n id\n " - " namespace\n }\n customAttributes {\n key\n value\n }\n " - "categories {\n path\n }\n catalogNs @include(if: $withMapping) {\n " - 'mappings(pageType: "productHome") {\n pageSlug\n pageType\n }\n ' - "}\n offerMappings @include(if: $withMapping) {\n pageSlug\n pageType\n " - "}\n price(country: $country) @include(if: $withPrice) {\n totalPrice {\n " - "discountPrice\n originalPrice\n voucherDiscount\n discount\n " - " currencyCode\n currencyInfo {\n decimals\n }\n fmtPrice(" - "locale: $locale) {\n originalPrice\n discountPrice\n " - "intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n " - " id\n endDate\n discountSetting {\n discountType\n " - " }\n }\n }\n }\n promotions(category: $category) @include(if: " - "$withPromotions) {\n promotionalOffers {\n promotionalOffers {\n " - "startDate\n endDate\n discountSetting {\n discountType\n " - " discountPercentage\n }\n }\n }\n " - "upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n " - "endDate\n discountSetting {\n discountType\n " - "discountPercentage\n }\n }\n }\n }\n }\n paging {\n " - " count\n total\n }\n }\n }\n}\n " -) +__Image = ''' +type +url +alt +''' -search_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) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n " - "category: $category\n count: $count\n country: $country\n keywords: $keywords\n locale: " - "$locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n sortDir: " - "$sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n priceRange: " - "$priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: $effectiveDate\n ) {" - "\n elements {\n title\n id\n namespace\n description\n " - "effectiveDate\n keyImages {\n type\n url\n }\n currentPrice\n " - "seller {\n id\n name\n }\n productSlug\n urlSlug\n url\n " - " tags {\n id\n }\n items {\n id\n namespace\n }\n " - "customAttributes {\n key\n value\n }\n categories {\n path\n " - '}\n catalogNs @include(if: $withMapping) {\n mappings(pageType: "productHome") {\n ' - " pageSlug\n pageType\n }\n }\n offerMappings @include(if: $withMapping) " - "{\n pageSlug\n pageType\n }\n price(country: $country) @include(if: " - "$withPrice) {\n totalPrice {\n discountPrice\n originalPrice\n " - "voucherDiscount\n discount\n currencyCode\n currencyInfo {\n " - "decimals\n }\n fmtPrice(locale: $locale) {\n originalPrice\n " - "discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n " - " appliedRules {\n id\n endDate\n discountSetting {\n " - "discountType\n }\n }\n }\n }\n promotions(category: " - "$category) @include(if: $withPromotions) {\n promotionalOffers {\n promotionalOffers {\n " - " startDate\n endDate\n discountSetting {\n " - "discountType\n discountPercentage\n }\n }\n }\n " - "upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n " - "endDate\n discountSetting {\n discountType\n discountPercentage\n " - " }\n }\n }\n }\n }\n paging {\n count\n " - "total\n }\n }\n }\n}\n " -) +__StorePageMapping = ''' +cmsSlug +offerId +prePurchaseOfferId +''' -wishlist_query = '\n query wishlistQuery($country:String!, $locale:String) {\n Wishlist {\n wishlistItems {\n elements {\n id\n order\n created\n offerId\n updated\n namespace\n \n offer {\n productSlug\n urlSlug\n title\n id\n namespace\n offerType\n expiryDate\n status\n isCodeRedemptionOnly\n description\n effectiveDate\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n productSlug\n urlSlug\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n catalogNs {\n mappings(pageType: "productHome") {\n pageSlug\n pageType\n }\n }\n offerMappings {\n pageSlug\n pageType\n }\n categories {\n path\n }\n price(country: $country) {\n totalPrice {\n discountPrice\n originalPrice\n voucherDiscount\n discount\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n currencyCode\n currencyInfo {\n decimals\n symbol\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n }\n }\n }\n }\n\n }\n }\n }\n }\n' -add_to_wishlist_query = "\n mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) {\n Wishlist {\n removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) {\n success\n }\n }\n }\n" -remove_from_wishlist_query = "\n mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) {\n Wishlist {\n removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) {\n success\n }\n }\n }\n" -coupon_query = "\n query getCoupons($currencyCountry: String!, $identityId: String!, $locale: String) {\n CodeRedemption {\n coupons(currencyCountry: $currencyCountry, identityId: $identityId, includeSalesEventInfo: true) {\n code\n codeStatus\n codeType\n consumptionMetadata {\n amountDisplay {\n amount\n currency\n placement\n symbol\n }\n minSalesPriceDisplay {\n amount\n currency\n placement\n symbol\n }\n }\n endDate\n namespace\n salesEvent(locale: $locale) {\n eventName\n eventSlug\n voucherImages {\n type\n url\n }\n voucherLink\n }\n startDate\n }\n }\n }\n" +__PageSandboxModel = ''' +pageSlug +pageType +productId +sandboxId +createdDate +updatedDate +deletedDate +mappings { + %s +} +''' % (__StorePageMapping) + +__CatalogNamespace = ''' +parent +displayName +store +home: mappings(pageType: "productHome") { + %s +} +addons: mappings(pageType: "addon--cms-hybrid") { + %s +} +offers: mappings(pageType: "offer") { + %s +} +''' % (__PageSandboxModel, __PageSandboxModel, __PageSandboxModel) + +__CatalogItem = ''' +id +namespace +''' + +__GetPriceRes = ''' + totalPrice { + discountPrice + originalPrice + voucherDiscount + discount + currencyCode + currencyInfo { + decimals + symbol + } + fmtPrice(locale: $locale) { + originalPrice + discountPrice + intermediatePrice + } + } + lineOffers { + appliedRules { + id + endDate + discountSetting { + discountType + } + } + } +''' + +__Promotions = ''' +promotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } +} +upcomingPromotionalOffers { + promotionalOffers { + startDate + endDate + discountSetting { + discountType + discountPercentage + } + } +} +''' + +__CatalogOffer = ''' +title +id +namespace +offerType +expiryDate +status +isCodeRedemptionOnly +description +effectiveDate +keyImages { + %(image)s +} +currentPrice +seller { + id + name +} +productSlug +urlSlug +url +tags { + id + name + groupName +} +items { + %(catalog_item)s +} +customAttributes { + key + value +} +categories { + path +} +catalogNs @include(if: $withMapping) { + %(catalog_namespace)s +} +offerMappings @include(if: $withMapping) { + %(page_sandbox_model)s +} +price(country: $country) @include(if: $withPrice) { + %(get_price_res)s +} +promotions(category: $category) @include(if: $withPromotions) { + %(promotions)s +} +''' % { + "image": __Image, + "catalog_item": __CatalogItem, + "catalog_namespace": __CatalogNamespace, + "page_sandbox_model": __PageSandboxModel, + "get_price_res": __GetPriceRes, + "promotions": __Promotions, +} + +__Pagination = ''' +count +total +''' + +SEARCH_STORE_QUERY = ''' +query searchStoreQuery( + $allowCountries: String + $category: String + $count: Int + $country: String! + $keywords: String + $locale: String + $namespace: String + $withMapping: Boolean = false + $itemNs: String + $sortBy: String + $sortDir: String + $start: Int + $tag: String + $releaseDate: String + $withPrice: Boolean = false + $withPromotions: Boolean = false + $priceRange: String + $freeGame: Boolean + $onSale: Boolean + $effectiveDate: String +) { + Catalog { + searchStore( + allowCountries: $allowCountries + category: $category + count: $count + country: $country + keywords: $keywords + locale: $locale + namespace: $namespace + itemNs: $itemNs + sortBy: $sortBy + sortDir: $sortDir + releaseDate: $releaseDate + start: $start + tag: $tag + priceRange: $priceRange + freeGame: $freeGame + onSale: $onSale + effectiveDate: $effectiveDate + ) { + elements { + %s + } + paging { + %s + } + } + } +} +''' % (__CatalogOffer, __Pagination) + +__WISHLIST_ITEM = ''' +id +order +created +offerId +updated +namespace +isFirstTime +offer(locale: $locale) { + %s +} +''' % __CatalogOffer + +WISHLIST_QUERY = ''' +query wishlistQuery( + $country: String! + $locale: String + $category: String + $withMapping: Boolean = false + $withPrice: Boolean = false + $withPromotions: Boolean = false +) { + Wishlist { + wishlistItems { + elements { + %s + } + } + } +} +''' % __WISHLIST_ITEM + +WISHLIST_ADD_QUERY = ''' +mutation addWishlistMutation( + $namespace: String! + $offerId: String! + $country: String! + $locale: String + $category: String + $withMapping: Boolean = false + $withPrice: Boolean = false + $withPromotions: Boolean = false +) { + Wishlist { + addToWishlist( + namespace: $namespace + offerId: $offerId + ) { + wishlistItem { + %s + } + success + } + } +} +''' % __WISHLIST_ITEM + +WISHLIST_REMOVE_QUERY = ''' +mutation removeFromWishlistMutation( + $namespace: String! + $offerId: String! + $operation: RemoveOperation! +) { + Wishlist { + removeFromWishlist( + namespace: $namespace + offerId: $offerId + operation: $operation + ) { + success + } + } +} +''' + +COUPONS_QUERY = ''' +query getCoupons( + $currencyCountry: String! + $identityId: String! + $locale: String +) { + CodeRedemption { + coupons( + currencyCountry: $currencyCountry + identityId: $identityId + includeSalesEventInfo: true + ) { + code + codeStatus + codeType + consumptionMetadata { + amountDisplay { + amount + currency + placement + symbol + } + minSalesPriceDisplay { + amount + currency + placement + symbol + } + } + endDate + namespace + salesEvent(locale: $locale) { + eventName + eventSlug + voucherImages { + type + url + } + voucherLink + } + startDate + } + } +} +''' + +STORE_CONFIG_QUERY = ''' +query getStoreConfig( + $includeCriticReviews: Boolean = false + $locale: String! + $sandboxId: String! + $templateId: String +) { + Product { + sandbox(sandboxId: $sandboxId) { + configuration(locale: $locale, templateId: $templateId) { + ... on StoreConfiguration { + configs { + shortDescription + criticReviews @include(if: $includeCriticReviews) { + openCritic + } + socialLinks { + platform + url + } + supportedAudio + supportedText + tags(locale: $locale) { + id + name + groupName + } + technicalRequirements { + macos { + minimum + recommended + title + } + windows { + minimum + recommended + title + } + } + } + } + ... on HomeConfiguration { + configs { + keyImages { + ... on KeyImage { + type + url + alt + } + } + longDescription + } + } + } + } + } +} +''' + + +def compress_query(query: str) -> str: + return query.replace(" ", "").replace("\n", " ") + + +game_query = compress_query(SEARCH_STORE_QUERY) +search_query = compress_query(SEARCH_STORE_QUERY) +wishlist_query = compress_query(WISHLIST_QUERY) +wishlist_add_query = compress_query(WISHLIST_ADD_QUERY) +wishlist_remove_query = compress_query(WISHLIST_REMOVE_QUERY) +coupons_query = compress_query(COUPONS_QUERY) +store_config_query = compress_query(STORE_CONFIG_QUERY) + + +if __name__ == "__main__": + print(SEARCH_STORE_QUERY) diff --git a/rare/components/tabs/store/game_info.py b/rare/components/tabs/store/game_info.py deleted file mode 100644 index 263f91e9..00000000 --- a/rare/components/tabs/store/game_info.py +++ /dev/null @@ -1,271 +0,0 @@ -import logging - -from PyQt5.QtCore import Qt, QUrl -from PyQt5.QtGui import QPixmap, QFont, QDesktopServices -from PyQt5.QtWidgets import ( - QWidget, - QLabel, - QPushButton, - QHBoxLayout, - QSpacerItem, - QGroupBox, - QTabWidget, - QGridLayout, -) - -from rare.components.tabs.store.shop_models import ShopGame -from rare.shared import LegendaryCoreSingleton -from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info -from rare.utils.extra_widgets import ImageLabel -from rare.utils.misc import qta_icon as icon -from rare.widgets.loading_widget import LoadingWidget - -logger = logging.getLogger("ShopInfo") - - -class ShopGameInfo(QWidget, Ui_shop_info): - game: ShopGame - data: dict - - # TODO Design - def __init__(self, installed_titles: list, api_core): - super(ShopGameInfo, self).__init__() - self.setupUi(self) - self.core = LegendaryCoreSingleton() - self.api_core = api_core - self.installed = installed_titles - self.open_store_button.clicked.connect(self.button_clicked) - self.image = ImageLabel() - self.image_stack.addWidget(self.image) - self.image_stack.addWidget(LoadingWidget()) - warn_label = QLabel() - warn_label.setPixmap( - icon("fa.warning").pixmap(160, 160).scaled(240, 320, Qt.IgnoreAspectRatio) - ) - self.image_stack.addWidget(warn_label) - - self.wishlist_button.clicked.connect(self.add_to_wishlist) - self.in_wishlist = False - self.wishlist = [] - - def handle_wishlist_update(self, data): - if data and data[0] == "error": - return - self.wishlist = [i["offer"]["title"] for i in data] - if self.title_str in self.wishlist: - self.in_wishlist = True - self.wishlist_button.setVisible(True) - self.wishlist_button.setText(self.tr("Remove from Wishlist")) - else: - self.in_wishlist = False - self.wishlist_button.setVisible(False) - - def update_game(self, data: dict): - self.image_stack.setCurrentIndex(1) - self.title.setText(data["title"]) - self.title_str = data["title"] - self.api_core.get_wishlist(self.handle_wishlist_update) - for i in reversed(range(self.req_group_box.layout().count())): - self.req_group_box.layout().itemAt(i).widget().deleteLater() - slug = data["productSlug"] - if not slug: - for mapping in data["offerMappings"]: - if mapping["pageType"] == "productHome": - slug = mapping["pageSlug"] - break - else: - logger.error("Could not get page information") - slug = "" - if "/home" in slug: - slug = slug.replace("/home", "") - self.slug = slug - - if data["namespace"] in self.installed: - self.open_store_button.setText(self.tr("Show Game on Epic Page")) - self.owned_label.setVisible(True) - else: - self.open_store_button.setText(self.tr("Buy Game in Epic Games Store")) - self.owned_label.setVisible(False) - - for i in range(self.req_group_box.layout().count()): - self.req_group_box.layout().itemAt(i).widget().deleteLater() - - self.price.setText(self.tr("Loading")) - self.wishlist_button.setVisible(False) - # self.title.setText(self.tr("Loading")) - self.image.setPixmap(QPixmap()) - self.data = data - is_bundle = False - for i in data["categories"]: - if "bundles" in i.get("path", ""): - is_bundle = True - - # init API request - if slug: - self.api_core.get_game(slug, is_bundle, self.data_received) - else: - self.data_received({}) - - def add_to_wishlist(self): - if not self.in_wishlist: - return - # self.api_core.add_to_wishlist(self.game.namespace, self.game.offer_id, - # lambda success: self.wishlist_button.setText(self.tr("Remove from wishlist")) - # if success else self.wishlist_button.setText("Something goes wrong")) - else: - self.api_core.remove_from_wishlist( - self.game.namespace, - self.game.offer_id, - lambda success: self.wishlist_button.setVisible(False) - if success - else self.wishlist_button.setText("Something goes wrong"), - ) - - def data_received(self, game): - try: - self.game = ShopGame.from_json(game, self.data) - except Exception as e: - logger.error(str(e)) - self.price.setText("Error") - self.req_group_box.setVisible(False) - for img in self.data.get("keyImages"): - if img["type"] in [ - "DieselStoreFrontWide", - "OfferImageTall", - "VaultClosed", - "ProductLogo", - ]: - self.image.update_image(img["url"], self.title_str, size=(240, 320)) - self.image_stack.setCurrentIndex(0) - break - else: - self.image_stack.setCurrentIndex(2) - self.price.setText("") - self.discount_price.setText("") - self.social_link_gb.setVisible(False) - self.tags.setText("") - self.dev.setText(self.data.get("seller", {}).get("name", "")) - return - self.title.setText(self.game.title) - - self.price.setFont(QFont()) - if self.game.price == "0" or self.game.price == 0: - self.price.setText(self.tr("Free")) - else: - self.price.setText(self.game.price) - if self.game.price != self.game.discount_price: - font = QFont() - font.setStrikeOut(True) - self.price.setFont(font) - self.discount_price.setText( - self.game.discount_price - if self.game.discount_price != "0" - else self.tr("Free") - ) - self.discount_price.setVisible(True) - else: - self.discount_price.setVisible(False) - - bold_font = QFont() - bold_font.setBold(True) - - if self.game.reqs: - req_tabs = QTabWidget() - for system in self.game.reqs: - min_label = QLabel(self.tr("Minimum")) - min_label.setFont(bold_font) - rec_label = QLabel(self.tr("Recommend")) - rec_label.setFont(bold_font) - req_widget = QWidget() - req_widget.setLayout(QGridLayout()) - req_widget.layout().addWidget(min_label, 0, 1) - req_widget.layout().addWidget(rec_label, 0, 2) - for i, (key, value) in enumerate( - self.game.reqs.get(system, {}).items() - ): - req_widget.layout().addWidget(QLabel(key), i + 1, 0) - min_label = QLabel(value[0]) - min_label.setWordWrap(True) - req_widget.layout().addWidget(min_label, i + 1, 1) - rec_label = QLabel(value[1]) - rec_label.setWordWrap(True) - req_widget.layout().addWidget(rec_label, i + 1, 2) - req_tabs.addTab(req_widget, system) - self.req_group_box.layout().addWidget(req_tabs) - else: - self.req_group_box.layout().addWidget( - QLabel(self.tr("Could not get requirements")) - ) - self.req_group_box.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: - img_url = "" - self.image.update_image(img_url, self.game.title, (240, 320)) - - self.image_stack.setCurrentIndex(0) - try: - if isinstance(self.game.developer, list): - self.dev.setText(", ".join(self.game.developer)) - else: - self.dev.setText(self.game.developer) - except KeyError: - pass - self.tags.setText(", ".join(self.game.tags)) - - # clear Layout - for widget in ( - self.social_link_gb.layout().itemAt(i) - for i in range(self.social_link_gb.layout().count()) - ): - if not isinstance(widget, QSpacerItem): - widget.widget().deleteLater() - self.social_link_gb.deleteLater() - self.social_link_gb = QGroupBox(self.tr("Social Links")) - self.social_link_gb.setLayout(QHBoxLayout()) - - self.layout().insertWidget(3, self.social_link_gb) - - self.social_link_gb.layout().addStretch(1) - link_count = 0 - for name, url in self.game.links: - - if name.lower() == "homepage": - icn = icon("mdi.web", "fa.search", scale_factor=1.5) - else: - try: - icn = icon(f"mdi.{name.lower()}", f"fa.{name.lower()}", scale_factor=1.5) - except Exception as e: - logger.error(str(e)) - continue - - button = SocialButton(icn, url) - self.social_link_gb.layout().addWidget(button) - link_count += 1 - self.social_link_gb.layout().addStretch(1) - - if link_count == 0: - self.social_link_gb.setVisible(False) - else: - self.social_link_gb.setVisible(True) - self.social_link_gb.layout().addStretch(1) - - def add_wishlist_items(self, wishlist): - wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"] - for game in wishlist: - self.wishlist.append(game["offer"]["title"]) - - def button_clicked(self): - QDesktopServices.openUrl(QUrl(f"https://www.epicgames.com/store/{self.core.language_code}/p/{self.slug}")) - - -class SocialButton(QPushButton): - def __init__(self, icn, url): - super(SocialButton, self).__init__(icn, "") - self.url = url - self.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url))) - self.setToolTip(url) diff --git a/rare/components/tabs/store/game_widgets.py b/rare/components/tabs/store/game_widgets.py deleted file mode 100644 index 8085965b..00000000 --- a/rare/components/tabs/store/game_widgets.py +++ /dev/null @@ -1,142 +0,0 @@ -import logging - -from PyQt5 import QtGui -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtGui import QFont -from PyQt5.QtNetwork import QNetworkAccessManager -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout - -from rare.components.tabs.store.shop_models import ImageUrlModel -from rare.ui.components.tabs.store.wishlist_widget import Ui_WishlistWidget -from rare.utils.extra_widgets import ImageLabel -from rare.utils.misc import qta_icon as icon - -logger = logging.getLogger("GameWidgets") - - -class GameWidget(QWidget): - show_info = pyqtSignal(dict) - - def __init__(self, path, json_info=None, width=300): - super(GameWidget, self).__init__() - self.manager = QNetworkAccessManager() - self.width = width - self.path = path - if json_info: - self.init_ui(json_info) - - def init_ui(self, json_info): - self.layout = QVBoxLayout() - self.image = ImageLabel() - self.layout.addWidget(self.image) - mini_layout = QHBoxLayout() - self.layout.addLayout(mini_layout) - - if not json_info: - self.layout.addWidget(QLabel("An error occurred")) - self.setLayout(self.layout) - return - - self.title_label = QLabel(json_info.get("title")) - self.title_label.setWordWrap(True) - mini_layout.addWidget(self.title_label) - mini_layout.addStretch(1) - - price = json_info["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - discount_price = json_info["price"]["totalPrice"]["fmtPrice"]["discountPrice"] - price_label = QLabel(price) - if price != discount_price: - font = QFont() - font.setStrikeOut(True) - price_label.setFont(font) - mini_layout.addWidget( - QLabel(discount_price if discount_price != "0" else self.tr("Free")) - ) - mini_layout.addWidget(price_label) - else: - if price == "0": - price_label.setText(self.tr("Free")) - mini_layout.addWidget(price_label) - - for c in r'<>?":|\/*': - json_info["title"] = json_info["title"].replace(c, "") - - self.json_info = json_info - self.slug = json_info["productSlug"] - - self.title = json_info["title"] - for img in json_info["keyImages"]: - if img["type"] in [ - "DieselStoreFrontWide", - "OfferImageWide", - "VaultClosed", - "ProductLogo", - ]: - if img["type"] == "VaultClosed" and self.title != "Mystery Game": - continue - self.image.update_image( - img["url"], - json_info["title"], - (self.width, int(self.width * 9 / 16)), - ) - break - else: - logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) - - self.setLayout(self.layout) - - self.setFixedSize(self.width + 10, self.width * 9 // 16 + 50) - - def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - self.show_info.emit(self.json_info) - - -class WishlistWidget(QWidget, Ui_WishlistWidget): - open_game = pyqtSignal(dict) - delete_from_wishlist = pyqtSignal(dict) - - def __init__(self, game: dict): - super(WishlistWidget, self).__init__() - self.setupUi(self) - self.game = game - self.title_label.setText(game.get("title")) - for attr in game["customAttributes"]: - if attr["key"] == "developerName": - self.developer.setText(attr["value"]) - break - else: - self.developer.setText(game["seller"]["name"]) - original_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - discount_price = game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] - - self.price.setText(original_price if original_price != "0" else self.tr("Free")) - # if discount - if original_price != discount_price: - self.discount = True - font = QFont() - font.setStrikeOut(True) - self.price.setFont(font) - self.discount_price.setText(discount_price) - else: - self.discount = False - self.discount_price.setVisible(False) - - self.image = ImageLabel() - self.layout().insertWidget(0, self.image) - image_model = ImageUrlModel.from_json(game["keyImages"]) - url = image_model.front_wide - if not url: - url = image_model.offer_image_wide - self.image.update_image(url, game.get("title"), (240, 135)) - self.delete_button.setIcon(icon("mdi.delete", color="white")) - self.delete_button.clicked.connect( - lambda: self.delete_from_wishlist.emit(self.game) - ) - - def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: - # left button - if e.button() == 1: - self.open_game.emit(self.game) - # right - elif e.button() == 2: - pass # self.showMenu(e) diff --git a/rare/components/tabs/store/landing.py b/rare/components/tabs/store/landing.py new file mode 100644 index 00000000..00af2cf8 --- /dev/null +++ b/rare/components/tabs/store/landing.py @@ -0,0 +1,239 @@ +import datetime +import logging +from typing import List + +from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal, QObject, QEvent +from PyQt5.QtGui import QShowEvent, QHideEvent, QResizeEvent +from PyQt5.QtWidgets import ( + QHBoxLayout, + QWidget, + QSizePolicy, + QVBoxLayout, + QSpacerItem, + QScrollArea, + QFrame, +) + +from rare.components.tabs.store.api.models.response import CatalogOfferModel, WishlistItemModel +from rare.widgets.flow_layout import FlowLayout +from rare.widgets.side_tab import SideTabContents +from rare.widgets.sliding_stack import SlidingStackedWidget +from .store_api import StoreAPI +from .widgets.details import DetailsWidget +from .widgets.groups import StoreGroup +from .widgets.items import StoreItemWidget + +logger = logging.getLogger("StoreLanding") + + +class LandingPage(SlidingStackedWidget, SideTabContents): + + def __init__(self, store_api: StoreAPI, parent=None): + super(LandingPage, self).__init__(parent=parent) + self.implements_scrollarea = True + + self.landing_widget = LandingWidget(store_api, parent=self) + self.landing_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.landing_widget.set_title.connect(self.set_title) + self.landing_widget.show_details.connect(self.show_details) + + self.landing_scroll = QScrollArea(self) + self.landing_scroll.setWidgetResizable(True) + self.landing_scroll.setFrameStyle(QFrame.NoFrame | QFrame.Plain) + self.landing_scroll.setWidget(self.landing_widget) + self.landing_scroll.widget().setAutoFillBackground(False) + self.landing_scroll.viewport().setAutoFillBackground(False) + + self.details_widget = DetailsWidget([], store_api, parent=self) + self.details_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.details_widget.set_title.connect(self.set_title) + self.details_widget.back_clicked.connect(self.show_main) + + self.setDirection(Qt.Horizontal) + self.addWidget(self.landing_scroll) + self.addWidget(self.details_widget) + + @pyqtSlot() + def show_main(self): + self.slideInWidget(self.landing_scroll) + + @pyqtSlot(object) + def show_details(self, game: CatalogOfferModel): + self.details_widget.update_game(game) + self.slideInWidget(self.details_widget) + + +class FreeGamesScroll(QScrollArea): + def __init__(self, parent=None): + super(FreeGamesScroll, self).__init__(parent=parent) + self.setObjectName(type(self).__name__) + + def setWidget(self, w): + super().setWidget(w) + w.installEventFilter(self) + + def eventFilter(self, a0: QObject, a1: QEvent) -> bool: + if a0 is self.widget() and a1.type() == QEvent.Resize: + self.__resize(a0) + return a0.event(a1) + return False + + def __resize(self, e: QResizeEvent): + minh = self.horizontalScrollBar().minimum() + maxh = self.horizontalScrollBar().maximum() + # lk: when the scrollbar is not visible, min and max are 0 + if maxh > minh: + height = ( + e.size().height() + + self.rect().height() // 2 + - self.contentsRect().height() // 2 + + self.widget().layout().spacing() + + self.horizontalScrollBar().sizeHint().height() + ) + else: + height = e.size().height() + self.rect().height() - self.contentsRect().height() + self.setMinimumHeight(max(height, self.minimumHeight())) + + +class LandingWidget(QWidget, SideTabContents): + show_details = pyqtSignal(CatalogOfferModel) + + def __init__(self, api: StoreAPI, parent=None): + super(LandingWidget, self).__init__(parent=parent) + self.api = api + + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 3, 0) + self.setLayout(layout) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + self.free_games_now = StoreGroup(self.tr("Free now"), layout=QHBoxLayout, parent=self) + self.free_games_now.main_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.free_games_now.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.free_games_next = StoreGroup(self.tr("Free next week"), layout=QHBoxLayout, parent=self) + self.free_games_next.main_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.free_games_next.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + + self.discounts_group = StoreGroup(self.tr("Wishlist discounts"), layout=FlowLayout, parent=self) + self.discounts_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + self.games_group = StoreGroup(self.tr("Free to play"), FlowLayout, self) + self.games_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.games_group.loading(False) + self.games_group.setVisible(False) + + free_scroll = FreeGamesScroll(self) + free_container = QWidget(free_scroll) + free_scroll.setWidget(free_container) + free_container_layout = QHBoxLayout(free_container) + + free_scroll.setWidgetResizable(True) + free_scroll.setFrameShape(QScrollArea.NoFrame) + free_scroll.setSizeAdjustPolicy(QScrollArea.AdjustToContents) + free_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + free_container_layout.setContentsMargins(0, 0, 0, 0) + free_container_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + free_container_layout.setSizeConstraint(QHBoxLayout.SetFixedSize) + free_container_layout.addWidget(self.free_games_now) + free_container_layout.addWidget(self.free_games_next) + + free_scroll.widget().setAutoFillBackground(False) + free_scroll.viewport().setAutoFillBackground(False) + + # layout.addWidget(self.free_games_now, alignment=Qt.AlignTop) + # layout.addWidget(self.free_games_next, alignment=Qt.AlignTop) + layout.addWidget(free_scroll, alignment=Qt.AlignTop) + layout.addWidget(self.discounts_group, alignment=Qt.AlignTop) + layout.addWidget(self.games_group, alignment=Qt.AlignTop) + layout.addItem(QSpacerItem(0, 0, QSizePolicy.Fixed, QSizePolicy.Expanding)) + + def showEvent(self, a0: QShowEvent) -> None: + if a0.spontaneous(): + return super().showEvent(a0) + self.api.get_free(self.__update_free_games) + self.api.get_wishlist(self.__update_wishlist_discounts) + return super().showEvent(a0) + + def hideEvent(self, a0: QHideEvent) -> None: + if a0.spontaneous(): + return super().hideEvent(a0) + # TODO: Implement tab unloading + return super().hideEvent(a0) + + def __update_wishlist_discounts(self, wishlist: List[WishlistItemModel]): + for w in self.discounts_group.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly): + self.discounts_group.layout().removeWidget(w) + w.deleteLater() + + for item in filter(lambda x: bool(x.offer.price.totalPrice.discount), wishlist): + w = StoreItemWidget(self.api.cached_manager, item.offer) + w.show_details.connect(self.show_details) + self.discounts_group.layout().addWidget(w) + have_discounts = any(map(lambda x: bool(x.offer.price.totalPrice.discount), wishlist)) + self.discounts_group.setVisible(have_discounts) + self.discounts_group.loading(False) + + def __update_free_games(self, free_games: List[CatalogOfferModel]): + for w in self.free_games_now.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly): + self.free_games_now.layout().removeWidget(w) + w.deleteLater() + + for w in self.free_games_next.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly): + self.free_games_next.layout().removeWidget(w) + w.deleteLater() + + date = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) + free_now = [] + free_next = [] + for item in free_games: + try: + if item.price.totalPrice.discountPrice == 0: + free_now.append(item) + continue + if item.title == "Mystery Game": + free_next.append(item) + continue + except KeyError as e: + logger.warning(str(e)) + + if item.promotions is not None: + if not item.promotions.promotionalOffers: + start_date = item.promotions.upcomingPromotionalOffers[0].promotionalOffers[0].startDate + else: + start_date = item.promotions.promotionalOffers[0].promotionalOffers[0].startDate + + if start_date > date: + free_next.append(item) + + # free games now + self.free_games_now.setVisible(bool(free_now)) + for item in free_now: + w = StoreItemWidget(self.api.cached_manager, item) + w.show_details.connect(self.show_details) + self.free_games_now.layout().addWidget(w) + self.free_games_now.loading(False) + + # free games next week + self.free_games_next.setVisible(bool(free_next)) + for item in free_next: + w = StoreItemWidget(self.api.cached_manager, item) + if item.title != "Mystery Game": + w.show_details.connect(self.show_details) + self.free_games_next.layout().addWidget(w) + self.free_games_next.loading(False) + + def show_games(self, data): + if not data: + return + + for w in self.games_group.findChildren(StoreItemWidget, options=Qt.FindDirectChildrenOnly): + self.games_group.layout().removeWidget(w) + w.deleteLater() + + for game in data: + w = StoreItemWidget(self.api.cached_manager, game) + w.show_details.connect(self.show_details) + self.games_group.layout().addWidget(w) + self.games_group.loading(False) diff --git a/rare/components/tabs/store/results.py b/rare/components/tabs/store/results.py new file mode 100644 index 00000000..6ae53088 --- /dev/null +++ b/rare/components/tabs/store/results.py @@ -0,0 +1,56 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import ( + QWidget, + QSizePolicy, + QLabel, + QScrollArea, +) + +from rare.widgets.flow_layout import FlowLayout +from .api.models.response import CatalogOfferModel +from .widgets.items import ResultsItemWidget + + +class ResultsWidget(QScrollArea): + show_details = pyqtSignal(CatalogOfferModel) + + def __init__(self, store_api, parent=None): + super(ResultsWidget, self).__init__(parent=parent) + self.store_api = store_api + + self.results_container = QWidget(self) + self.results_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.results_layout = FlowLayout(self.results_container) + self.setWidget(self.results_container) + self.setWidgetResizable(True) + + # self.main_layout = QVBoxLayout(self) + # self.main_layout.setContentsMargins(0, 0, 0, 0) + # self.main_layout.addWidget(self.results_scrollarea) + + self.setEnabled(False) + + def load_results(self, text: str): + self.setEnabled(False) + if text != "": + self.store_api.search_game(text, self.show_results) + + def show_results(self, results: dict): + for w in self.results_container.findChildren(QLabel, options=Qt.FindDirectChildrenOnly): + self.results_layout.removeWidget(w) + w.deleteLater() + for w in self.results_container.findChildren(ResultsItemWidget, options=Qt.FindDirectChildrenOnly): + self.results_layout.removeWidget(w) + w.deleteLater() + + if not results: + self.results_layout.addWidget(QLabel(self.tr("No results found"))) + else: + for res in results: + w = ResultsItemWidget(self.store_api.cached_manager, res, parent=self.results_container) + w.show_details.connect(self.show_details.emit) + self.results_layout.addWidget(w) + self.results_layout.update() + self.setEnabled(True) + diff --git a/rare/components/tabs/store/search.py b/rare/components/tabs/store/search.py new file mode 100644 index 00000000..1e93b1e0 --- /dev/null +++ b/rare/components/tabs/store/search.py @@ -0,0 +1,219 @@ +import logging +from typing import List + +from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot +from PyQt5.QtWidgets import ( + QCheckBox, + QWidget, + QSizePolicy, + QScrollArea, + QFrame, +) +from legendary.core import LegendaryCore + +from rare.ui.components.tabs.store.search import Ui_SearchWidget +from rare.utils.extra_widgets import ButtonLineEdit +from rare.widgets.side_tab import SideTabContents +from rare.widgets.sliding_stack import SlidingStackedWidget +from .api.models.query import SearchStoreQuery +from .api.models.response import CatalogOfferModel +from .constants import Constants +from .results import ResultsWidget +from .store_api import StoreAPI +from .widgets.details import DetailsWidget + +logger = logging.getLogger("Shop") + + +class SearchPage(SlidingStackedWidget, SideTabContents): + def __init__(self, store_api: StoreAPI, parent=None): + super(SearchPage, self).__init__(parent=parent) + self.implements_scrollarea = True + + self.search_widget = SearchWidget(store_api, parent=self) + self.search_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.search_widget.set_title.connect(self.set_title) + self.search_widget.show_details.connect(self.show_details) + + self.details_widget = DetailsWidget([], store_api, parent=self) + self.details_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.details_widget.set_title.connect(self.set_title) + self.details_widget.back_clicked.connect(self.show_main) + + self.setDirection(Qt.Horizontal) + self.addWidget(self.search_widget) + self.addWidget(self.details_widget) + + @pyqtSlot() + def show_main(self): + self.slideInWidget(self.search_widget) + + @pyqtSlot(object) + def show_details(self, game: CatalogOfferModel): + self.details_widget.update_game(game) + self.slideInWidget(self.details_widget) + + +# noinspection PyAttributeOutsideInit,PyBroadException +class SearchWidget(QWidget, SideTabContents): + show_details = pyqtSignal(CatalogOfferModel) + + def __init__(self, store_api: StoreAPI, parent=None): + super(SearchWidget, self).__init__(parent=parent) + self.implements_scrollarea = True + self.ui = Ui_SearchWidget() + self.ui.setupUi(self) + self.ui.main_layout.setContentsMargins(0, 0, 3, 0) + + self.ui.filter_scrollarea.widget().setAutoFillBackground(False) + self.ui.filter_scrollarea.viewport().setAutoFillBackground(False) + + self.store_api = store_api + self.price = "" + self.tags = [] + self.types = [] + self.update_games_allowed = True + + self.active_search_request = False + self.next_search = "" + self.wishlist: List = [] + + self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search")) + self.results_scrollarea = ResultsWidget(self.store_api, self) + self.results_scrollarea.show_details.connect(self.show_details) + + self.ui.left_layout.addWidget(self.search_bar) + self.ui.left_layout.addWidget(self.results_scrollarea) + + self.search_bar.returnPressed.connect(self.show_search_results) + self.search_bar.buttonClicked.connect(self.show_search_results) + + # self.init_filter() + + def load(self): + # load browse games + self.prepare_request() + + def show_search_results(self): + if text := self.search_bar.text(): + self.results_scrollarea.load_results(text) + # self.show_info.emit(self.search_bar.text()) + + def init_filter(self): + self.ui.none_price.toggled.connect( + lambda: self.prepare_request("") if self.ui.none_price.isChecked() else None + ) + self.ui.free_button.toggled.connect( + lambda: self.prepare_request("free") if self.ui.free_button.isChecked() else None + ) + self.ui.under10.toggled.connect( + lambda: self.prepare_request("[0, 1000)") if self.ui.under10.isChecked() else None + ) + self.ui.under20.toggled.connect( + lambda: self.prepare_request("[0, 2000)") if self.ui.under20.isChecked() else None + ) + self.ui.under30.toggled.connect( + lambda: self.prepare_request("[0, 3000)") if self.ui.under30.isChecked() else None + ) + self.ui.above.toggled.connect( + lambda: self.prepare_request("[1499,]") if self.ui.above.isChecked() else None + ) + # self.on_discount.toggled.connect(lambda: self.prepare_request("sale") if self.on_discount.isChecked() else None) + self.ui.on_discount.toggled.connect(lambda: self.prepare_request()) + constants = Constants() + + self.checkboxes = [] + + for groupbox, variables in [ + (self.ui.genre_group, constants.categories), + (self.ui.platform_group, constants.platforms), + (self.ui.others_group, constants.others), + (self.ui.type_group, constants.types), + ]: + for text, tag in variables: + checkbox = CheckBox(text, tag) + checkbox.activated.connect(lambda x: self.prepare_request(added_tag=x)) + checkbox.deactivated.connect(lambda x: self.prepare_request(removed_tag=x)) + groupbox.layout().addWidget(checkbox) + self.checkboxes.append(checkbox) + self.ui.reset_button.clicked.connect(self.reset_filters) + self.ui.filter_scrollarea.setMinimumWidth( + self.ui.filter_container.sizeHint().width() + + self.ui.filter_container.layout().contentsMargins().left() + + self.ui.filter_container.layout().contentsMargins().right() + + self.ui.filter_scrollarea.verticalScrollBar().sizeHint().width() + ) + + def reset_filters(self): + self.update_games_allowed = False + for cb in self.checkboxes: + cb.setChecked(False) + self.ui.none_price.setChecked(True) + + self.tags = [] + self.types = [] + self.update_games_allowed = True + + self.ui.on_discount.setChecked(False) + + def prepare_request( + self, + price: str = None, + added_tag: int = 0, + removed_tag: int = 0, + added_type: str = "", + removed_type: str = "", + ): + if not self.update_games_allowed: + return + if price is not None: + self.price = price + + if added_tag != 0: + self.tags.append(added_tag) + if removed_tag != 0 and removed_tag in self.tags: + self.tags.remove(removed_tag) + + if added_type: + self.types.append(added_type) + if removed_type and removed_type in self.types: + self.types.remove(removed_type) + if (self.types or self.price) or self.tags or self.ui.on_discount.isChecked(): + # self.free_scrollarea.setVisible(False) + self.discounts_group.setVisible(False) + else: + # self.free_scrollarea.setVisible(True) + if len(self.discounts_group.layout().children()) > 0: + self.discounts_group.setVisible(True) + + self.games_group.loading(True) + + browse_model = SearchStoreQuery( + language=self.store_api.language_code, + country=self.store_api.country_code, + count=20, + price_range=self.price, + on_sale=self.ui.on_discount.isChecked(), + ) + browse_model.tag = "|".join(self.tags) + + if self.types: + browse_model.category = "|".join(self.types) + self.store_api.browse_games(browse_model, self.show_games) + + +class CheckBox(QCheckBox): + activated = pyqtSignal(str) + deactivated = pyqtSignal(str) + + def __init__(self, text, tag): + super(CheckBox, self).__init__(text) + self.tag = tag + + self.toggled.connect(self.handle_toggle) + + def handle_toggle(self): + if self.isChecked(): + self.activated.emit(self.tag) + else: + self.deactivated.emit(self.tag) diff --git a/rare/components/tabs/store/search_results.py b/rare/components/tabs/store/search_results.py deleted file mode 100644 index 763aac3e..00000000 --- a/rare/components/tabs/store/search_results.py +++ /dev/null @@ -1,111 +0,0 @@ -from PyQt5 import QtGui -from PyQt5.QtCore import pyqtSignal, Qt -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import ( - QWidget, - QVBoxLayout, - QHBoxLayout, - QLabel, - QScrollArea, - QGroupBox, - QPushButton, - QStackedWidget, -) - -from rare.utils.extra_widgets import ImageLabel, WaitingSpinner -from rare.widgets.flow_layout import FlowLayout - - -class SearchResults(QStackedWidget): - show_info = pyqtSignal(dict) - - def __init__(self, api_core): - super(SearchResults, self).__init__() - self.search_result_widget = QWidget() - self.api_core = api_core - self.addWidget(self.search_result_widget) - self.main_layout = QVBoxLayout() - self.back_button = QPushButton(self.tr("Back")) - self.main_layout.addWidget(self.back_button) - self.main_layout.addWidget(self.back_button) - self.result_area = QScrollArea() - self.widget = QWidget() - self.result_area.setWidgetResizable(True) - self.main_layout.addWidget(self.result_area) - self.result_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - - self.result_area.setWidget(self.widget) - self.layout = FlowLayout() - self.widget.setLayout(self.layout) - - self.search_result_widget.setLayout(self.main_layout) - - self.addWidget(WaitingSpinner()) - self.setCurrentIndex(1) - - def load_results(self, text: str): - self.setCurrentIndex(1) - if text != "": - self.api_core.search_game(text, self.show_results) - - def show_results(self, results: dict): - self.widget.deleteLater() - self.widget = QWidget() - self.layout = FlowLayout() - if not results: - self.layout.addWidget(QLabel(self.tr("No results found"))) - else: - for res in results: - w = _SearchResultItem(res) - w.show_info.connect(self.show_info.emit) - self.layout.addWidget(w) - self.widget.setLayout(self.layout) - self.result_area.setWidget(self.widget) - self.setCurrentIndex(0) - - -class _SearchResultItem(QGroupBox): - res: dict - show_info = pyqtSignal(dict) - - def __init__(self, result: dict): - super(_SearchResultItem, self).__init__() - self.layout = QVBoxLayout() - self.image = ImageLabel() - for img in result["keyImages"]: - if img["type"] == "DieselStoreFrontTall": - width = 240 - self.image.update_image(img["url"], result["title"], (width, 360)) - break - else: - print("No image found") - self.layout.addWidget(self.image) - - self.res = result - self.title = QLabel(self.res["title"]) - title_font = QFont() - title_font.setPixelSize(15) - self.title.setFont(title_font) - self.title.setWordWrap(True) - self.layout.addWidget(self.title) - price = result["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - discount_price = result["price"]["totalPrice"]["fmtPrice"]["discountPrice"] - price_layout = QHBoxLayout() - price_label = QLabel(price if price != "0" else self.tr("Free")) - price_layout.addWidget(price_label) - - if price != discount_price: - font = QFont() - font.setStrikeOut(True) - price_label.setFont(font) - price_layout.addWidget(QLabel(discount_price)) - # self.discount_price = QLabel(f"{self.tr('Discount price: ')}{discount_price}") - self.layout.addLayout(price_layout) - - self.setLayout(self.layout) - - self.setFixedWidth(260) - - def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - if a0.button() == 1: - self.show_info.emit(self.res) diff --git a/rare/components/tabs/store/shop_api_core.py b/rare/components/tabs/store/shop_api_core.py deleted file mode 100644 index f1368b43..00000000 --- a/rare/components/tabs/store/shop_api_core.py +++ /dev/null @@ -1,233 +0,0 @@ -import urllib.parse -from logging import getLogger - -from PyQt5.QtCore import pyqtSignal, QObject - -from rare.components.tabs.store.constants import ( - wishlist_query, - search_query, - add_to_wishlist_query, - remove_from_wishlist_query, -) -from rare.components.tabs.store.shop_models import BrowseModel -from rare.utils.qt_requests import QtRequests - -logger = getLogger("ShopAPICore") -graphql_url = "https://www.epicgames.com/graphql" - - -class ShopApiCore(QObject): - update_wishlist = pyqtSignal() - - def __init__(self, auth_token, lc: str, cc: str): - super(ShopApiCore, self).__init__() - self.token = auth_token - self.language_code: str = lc - self.country_code: str = cc - self.locale = f"{self.language_code}-{self.country_code}" - self.manager = QtRequests(parent=self) - self.auth_manager = QtRequests(token=auth_token, parent=self) - - self.browse_active = False - self.next_browse_request = tuple(()) - - def get_free_games(self, handle_func: callable): - url = f"https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions?locale={self.language_code}&country={self.country_code}&allowCountries={self.country_code}" - - self.manager.get(url, lambda data: self._handle_free_games(data, handle_func)) - - def _handle_free_games(self, data, handle_func): - try: - results: dict = data["data"]["Catalog"]["searchStore"]["elements"] - except KeyError: - logger.error("Free games Api request failed") - handle_func(["error", "Key error"]) - return - except Exception as e: - logger.error(f"Free games Api request failed: {e}") - handle_func(["error", e]) - return - handle_func(results) - - def get_wishlist(self, handle_func): - self.auth_manager.post( - graphql_url, - lambda data: self._handle_wishlist(data, handle_func), - { - "query": wishlist_query, - "variables": { - "country": self.country_code, - "locale": f"{self.language_code}-{self.country_code}", - }, - }, - ) - - def _handle_wishlist(self, data, handle_func): - try: - results: list = data["data"]["Wishlist"]["wishlistItems"]["elements"] - except KeyError: - logger.error("Free games Api request failed") - handle_func(["error", "Key error"]) - return - except Exception as e: - logger.error(f"Free games Api request failed: {e}") - handle_func(["error", e]) - return - - handle_func(results) - - def search_game(self, name, handle_func): - payload = { - "query": search_query, - "variables": { - "category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 10, - "country": self.country_code, - "keywords": name, - "locale": self.locale, - "sortDir": "DESC", - "allowCountries": self.country_code, - "start": 0, - "tag": "", - "withMapping": False, - "withPrice": True, - }, - } - - self.manager.post( - graphql_url, lambda data: self._handle_search(data, handle_func), payload, - ) - - def _handle_search(self, data, handle_func): - try: - handle_func(data["data"]["Catalog"]["searchStore"]["elements"]) - except KeyError as e: - logger.error(str(e)) - handle_func([]) - except Exception as e: - logger.error(f"Search Api request failed: {e}") - handle_func([]) - return - - def browse_games(self, browse_model: BrowseModel, handle_func): - if self.browse_active: - self.next_browse_request = (browse_model, handle_func) - return - self.browse_active = True - url = "https://www.epicgames.com/graphql?operationName=searchStoreQuery&variables={}&extensions={}" - variables = urllib.parse.quote_plus(str( - dict(browse_model.__dict__)) - ) - extensions = urllib.parse.quote_plus(str( - dict( - persistedQuery=dict( - version=1, - sha256Hash="6e7c4dd0177150eb9a47d624be221929582df8648e7ec271c821838ff4ee148e" - ) - ) - ) - ) - - for old, new in [ - ("%26", "&"), - ("%27", "%22"), - ("+", ""), - ("%3A", ":"), - ("%2C", ","), - ("%5B", "["), - ("%5D", "]"), - ("True", "true"), - ]: - variables = variables.replace(old, new) - extensions = extensions.replace(old, new) - - url = url.format(variables, extensions) - self.auth_manager.get( - url, lambda data: self._handle_browse_games(data, handle_func) - ) - - def _handle_browse_games(self, data, handle_func): - self.browse_active = False - if data is None: - data = {} - if not self.next_browse_request: - - try: - handle_func(data["data"]["Catalog"]["searchStore"]["elements"]) - except KeyError as e: - logger.error(str(e)) - handle_func([]) - except Exception as e: - logger.error(f"Browse games Api request failed: {e}") - handle_func([]) - return - else: - self.browse_games(*self.next_browse_request) # pylint: disable=E1120 - self.next_browse_request = tuple(()) - - def get_game(self, slug: str, is_bundle: bool, handle_func): - url = f"https://store-content.ak.epicgames.com/api/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" - self.manager.get(url, lambda data: self._handle_get_game(data, handle_func)) - - def _handle_get_game(self, data, handle_func): - try: - handle_func(data) - except Exception as e: - logger.error(str(e)) - handle_func({}) - - # needs a captcha - def add_to_wishlist(self, namespace, offer_id, handle_func: callable): - payload = { - "variables": { - "offerId": offer_id, - "namespace": namespace, - "country": self.country_code, - "locale": self.locale, - }, - "query": add_to_wishlist_query, - } - self.auth_manager.post( - graphql_url, - lambda data: self._handle_add_to_wishlist(data, handle_func), - payload, - ) - - def _handle_add_to_wishlist(self, data, handle_func): - try: - data = data["data"]["Wishlist"]["addToWishlist"] - if data["success"]: - handle_func(True) - else: - handle_func(False) - except Exception as e: - logger.error(str(e)) - handle_func(False) - self.update_wishlist.emit() - - def remove_from_wishlist(self, namespace, offer_id, handle_func: callable): - payload = { - "variables": { - "offerId": offer_id, - "namespace": namespace, - "operation": "REMOVE", - }, - "query": remove_from_wishlist_query, - } - self.auth_manager.post( - graphql_url, - lambda data: self._handle_remove_from_wishlist(data, handle_func), - payload, - ) - - def _handle_remove_from_wishlist(self, data, handle_func): - try: - data = data["data"]["Wishlist"]["removeFromWishlist"] - if data["success"]: - handle_func(True) - else: - handle_func(False) - except Exception as e: - logger.error(str(e)) - handle_func(False) - self.update_wishlist.emit() diff --git a/rare/components/tabs/store/shop_models.py b/rare/components/tabs/store/shop_models.py deleted file mode 100644 index b850a71f..00000000 --- a/rare/components/tabs/store/shop_models.py +++ /dev/null @@ -1,184 +0,0 @@ -import datetime -from dataclasses import dataclass - - -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 = "", - image_urls: ImageUrlModel = None, - social_links: dict = None, - langs: list = 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.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 - self.reqs = reqs - self.publisher = publisher - self.developer = developer - self.price = original_price - self.discount_price = discount_price - self.tags = tags - 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() - tmp.title = search_data.get("title", "Fail") - tmp.image_urls = ImageUrlModel.from_json(search_data["keyImages"]) - 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", "") - 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.tags = [ - i.replace("_", " ").capitalize() - for i in api_data["data"]["meta"].get("tags", []) - ] - tmp.namespace = search_data["namespace"] - tmp.offer_id = search_data["id"] - - 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 - - @property - def __dict__(self): - payload = { - "allowCountries": self.country_code, - "category": self.category, - "count": self.count, - "country": self.country_code, - "keywords": self.keywords, - "locale": self.language_code, - "priceRange": self.price, - "releaseDate": self.date, - "sortBy": "releaseDate", - "sortDir": self.sortDir, - "start": self.start, - "tag": self.tag, - "withPrice": self.withPrice, - } - if self.price == "free": - payload["freeGame"] = True - payload.pop("priceRange") - elif self.price.startswith(""): - payload["priceRange"] = self.price.replace("", "") - if self.onSale: - payload["onSale"] = True - - if self.price: - payload["effectiveDate"] = self.date - else: - payload.pop("priceRange") - - return payload diff --git a/rare/components/tabs/store/shop_widget.py b/rare/components/tabs/store/shop_widget.py deleted file mode 100644 index af5a02ce..00000000 --- a/rare/components/tabs/store/shop_widget.py +++ /dev/null @@ -1,367 +0,0 @@ -import datetime -import logging - -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import ( - QGroupBox, - QScrollArea, - QCheckBox, - QLabel, - QPushButton, - QHBoxLayout, -) - -from legendary.core import LegendaryCore -from rare.ui.components.tabs.store.store import Ui_ShopWidget -from rare.utils.extra_widgets import WaitingSpinner, ButtonLineEdit -from rare.widgets.flow_layout import FlowLayout -from .constants import Constants -from .game_widgets import GameWidget -from .shop_api_core import ShopApiCore -from .shop_models import BrowseModel - -logger = logging.getLogger("Shop") - - -# noinspection PyAttributeOutsideInit,PyBroadException -class ShopWidget(QScrollArea, Ui_ShopWidget): - show_info = pyqtSignal(str) - show_game = pyqtSignal(dict) - free_game_widgets = [] - active_search_request = False - next_search = "" - wishlist: list = [] - - def __init__(self, path, core: LegendaryCore, shop_api: ShopApiCore): - super(ShopWidget, self).__init__() - self.setWidgetResizable(True) - self.setupUi(self) - self.path = path - self.core = core - self.api_core = shop_api - self.price = "" - self.tags = [] - self.types = [] - self.update_games_allowed = True - self.free_widget.setLayout(FlowLayout()) - - self.free_stack.addWidget(WaitingSpinner()) - self.free_stack.setCurrentIndex(1) - - self.discount_widget.setLayout(FlowLayout()) - self.discount_stack.addWidget(WaitingSpinner()) - self.discount_stack.setCurrentIndex(1) - - self.game_widget.setLayout(FlowLayout()) - self.game_stack.addWidget(WaitingSpinner()) - self.game_stack.setCurrentIndex(1) - - self.search_bar = ButtonLineEdit( - "fa.search", placeholder_text=self.tr("Search Games") - ) - self.layout().insertWidget(0, self.search_bar) - - # self.search_bar.textChanged.connect(self.search_games) - - self.search_bar.returnPressed.connect(self.show_search_results) - self.search_bar.buttonClicked.connect(self.show_search_results) - - self.init_filter() - - self.search_bar.setHidden(True) - self.filter_gb.setHidden(True) - self.filter_game_gb.setHidden(True) - - # self.search_bar.textChanged.connect(self.load_completer) - - def load(self): - # load free games - self.api_core.get_free_games(self.add_free_games) - # load wishlist - self.api_core.get_wishlist(self.add_wishlist_items) - # load browse games - self.prepare_request() - - def update_wishlist(self): - self.api_core.get_wishlist(self.add_wishlist_items) - - def add_wishlist_items(self, wishlist): - for i in range(self.discount_widget.layout().count()): - item = self.discount_widget.layout().itemAt(i) - if item: - item.widget().deleteLater() - - if wishlist and wishlist[0] == "error": - self.discount_widget.layout().addWidget( - QLabel(self.tr("Failed to get wishlist: {}").format(wishlist[1])) - ) - btn = QPushButton(self.tr("Reload")) - self.discount_widget.layout().addWidget(btn) - btn.clicked.connect( - lambda: self.api_core.get_wishlist(self.add_wishlist_items) - ) - self.discount_stack.setCurrentIndex(0) - return - - discounts = 0 - for game in wishlist: - if not game: - continue - try: - if game["offer"]["price"]["totalPrice"]["discount"] > 0: - w = GameWidget(self.path, game["offer"]) - w.show_info.connect(self.show_game.emit) - self.discount_widget.layout().addWidget(w) - discounts += 1 - except Exception as e: - logger.warning(f"{game} {e}") - continue - self.discounts_gb.setVisible(discounts > 0) - self.discount_stack.setCurrentIndex(0) - # fix widget overlay - self.discount_widget.layout().update() - - def add_free_games(self, free_games: list): - for i in range(self.free_widget.layout().count()): - item = self.free_widget.layout().itemAt(i) - if item: - item.widget().deleteLater() - - if free_games and free_games[0] == "error": - self.free_widget.layout().addWidget( - QLabel(self.tr("Failed to fetch free games: {}").format(free_games[1])) - ) - btn = QPushButton(self.tr("Reload")) - self.free_widget.layout().addWidget(btn) - btn.clicked.connect( - lambda: self.api_core.get_free_games(self.add_free_games) - ) - self.free_stack.setCurrentIndex(0) - return - - self.free_games_now = QGroupBox(self.tr("Now Free")) - self.free_games_now.setLayout(QHBoxLayout()) - self.free_widget.layout().addWidget(self.free_games_now) - - self.coming_free_games = QGroupBox(self.tr("Free Games next week")) - self.coming_free_games.setLayout(QHBoxLayout()) - self.free_widget.layout().addWidget(self.coming_free_games) - - date = datetime.datetime.now() - free_games_now = [] - coming_free_games = [] - for game in free_games: - try: - if ( - game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] == "0" - and game["price"]["totalPrice"]["fmtPrice"]["originalPrice"] - != game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] - ): - free_games_now.append(game) - continue - - if game["title"] == "Mystery Game": - coming_free_games.append(game) - continue - except KeyError as e: - logger.warning(str(e)) - - try: - # parse datetime to check if game is next week or now - try: - start_date = datetime.datetime.strptime( - game["promotions"]["upcomingPromotionalOffers"][0][ - "promotionalOffers" - ][0]["startDate"], - "%Y-%m-%dT%H:%M:%S.%fZ", - ) - except Exception: - try: - start_date = datetime.datetime.strptime( - game["promotions"]["promotionalOffers"][0][ - "promotionalOffers" - ][0]["startDate"], - "%Y-%m-%dT%H:%M:%S.%fZ", - ) - except Exception as e: - - continue - - except TypeError: - print("type error") - continue - - if start_date > date: - coming_free_games.append(game) - # free games now - now_free = 0 - for free_game in free_games_now: - w = GameWidget(self.path, free_game) - w.show_info.connect(self.show_game.emit) - self.free_games_now.layout().addWidget(w) - self.free_game_widgets.append(w) - now_free += 1 - if now_free == 0: - self.free_games_now.layout().addWidget( - QLabel(self.tr("Could not find current free game")) - ) - - # free games next week - for free_game in coming_free_games: - w = GameWidget(self.path, free_game) - if free_game["title"] != "Mystery Game": - w.show_info.connect(self.show_game.emit) - self.coming_free_games.layout().addWidget(w) - # self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300)) - self.free_stack.setCurrentIndex(0) - - def show_search_results(self): - if self.search_bar.text(): - self.show_info.emit(self.search_bar.text()) - - def init_filter(self): - self.none_price.toggled.connect( - lambda: self.prepare_request("") if self.none_price.isChecked() else None - ) - self.free_button.toggled.connect( - lambda: self.prepare_request("free") - if self.free_button.isChecked() - else None - ) - self.under10.toggled.connect( - lambda: self.prepare_request("[0, 1000)") - if self.under10.isChecked() - else None - ) - self.under20.toggled.connect( - lambda: self.prepare_request("[0, 2000)") - if self.under20.isChecked() - else None - ) - self.under30.toggled.connect( - lambda: self.prepare_request("[0, 3000)") - if self.under30.isChecked() - else None - ) - self.above.toggled.connect( - lambda: self.prepare_request("[1499,]") - if self.above.isChecked() - else None - ) - # self.on_discount.toggled.connect(lambda: self.prepare_request("sale") if self.on_discount.isChecked() else None) - self.on_discount.toggled.connect(lambda: self.prepare_request()) - constants = Constants() - - self.checkboxes = [] - - for groupbox, variables in [ - (self.genre_gb, constants.categories), - (self.platform_gb, constants.platforms), - (self.others_gb, constants.others), - (self.type_gb, constants.types), - ]: - - for text, tag in variables: - checkbox = CheckBox(text, tag) - checkbox.activated.connect(lambda x: self.prepare_request(added_tag=x)) - checkbox.deactivated.connect( - lambda x: self.prepare_request(removed_tag=x) - ) - groupbox.layout().addWidget(checkbox) - self.checkboxes.append(checkbox) - self.reset_button.clicked.connect(self.reset_filters) - - def reset_filters(self): - self.update_games_allowed = False - for cb in self.checkboxes: - cb.setChecked(False) - self.none_price.setChecked(True) - - self.tags = [] - self.types = [] - self.update_games_allowed = True - self.prepare_request("") - - self.on_discount.setChecked(False) - - def prepare_request( - self, - price: str = None, - added_tag: int = 0, - removed_tag: int = 0, - added_type: str = "", - removed_type: str = "", - ): - if not self.update_games_allowed: - return - if price is not None: - self.price = price - - if added_tag != 0: - self.tags.append(added_tag) - if removed_tag != 0 and removed_tag in self.tags: - self.tags.remove(removed_tag) - - if added_type: - self.types.append(added_type) - if removed_type and removed_type in self.types: - self.types.remove(removed_type) - if (self.types or self.price) or self.tags or self.on_discount.isChecked(): - self.free_game_group_box.setVisible(False) - self.discounts_gb.setVisible(False) - else: - self.free_game_group_box.setVisible(True) - if len(self.discounts_gb.layout().children()) > 0: - self.discounts_gb.setVisible(True) - - self.game_stack.setCurrentIndex(1) - - browse_model = BrowseModel( - language_code=self.core.language_code, - country_code=self.core.country_code, - count=20, - price=self.price, - onSale=self.on_discount.isChecked(), - ) - browse_model.tag = "|".join(self.tags) - - if self.types: - browse_model.category = "|".join(self.types) - self.api_core.browse_games(browse_model, self.show_games) - - def show_games(self, data): - for item in ( - self.game_widget.layout().itemAt(i) - for i in range(self.game_widget.layout().count()) - ): - item.widget().deleteLater() - if data: - for game in data: - w = GameWidget(self.path, game, 275) - self.game_widget.layout().addWidget(w) - w.show_info.connect(self.show_game.emit) - - else: - self.game_widget.layout().addWidget( - QLabel(self.tr("Could not get games matching the filter")) - ) - self.game_stack.setCurrentIndex(0) - - self.game_widget.layout().update() - - -class CheckBox(QCheckBox): - activated = pyqtSignal(str) - deactivated = pyqtSignal(str) - - def __init__(self, text, tag): - super(CheckBox, self).__init__(text) - self.tag = tag - - self.toggled.connect(self.handle_toggle) - - def handle_toggle(self): - if self.isChecked(): - self.activated.emit(self.tag) - else: - self.deactivated.emit(self.tag) diff --git a/rare/components/tabs/store/store_api.py b/rare/components/tabs/store/store_api.py new file mode 100644 index 00000000..bfdff865 --- /dev/null +++ b/rare/components/tabs/store/store_api.py @@ -0,0 +1,258 @@ +from logging import getLogger +from typing import List, Callable + +from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtWidgets import QApplication + +from rare.components.tabs.store.constants import ( + wishlist_query, + search_query, + wishlist_add_query, + wishlist_remove_query, +) +from rare.utils.paths import cache_dir +from rare.utils.qt_requests import QtRequests +from .api.models.query import SearchStoreQuery +from .api.models.diesel import DieselProduct +from .api.models.response import ( + ResponseModel, + CatalogOfferModel, +) + +logger = getLogger("StoreAPI") +graphql_url = "https://graphql.epicgames.com/graphql" + + +DEBUG: Callable[[], bool] = lambda: "--debug" in QApplication.arguments() + + +class StoreAPI(QObject): + update_wishlist = pyqtSignal() + + def __init__(self, token, language: str, country: str, installed): + super(StoreAPI, self).__init__() + self.token = token + self.language_code: str = language + self.country_code: str = country + self.locale = f"{self.language_code}-{self.country_code}" + self.locale = "en-US" + self.manager = QtRequests(parent=self) + self.authed_manager = QtRequests(token=token, parent=self) + self.cached_manager = QtRequests(cache=str(cache_dir().joinpath("store")), parent=self) + + self.installed = installed + + self.browse_active = False + self.next_browse_request = tuple(()) + + def get_free(self, handle_func: callable): + url = "https://store-site-backend-static-ipv4.ak.epicgames.com/freeGamesPromotions" + params = { + "locale": self.locale, + "country": self.country_code, + "allowCountries": self.country_code, + } + self.manager.get(url, lambda data: self.__handle_free_games(data, handle_func), params=params) + + @staticmethod + def __handle_free_games(data, handle_func): + try: + response = ResponseModel.from_dict(data) + results: List[CatalogOfferModel] = response.data.catalog.searchStore.elements + handle_func(results) + except KeyError as e: + if DEBUG(): + raise e + logger.error("Free games Api request failed") + handle_func(["error", "Key error"]) + return + except Exception as e: + if DEBUG(): + raise e + logger.error(f"Free games Api request failed: {e}") + handle_func(["error", e]) + return + + def get_wishlist(self, handle_func): + self.authed_manager.post( + graphql_url, + lambda data: self.__handle_wishlist(data, handle_func), + { + "query": wishlist_query, + "variables": { + "country": self.country_code, + "locale": self.locale, + "withPrice": True, + }, + }, + ) + + @staticmethod + def __handle_wishlist(data, handle_func): + try: + response = ResponseModel.from_dict(data) + if response.errors: + logger.error(response.errors) + handle_func(response.data.wishlist.wishlistItems.elements) + except KeyError as e: + if DEBUG(): + raise e + logger.error("Free games Api request failed") + handle_func(["error", "Key error"]) + return + except Exception as e: + if DEBUG(): + raise e + logger.error(f"Free games Api request failed: {e}") + handle_func(["error", e]) + return + + def search_game(self, name, handler): + payload = { + "query": search_query, + "variables": { + "category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 20, + "country": self.country_code, + "keywords": name, + "locale": self.locale, + "sortDir": "DESC", + "allowCountries": self.country_code, + "start": 0, + "tag": "", + "withMapping": False, + "withPrice": True, + }, + } + + self.manager.post(graphql_url, lambda data: self.__handle_search(data, handler), payload) + + @staticmethod + def __handle_search(data, handler): + try: + response = ResponseModel.from_dict(data) + handler(response.data.catalog.searchStore.elements) + except KeyError as e: + logger.error(str(e)) + if DEBUG(): + raise e + handler([]) + except Exception as e: + logger.error(f"Search Api request failed: {e}") + if DEBUG(): + raise e + handler([]) + return + + def browse_games(self, browse_model: SearchStoreQuery, handle_func): + if self.browse_active: + self.next_browse_request = (browse_model, handle_func) + return + self.browse_active = True + payload = { + "query": search_query, + "variables": browse_model.to_dict() + } + self.manager.post(graphql_url, lambda data: self.__handle_browse_games(data, handle_func), payload) + + def __handle_browse_games(self, data, handle_func): + self.browse_active = False + if data is None: + data = {} + if not self.next_browse_request: + try: + response = ResponseModel.from_dict(data) + handle_func(response.data.catalog.searchStore.elements) + except KeyError as e: + if DEBUG(): + raise e + logger.error(str(e)) + handle_func([]) + except Exception as e: + if DEBUG(): + raise e + logger.error(f"Browse games Api request failed: {e}") + handle_func([]) + return + else: + self.browse_games(*self.next_browse_request) # pylint: disable=E1120 + self.next_browse_request = tuple(()) + + # def get_game_config_graphql(self, namespace: str, handle_func): + # payload = { + # "query": config_query, + # "variables": { + # "namespace": namespace + # } + # } + + def __make_graphql_query(self): + pass + + def __make_api_query(self): + pass + + def get_game_config_cms(self, slug: str, is_bundle: bool, handle_func): + url = "https://store-content.ak.epicgames.com/api" + url += f"/{self.locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" + self.manager.get(url, lambda data: self.__handle_get_game(data, handle_func)) + + @staticmethod + def __handle_get_game(data, handle_func): + try: + product = DieselProduct.from_dict(data) + handle_func(product) + except Exception as e: + if DEBUG(): + raise e + logger.error(str(e)) + # handle_func({}) + + # needs a captcha + def add_to_wishlist(self, namespace, offer_id, handle_func: callable): + payload = { + "query": wishlist_add_query, + "variables": { + "offerId": offer_id, + "namespace": namespace, + "country": self.country_code, + "locale": self.locale, + }, + } + 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): + try: + response = ResponseModel.from_dict(data) + data = response.data.wishlist.addToWishlist + handle_func(data.success) + except Exception as e: + if DEBUG(): + raise e + logger.error(str(e)) + handle_func(False) + self.update_wishlist.emit() + + def remove_from_wishlist(self, namespace, offer_id, handle_func: callable): + payload = { + "query": wishlist_remove_query, + "variables": { + "offerId": offer_id, + "namespace": namespace, + "operation": "REMOVE", + }, + } + self.authed_manager.post(graphql_url, lambda data: self._handle_remove_from_wishlist(data, handle_func), + payload) + + def _handle_remove_from_wishlist(self, data, handle_func): + try: + response = ResponseModel.from_dict(data) + data = response.data.wishlist.removeFromWishlist + handle_func(data.success) + except Exception as e: + if DEBUG(): + raise e + logger.error(str(e)) + handle_func(False) + self.update_wishlist.emit() diff --git a/rare/components/tabs/store/widgets/__init__.py b/rare/components/tabs/store/widgets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/components/tabs/store/widgets/details.py b/rare/components/tabs/store/widgets/details.py new file mode 100644 index 00000000..309856bc --- /dev/null +++ b/rare/components/tabs/store/widgets/details.py @@ -0,0 +1,269 @@ +import logging +from typing import List, Dict + +from PyQt5.QtCore import Qt, QUrl, pyqtSignal +from PyQt5.QtGui import QFont, QDesktopServices, QKeyEvent +from PyQt5.QtWidgets import ( + QWidget, + QLabel, + QPushButton, + QGridLayout, + QSizePolicy, +) + +from rare.components.tabs.store.api.models.diesel import DieselProduct, DieselProductDetail, DieselSystemDetail +from rare.components.tabs.store.api.models.response import CatalogOfferModel +from rare.components.tabs.store.store_api import StoreAPI +from rare.models.image import ImageSize +from rare.ui.components.tabs.store.details import Ui_DetailsWidget +from rare.utils.misc import qta_icon +from rare.widgets.elide_label import ElideLabel +from rare.widgets.side_tab import SideTabWidget, SideTabContents +from .image import LoadingImageWidget + +logger = logging.getLogger("StoreDetails") + + +class DetailsWidget(QWidget, SideTabContents): + back_clicked: pyqtSignal = pyqtSignal() + + # TODO Design + def __init__(self, installed: List, store_api: StoreAPI, parent=None): + super(DetailsWidget, self).__init__(parent=parent) + self.implements_scrollarea = True + + self.ui = Ui_DetailsWidget() + self.ui.setupUi(self) + self.ui.main_layout.setContentsMargins(0, 0, 3, 0) + + self.store_api = store_api + self.installed = installed + self.catalog_offer: CatalogOfferModel = None + + self.image = LoadingImageWidget(store_api.cached_manager, self) + self.image.setFixedSize(ImageSize.Display) + self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop) + self.ui.left_layout.setAlignment(Qt.AlignTop) + + self.ui.wishlist_button.clicked.connect(self.add_to_wishlist) + self.ui.store_button.clicked.connect(self.button_clicked) + self.ui.wishlist_button.setVisible(True) + self.in_wishlist = False + self.wishlist = [] + + self.requirements_tabs = SideTabWidget(parent=self.ui.requirements_frame) + self.requirements_tabs.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.ui.requirements_layout.addWidget(self.requirements_tabs) + + self.ui.back_button.setIcon(qta_icon("fa.chevron-left")) + self.ui.back_button.clicked.connect(self.back_clicked) + + self.setDisabled(False) + + def handle_wishlist_update(self, wishlist: List[CatalogOfferModel]): + if wishlist and wishlist[0] == "error": + return + self.wishlist = [game.id for game in wishlist] + if self.id_str in self.wishlist: + self.in_wishlist = True + self.ui.wishlist_button.setText(self.tr("Remove from Wishlist")) + else: + self.in_wishlist = False + + def update_game(self, offer: CatalogOfferModel): + self.ui.title.setText(offer.title) + self.title_str = offer.title + self.id_str = offer.id + self.store_api.get_wishlist(self.handle_wishlist_update) + + # lk: delete tabs in reverse order because indices are updated on deletion + while self.requirements_tabs.count(): + self.requirements_tabs.widget(0).deleteLater() + self.requirements_tabs.removeTab(0) + self.requirements_tabs.clear() + + slug = offer.productSlug + if not slug: + for mapping in offer.offerMappings: + if mapping["pageType"] == "productHome": + slug = mapping["pageSlug"] + break + else: + logger.error("Could not get page information") + slug = "" + if "/home" in slug: + slug = slug.replace("/home", "") + self.slug = slug + + if offer.namespace in self.installed: + self.ui.store_button.setText(self.tr("Show Game on Epic Page")) + self.ui.status.setVisible(True) + else: + self.ui.store_button.setText(self.tr("Buy Game in Epic Games Store")) + self.ui.status.setVisible(False) + + self.ui.original_price.setText(self.tr("Loading")) + # self.title.setText(self.tr("Loading")) + # self.image.setPixmap(QPixmap()) + is_bundle = False + for i in offer.categories: + if "bundles" in i.get("path", ""): + is_bundle = True + + # init API request + if slug: + self.store_api.get_game_config_cms(offer.productSlug, is_bundle, self.data_received) + # else: + # self.data_received({}) + self.catalog_offer = offer + + def add_to_wishlist(self): + if not self.in_wishlist: + self.store_api.add_to_wishlist( + self.catalog_offer.namespace, + self.catalog_offer.id, + lambda success: self.ui.wishlist_button.setText(self.tr("Remove from wishlist")) + if success + else self.ui.wishlist_button.setText("Something went wrong") + ) + else: + self.store_api.remove_from_wishlist( + self.catalog_offer.namespace, + self.catalog_offer.id, + lambda success: self.ui.wishlist_button.setText(self.tr("Add to wishlist")) + if success + else self.ui.wishlist_button.setText("Something went wrong"), + ) + + def data_received(self, product: DieselProduct): + try: + if product.pages: + product_data: DieselProductDetail = product.pages[0].data + else: + product_data: DieselProductDetail = product.data + except Exception as e: + raise e + logger.error(str(e)) + + self.ui.original_price.setFont(self.font()) + price = self.catalog_offer.price.totalPrice.fmtPrice["originalPrice"] + discount_price = self.catalog_offer.price.totalPrice.fmtPrice["discountPrice"] + if price == "0" or price == 0: + self.ui.original_price.setText(self.tr("Free")) + else: + self.ui.original_price.setText(price) + if price != discount_price: + font = self.font() + font.setStrikeOut(True) + self.ui.original_price.setFont(font) + self.ui.discount_price.setText( + discount_price + if discount_price != "0" + else self.tr("Free") + ) + self.ui.discount_price.setVisible(True) + else: + self.ui.discount_price.setVisible(False) + + requirements = product_data.requirements + if requirements and requirements.systems: + for system in requirements.systems: + req_widget = RequirementsWidget(system, self.requirements_tabs) + self.requirements_tabs.addTab(req_widget, system.systemType) + self.ui.requirements_frame.setVisible(True) + else: + self.ui.requirements_frame.setVisible(False) + + key_images = self.catalog_offer.keyImages + img_url = key_images.for_dimensions(self.image.size().width(), self.image.size().height()) + self.image.fetchPixmap(img_url.url) + + # self.image_stack.setCurrentIndex(0) + about = product_data.about + self.ui.description_label.setMarkdown(about.desciption) + self.ui.developer.setText(about.developerAttribution) + # try: + # if isinstance(aboudeveloper, list): + # self.ui.dev.setText(", ".join(self.game.developer)) + # else: + # self.ui.dev.setText(self.game.developer) + # except KeyError: + # pass + tags = product_data.unmapped["meta"].get("tags", []) + self.ui.tags.setText(", ".join(tags)) + + # clear Layout + for b in self.ui.social_links.findChildren(SocialButton, options=Qt.FindDirectChildrenOnly): + self.ui.social_links_layout.removeWidget(b) + b.deleteLater() + + links = product_data.socialLinks + link_count = 0 + for name, url in links.items(): + if name == "_type": + continue + name = name.replace("link", "").lower() + if name == "homepage": + icn = qta_icon("mdi.web", "fa.search", scale_factor=1.5) + else: + try: + icn = qta_icon(f"mdi.{name}", f"fa.{name}", scale_factor=1.5) + except Exception as e: + logger.error(str(e)) + continue + + button = SocialButton(icn, url, parent=self.ui.social_links) + self.ui.social_links_layout.addWidget(button) + link_count += 1 + + self.ui.social_links.setEnabled(bool(link_count)) + + self.setEnabled(True) + + # def add_wishlist_items(self, wishlist: List[CatalogGameModel]): + # wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"] + # for game in wishlist: + # self.wishlist.append(game["offer"]["title"]) + + def button_clicked(self): + QDesktopServices.openUrl(QUrl(f"https://www.epicgames.com/store/{self.store_api.language_code}/p/{self.slug}")) + + def keyPressEvent(self, a0: QKeyEvent): + if a0.key() == Qt.Key_Escape: + self.back_clicked.emit() + + +class SocialButton(QPushButton): + def __init__(self, icn, url, parent=None): + super(SocialButton, self).__init__(icn, "", parent=parent) + self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.url = url + self.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url))) + self.setToolTip(url) + + +class RequirementsWidget(QWidget, SideTabContents): + def __init__(self, system: DieselSystemDetail, parent=None): + super().__init__(parent=parent) + self.implements_scrollarea = True + + bold_font = self.font() + bold_font.setBold(True) + + req_layout = QGridLayout(self) + min_label = QLabel(self.tr("Minimum"), parent=self) + min_label.setFont(bold_font) + rec_label = QLabel(self.tr("Recommend"), parent=self) + rec_label.setFont(bold_font) + req_layout.addWidget(min_label, 0, 1) + req_layout.addWidget(rec_label, 0, 2) + req_layout.setColumnStretch(1, 2) + req_layout.setColumnStretch(2, 2) + for i, detail in enumerate(system.details): + req_layout.addWidget(QLabel(detail.title, parent=self), i + 1, 0) + min_label = ElideLabel(detail.minimum, parent=self) + req_layout.addWidget(min_label, i + 1, 1) + rec_label = ElideLabel(detail.recommended, parent=self) + req_layout.addWidget(rec_label, i + 1, 2) + req_layout.setAlignment(Qt.AlignTop) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) diff --git a/rare/components/tabs/store/widgets/groups.py b/rare/components/tabs/store/widgets/groups.py new file mode 100644 index 00000000..3675ecc7 --- /dev/null +++ b/rare/components/tabs/store/widgets/groups.py @@ -0,0 +1,18 @@ +from PyQt5.QtWidgets import QGroupBox, QLayout + +from rare.widgets.loading_widget import LoadingWidget + + +class StoreGroup(QGroupBox): + def __init__(self, title: str, layout: type[QLayout], parent=None): + super().__init__(parent=parent) + self.setTitle(title) + self.main_layout = layout(self) + self.loading_widget = LoadingWidget(autostart=True, parent=self) + + def loading(self, state: bool) -> None: + if state: + self.loading_widget.start() + else: + self.loading_widget.stop() + diff --git a/rare/components/tabs/store/widgets/image.py b/rare/components/tabs/store/widgets/image.py new file mode 100644 index 00000000..22bd0140 --- /dev/null +++ b/rare/components/tabs/store/widgets/image.py @@ -0,0 +1,112 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import ( + QPixmap, + QImage, +) +from PyQt5.QtWidgets import ( + QWidget, + QVBoxLayout, + QSpacerItem, + QSizePolicy, + QHBoxLayout, + QLabel, +) + +from rare.utils.qt_requests import QtRequests +from rare.widgets.image_widget import ImageWidget +from rare.widgets.loading_widget import LoadingWidget + + +class IconWidget(object): + def __init__(self): + self.mini_widget: QWidget = None + self.title_label: QLabel = None + self.developer_label: QLabel = None + self.price_label: QLabel = None + self.discount_label: QLabel = None + + def setupUi(self, widget: QWidget): + # on-hover popup + self.mini_widget = QWidget(parent=widget) + self.mini_widget.setObjectName(f"{type(self).__name__}MiniWidget") + self.mini_widget.setFixedHeight(int(widget.height() // 3)) + + # game title + self.title_label = QLabel(parent=self.mini_widget) + self.title_label.setObjectName(f"{type(self).__name__}TitleLabel") + self.title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.title_label.setAlignment(Qt.AlignTop) + self.title_label.setAutoFillBackground(False) + self.title_label.setWordWrap(True) + + # information below title + self.developer_label = QLabel(parent=self.mini_widget) + self.developer_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.developer_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.developer_label.setAutoFillBackground(False) + + self.price_label = QLabel(parent=self.mini_widget) + self.price_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.price_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.price_label.setAutoFillBackground(False) + + self.discount_label = QLabel(parent=self.mini_widget) + self.discount_label.setObjectName(f"{type(self).__name__}TooltipLabel") + self.discount_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + self.discount_label.setAutoFillBackground(False) + + # Create layouts + # layout on top of the image, holds the status label, a spacer item and the mini widget + image_layout = QVBoxLayout() + image_layout.setContentsMargins(2, 2, 2, 2) + + # layout for the mini widget, holds the top row and the info label + mini_layout = QVBoxLayout() + mini_layout.setSpacing(0) + + # layout for the top row, holds the title and the launch button + row_layout = QHBoxLayout() + row_layout.setSpacing(6) + row_layout.setAlignment(Qt.AlignBottom) + + # Layout the widgets + # (from inner to outer) + row_layout.addWidget(self.developer_label, stretch=2) + row_layout.addWidget(self.price_label) + row_layout.addWidget(self.discount_label) + mini_layout.addWidget(self.title_label) + mini_layout.addLayout(row_layout) + self.mini_widget.setLayout(mini_layout) + + image_layout.addSpacerItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding)) + image_layout.addWidget(self.mini_widget) + widget.setLayout(image_layout) + + +class LoadingImageWidget(ImageWidget): + def __init__(self, manager: QtRequests, parent=None): + super(LoadingImageWidget, self).__init__(parent=parent) + self.ui = IconWidget() + self.spinner = LoadingWidget(parent=self) + self.spinner.setVisible(False) + self.manager = manager + + def fetchPixmap(self, url): + self.setPixmap(QPixmap()) + self.spinner.setFixedSize(self._image_size.size) + self.spinner.start() + self.manager.get(url, self.__on_image_ready, params={ + "resize": 1, + "w": self._image_size.base.size.width(), + "h": self._image_size.base.size.height(), + }) + + def __on_image_ready(self, data): + cover = QImage() + cover.loadFromData(data) + # cover = cover.scaled(self._image_size.size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + cover.setDevicePixelRatio(self._image_size.base.pixel_ratio) + cover = cover.convertToFormat(QImage.Format_ARGB32_Premultiplied) + cover = QPixmap(cover) + self.setPixmap(cover) + self.spinner.stop() diff --git a/rare/components/tabs/store/widgets/items.py b/rare/components/tabs/store/widgets/items.py new file mode 100644 index 00000000..94a022ea --- /dev/null +++ b/rare/components/tabs/store/widgets/items.py @@ -0,0 +1,136 @@ +import logging + +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtGui import QMouseEvent +from PyQt5.QtWidgets import QPushButton + +from rare.components.tabs.store.api.models.response import CatalogOfferModel +from rare.models.image import ImageSize +from rare.utils.misc import qta_icon +from rare.utils.qt_requests import QtRequests +from .image import LoadingImageWidget + +logger = logging.getLogger("StoreWidgets") + + +class ItemWidget(LoadingImageWidget): + show_details = pyqtSignal(CatalogOfferModel) + + def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None): + super(ItemWidget, self).__init__(manager, parent=parent) + self.catalog_game = catalog_game + + def mousePressEvent(self, a0: QMouseEvent) -> None: + if a0.button() == Qt.LeftButton: + a0.accept() + self.show_details.emit(self.catalog_game) + if a0.button() == Qt.RightButton: + a0.accept() + + +class StoreItemWidget(ItemWidget): + def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel = None, parent=None): + super(StoreItemWidget, self).__init__(manager, catalog_game, parent=parent) + self.setFixedSize(ImageSize.DisplayWide) + self.ui.setupUi(self) + if catalog_game: + self.init_ui(catalog_game) + + def init_ui(self, game: CatalogOfferModel): + if not game: + self.ui.title_label.setText(self.tr("An error occurred")) + return + + self.ui.title_label.setText(game.title) + for attr in game.customAttributes: + if attr["key"] == "developerName": + developer = attr["value"] + break + else: + developer = game.seller["name"] + self.ui.developer_label.setText(developer) + price = game.price.totalPrice.fmtPrice["originalPrice"] + discount_price = game.price.totalPrice.fmtPrice["discountPrice"] + self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') + if price != discount_price: + font = self.ui.price_label.font() + font.setStrikeOut(True) + self.ui.price_label.setFont(font) + self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') + else: + self.ui.discount_label.setVisible(False) + + key_images = game.keyImages + self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url) + + # for img in json_info["keyImages"]: + # if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"]: + # if img["type"] == "VaultClosed" and json_info["title"] != "Mystery Game": + # continue + # self.fetchPixmap(img["url"]) + # break + # else: + # logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) + + +class ResultsItemWidget(ItemWidget): + def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None): + super(ResultsItemWidget, self).__init__(manager, catalog_game, parent=parent) + self.setFixedSize(ImageSize.Display) + self.ui.setupUi(self) + + key_images = catalog_game.keyImages + self.fetchPixmap(key_images.for_dimensions(self.width(), self.height()).url) + + self.ui.title_label.setText(catalog_game.title) + + price = catalog_game.price.totalPrice.fmtPrice["originalPrice"] + discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"] + self.ui.price_label.setText(f'{price if price != "0" else self.tr("Free")}') + if price != discount_price: + font = self.ui.price_label.font() + font.setStrikeOut(True) + self.ui.price_label.setFont(font) + self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') + else: + self.ui.discount_label.setVisible(False) + + +class WishlistItemWidget(ItemWidget): + delete_from_wishlist = pyqtSignal(CatalogOfferModel) + + def __init__(self, manager: QtRequests, catalog_game: CatalogOfferModel, parent=None): + super(WishlistItemWidget, self).__init__(manager, catalog_game, parent=parent) + self.setFixedSize(ImageSize.DisplayWide) + self.ui.setupUi(self) + for attr in catalog_game.customAttributes: + if attr["key"] == "developerName": + developer = attr["value"] + break + else: + developer = catalog_game.seller["name"] + original_price = catalog_game.price.totalPrice.fmtPrice["originalPrice"] + discount_price = catalog_game.price.totalPrice.fmtPrice["discountPrice"] + + self.ui.title_label.setText(catalog_game.title) + self.ui.developer_label.setText(developer) + self.ui.price_label.setText(f'{original_price if original_price != "0" else self.tr("Free")}') + if original_price != discount_price: + font = self.ui.price_label.font() + font.setStrikeOut(True) + self.ui.price_label.setFont(font) + self.ui.discount_label.setText(f'{discount_price if discount_price != "0" else self.tr("Free")}') + else: + self.ui.discount_label.setVisible(False) + key_images = catalog_game.keyImages + self.fetchPixmap( + key_images.for_dimensions(self.width(), self.height()).url + ) + + self.delete_button = QPushButton(self) + self.delete_button.setIcon(qta_icon("mdi.delete", color="white")) + self.delete_button.clicked.connect( + lambda: self.delete_from_wishlist.emit(self.catalog_game) + ) + self.layout().insertWidget(0, self.delete_button, alignment=Qt.AlignRight) + diff --git a/rare/components/tabs/store/wishlist.py b/rare/components/tabs/store/wishlist.py index fbac0713..c2f1bd06 100644 --- a/rare/components/tabs/store/wishlist.py +++ b/rare/components/tabs/store/wishlist.py @@ -1,45 +1,116 @@ -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QStackedWidget, QMessageBox +from enum import IntEnum +from typing import List + +from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot +from PyQt5.QtGui import QShowEvent +from PyQt5.QtWidgets import QMessageBox, QWidget, QSizePolicy -from rare.components.tabs.store import ShopApiCore -from rare.components.tabs.store.game_widgets import WishlistWidget from rare.ui.components.tabs.store.wishlist import Ui_Wishlist -from rare.utils.extra_widgets import WaitingSpinner -from rare.utils.misc import qta_icon as icon +from rare.utils.misc import qta_icon +from rare.widgets.flow_layout import FlowLayout +from rare.widgets.side_tab import SideTabContents +from rare.widgets.sliding_stack import SlidingStackedWidget +from .api.models.response import WishlistItemModel, CatalogOfferModel +from .store_api import StoreAPI +from .widgets.details import DetailsWidget +from .widgets.items import WishlistItemWidget -class Wishlist(QStackedWidget, Ui_Wishlist): - show_game_info = pyqtSignal(dict) +class WishlistPage(SlidingStackedWidget, SideTabContents): + def __init__(self, api: StoreAPI, parent=None): + super(WishlistPage, self).__init__(parent=parent) + self.implements_scrollarea = True + + self.wishlist_widget = WishlistWidget(api, parent=self) + self.wishlist_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.wishlist_widget.set_title.connect(self.set_title) + self.wishlist_widget.show_details.connect(self.show_details) + + self.details_widget = DetailsWidget([], api, parent=self) + self.details_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.details_widget.set_title.connect(self.set_title) + self.details_widget.back_clicked.connect(self.show_main) + + self.setDirection(Qt.Horizontal) + self.addWidget(self.wishlist_widget) + self.addWidget(self.details_widget) + + @pyqtSlot() + def show_main(self): + self.slideInWidget(self.wishlist_widget) + + @pyqtSlot(object) + def show_details(self, game: CatalogOfferModel): + self.details_widget.update_game(game) + self.slideInWidget(self.details_widget) + + +class WishlistOrder(IntEnum): + NAME = 1 + PRICE = 2 + DISCOUNT = 3 + DEVELOPER = 4 + + +class WishlistFilter(IntEnum): + NONE = 0 + DISCOUNT = 1 + + +class WishlistWidget(QWidget, SideTabContents): + show_details = pyqtSignal(CatalogOfferModel) update_wishlist_signal = pyqtSignal() - def __init__(self, api_core: ShopApiCore): - super(Wishlist, self).__init__() - self.api_core = api_core - self.setupUi(self) - self.addWidget(WaitingSpinner()) - self.setCurrentIndex(1) - self.wishlist = [] - self.widgets = [] + def __init__(self, api: StoreAPI, parent=None): + super(WishlistWidget, self).__init__(parent=parent) + self.implements_scrollarea = True + self.api = api + self.ui = Ui_Wishlist() + self.ui.setupUi(self) + self.ui.main_layout.setContentsMargins(0, 0, 3, 0) - self.sort_cb.currentIndexChanged.connect( - lambda i: self.set_wishlist(self.wishlist, i) - ) - self.filter_cb.currentIndexChanged.connect(self.set_filter) - self.reload_button.clicked.connect(self.update_wishlist) - self.reload_button.setIcon(icon("fa.refresh", color="white")) + self.wishlist_layout = FlowLayout() + self.ui.container_layout.addLayout(self.wishlist_layout, stretch=1) - self.reverse.stateChanged.connect( - lambda: self.set_wishlist(sort=self.sort_cb.currentIndex()) + filters = { + WishlistFilter.NONE: self.tr("All items"), + WishlistFilter.DISCOUNT: self.tr("Discount"), + } + for data, text in filters.items(): + self.ui.filter_combo.addItem(text, data) + self.ui.filter_combo.currentIndexChanged.connect(self.filter_wishlist) + + sortings = { + WishlistOrder.NAME: self.tr("Name"), + WishlistOrder.PRICE: self.tr("Price"), + WishlistOrder.DISCOUNT: self.tr("Discount"), + WishlistOrder.DEVELOPER: self.tr("Developer"), + } + for data, text in sortings.items(): + self.ui.order_combo.addItem(text, data) + self.ui.order_combo.currentIndexChanged.connect(self.order_wishlist) + + self.ui.reload_button.setIcon(qta_icon("fa.refresh", color="white")) + self.ui.reload_button.clicked.connect(self.update_wishlist) + + self.ui.reverse_check.stateChanged.connect( + lambda: self.order_wishlist(self.ui.order_combo.currentIndex()) ) + self.setEnabled(False) + + def showEvent(self, a0: QShowEvent) -> None: + self.update_wishlist() + return super().showEvent(a0) + def update_wishlist(self): - self.setCurrentIndex(1) - self.api_core.get_wishlist(self.set_wishlist) + self.setEnabled(False) + self.api.get_wishlist(self.set_wishlist) - def delete_from_wishlist(self, game): - self.api_core.remove_from_wishlist( - game["namespace"], - game["id"], + def delete_from_wishlist(self, game: CatalogOfferModel): + self.api.remove_from_wishlist( + game.namespace, + game.id, lambda success: self.update_wishlist() if success else QMessageBox.warning( @@ -48,72 +119,68 @@ class Wishlist(QStackedWidget, Ui_Wishlist): ) self.update_wishlist_signal.emit() - def set_filter(self, i): - count = 0 - for w in self.widgets: - if i == 1 and not w.discount: - w.setVisible(False) + @pyqtSlot(int) + def filter_wishlist(self, index: int = int(WishlistFilter.NONE)): + list_filter = self.ui.filter_combo.itemData(index, Qt.UserRole) + widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindDirectChildrenOnly) + for w in widgets: + if list_filter == WishlistFilter.NONE: + w.setVisible(True) + elif list_filter == WishlistFilter.DISCOUNT: + w.setVisible(bool(w.catalog_game.price.totalPrice.discount)) else: w.setVisible(True) - count += 1 + have_visible = any(map(lambda x: x.isVisible(), widgets)) + self.ui.no_games_label.setVisible(not have_visible) - if i == 0: - w.setVisible(True) + @pyqtSlot(int) + def order_wishlist(self, index: int = int(WishlistOrder.NAME)): + list_order = self.ui.order_combo.itemData(index, Qt.UserRole) + widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindDirectChildrenOnly) + for w in widgets: + self.wishlist_layout.removeWidget(w) - if count == 0: - self.no_games_label.setVisible(True) + if list_order == WishlistOrder.NAME: + def func(x: WishlistItemWidget): + return x.catalog_game.title + elif list_order == WishlistOrder.PRICE: + def func(x: WishlistItemWidget): + return x.catalog_game.price.totalPrice.discountPrice + elif list_order == WishlistOrder.DEVELOPER: + def func(x: WishlistItemWidget): + return x.catalog_game.seller["name"] + elif list_order == WishlistOrder.DISCOUNT: + def func(x: WishlistItemWidget): + discount = x.catalog_game.price.totalPrice.discountPrice + original = x.catalog_game.price.totalPrice.originalPrice + return 1 - (discount / original) else: - self.no_games_label.setVisible(False) + def func(x: WishlistItemWidget): + return x.catalog_game.title - def set_wishlist(self, wishlist=None, sort=0): + reverse = self.ui.reverse_check.isChecked() + widgets = sorted(widgets, key=func, reverse=reverse) + for w in widgets: + self.wishlist_layout.addWidget(w) + + def set_wishlist(self, wishlist: List[WishlistItemModel] = None): if wishlist and wishlist[0] == "error": return - if wishlist is not None: - self.wishlist = wishlist + widgets = self.ui.container.findChildren(WishlistItemWidget, options=Qt.FindDirectChildrenOnly) + for w in widgets: + self.wishlist_layout.removeWidget(w) + w.deleteLater() - for i in self.widgets: - i.deleteLater() + self.ui.no_games_label.setVisible(bool(wishlist)) - if sort == 0: - sorted_list = sorted(self.wishlist, key=lambda x: x["offer"]["title"]) - elif sort == 1: - sorted_list = sorted( - self.wishlist, - key=lambda x: x["offer"]["price"]["totalPrice"]["fmtPrice"][ - "discountPrice" - ], - ) - elif sort == 2: - sorted_list = sorted( - self.wishlist, key=lambda x: x["offer"]["seller"]["name"] - ) - elif sort == 3: - sorted_list = sorted( - self.wishlist, - reverse=True, - key=lambda x: 1 - - ( - x["offer"]["price"]["totalPrice"]["discountPrice"] - / x["offer"]["price"]["totalPrice"]["originalPrice"] - ), - ) - else: - sorted_list = self.wishlist - self.widgets.clear() - - if len(sorted_list) == 0: - self.no_games_label.setVisible(True) - else: - self.no_games_label.setVisible(False) - - if self.reverse.isChecked(): - sorted_list.reverse() - - for game in sorted_list: - w = WishlistWidget(game["offer"]) - self.widgets.append(w) - self.list_layout.addWidget(w) - w.open_game.connect(self.show_game_info.emit) + for game in wishlist: + w = WishlistItemWidget(self.api.cached_manager, game.offer, self.ui.container) + w.show_details.connect(self.show_details) w.delete_from_wishlist.connect(self.delete_from_wishlist) - self.setCurrentIndex(0) + self.wishlist_layout.addWidget(w) + + self.order_wishlist(self.ui.order_combo.currentIndex()) + self.filter_wishlist(self.ui.filter_combo.currentIndex()) + + self.setEnabled(True) diff --git a/rare/resources/resources.py b/rare/resources/resources.py index a45a4bb1..f3fc0f5b 100644 Binary files a/rare/resources/resources.py and b/rare/resources/resources.py differ diff --git a/rare/resources/static_css/__init__.py b/rare/resources/static_css/__init__.py index 0e139851..2e4d9ccf 100644 Binary files a/rare/resources/static_css/__init__.py and b/rare/resources/static_css/__init__.py differ diff --git a/rare/resources/static_css/stylesheet.py b/rare/resources/static_css/stylesheet.py index 551826e7..43a7aece 100644 --- a/rare/resources/static_css/stylesheet.py +++ b/rare/resources/static_css/stylesheet.py @@ -64,6 +64,14 @@ def css_name(widget: Union[wrappertype, QObject, Type], subwidget: str = ""): css = qstylizer.style.StyleSheet() +# Generic flat button +css['QPushButton[flat="true"]'].setValues( + border="0px", + borderRadius="5px", + backgroundColor="rgba(255, 255, 255, 5%)", +) + + # InfoLabel css.QLabel["#InfoLabel"].setValues( color="#999", diff --git a/rare/resources/static_css/stylesheet.qss b/rare/resources/static_css/stylesheet.qss index 051a419d..df10c32d 100644 --- a/rare/resources/static_css/stylesheet.qss +++ b/rare/resources/static_css/stylesheet.qss @@ -1,6 +1,11 @@ /* This file is auto-generated from "stylesheet.py". DO NOT EDIT!!! */ +QPushButton[flat="true"] { + border: 0px; + border-radius: 5px; + background-color: rgba(255, 255, 255, 5%); +} QLabel#InfoLabel { color: #999; font-style: italic; diff --git a/rare/resources/stylesheets/ChildOfMetropolis/__init__.py b/rare/resources/stylesheets/ChildOfMetropolis/__init__.py index effb5639..73407d65 100644 Binary files a/rare/resources/stylesheets/ChildOfMetropolis/__init__.py and b/rare/resources/stylesheets/ChildOfMetropolis/__init__.py differ diff --git a/rare/resources/stylesheets/RareStyle/__init__.py b/rare/resources/stylesheets/RareStyle/__init__.py index b987bee2..dfc98d19 100644 Binary files a/rare/resources/stylesheets/RareStyle/__init__.py and b/rare/resources/stylesheets/RareStyle/__init__.py differ diff --git a/rare/ui/components/tabs/store/details.py b/rare/ui/components/tabs/store/details.py new file mode 100644 index 00000000..0862694b --- /dev/null +++ b/rare/ui/components/tabs/store/details.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'rare/ui/components/tabs/store/details.ui' +# +# Created by: PyQt5 UI code generator 5.15.10 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_DetailsWidget(object): + def setupUi(self, DetailsWidget): + DetailsWidget.setObjectName("DetailsWidget") + DetailsWidget.resize(630, 371) + DetailsWidget.setWindowTitle("DetailsWidget") + self.main_layout = QtWidgets.QHBoxLayout(DetailsWidget) + self.main_layout.setObjectName("main_layout") + self.left_layout = QtWidgets.QVBoxLayout() + self.left_layout.setObjectName("left_layout") + self.back_button = QtWidgets.QPushButton(DetailsWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.back_button.sizePolicy().hasHeightForWidth()) + self.back_button.setSizePolicy(sizePolicy) + self.back_button.setText("") + self.back_button.setIconSize(QtCore.QSize(32, 32)) + self.back_button.setFlat(True) + self.back_button.setObjectName("back_button") + self.left_layout.addWidget(self.back_button) + self.main_layout.addLayout(self.left_layout) + self.right_layout = QtWidgets.QVBoxLayout() + self.right_layout.setObjectName("right_layout") + self.details_layout = QtWidgets.QFormLayout() + self.details_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) + self.details_layout.setFieldGrowthPolicy(QtWidgets.QFormLayout.FieldsStayAtSizeHint) + self.details_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.details_layout.setContentsMargins(6, 6, 6, 6) + self.details_layout.setSpacing(12) + self.details_layout.setObjectName("details_layout") + self.title_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.title_label.setFont(font) + self.title_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.title_label.setObjectName("title_label") + self.details_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.title_label) + self.title = QtWidgets.QLabel(DetailsWidget) + self.title.setText("title") + self.title.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.title.setObjectName("title") + self.details_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.title) + self.developer_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.developer_label.setFont(font) + self.developer_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.developer_label.setObjectName("developer_label") + self.details_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.developer_label) + self.developer = QtWidgets.QLabel(DetailsWidget) + self.developer.setText("developer") + self.developer.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.developer.setObjectName("developer") + self.details_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.developer) + self.publisher_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.publisher_label.setFont(font) + self.publisher_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.publisher_label.setObjectName("publisher_label") + self.details_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.publisher_label) + self.publisher = QtWidgets.QLabel(DetailsWidget) + self.publisher.setText("publisher") + self.publisher.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.publisher.setObjectName("publisher") + self.details_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.publisher) + self.status_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.status_label.setFont(font) + self.status_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.status_label.setObjectName("status_label") + self.details_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.status_label) + self.status = QtWidgets.QLabel(DetailsWidget) + self.status.setObjectName("status") + self.details_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.status) + self.price_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.price_label.setFont(font) + self.price_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.price_label.setObjectName("price_label") + self.details_layout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.price_label) + self.tags_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.tags_label.setFont(font) + self.tags_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.tags_label.setObjectName("tags_label") + self.details_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.tags_label) + self.tags = QtWidgets.QLabel(DetailsWidget) + self.tags.setText("tags") + self.tags.setObjectName("tags") + self.details_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.tags) + self.social_links_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.social_links_label.setFont(font) + self.social_links_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.social_links_label.setObjectName("social_links_label") + self.details_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.social_links_label) + self.actions_label = QtWidgets.QLabel(DetailsWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.actions_label.setFont(font) + self.actions_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.actions_label.setObjectName("actions_label") + self.details_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.actions_label) + self.social_links = QtWidgets.QWidget(DetailsWidget) + self.social_links.setObjectName("social_links") + self.social_links_layout = QtWidgets.QHBoxLayout(self.social_links) + self.social_links_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) + self.social_links_layout.setContentsMargins(0, 0, 0, 0) + self.social_links_layout.setObjectName("social_links_layout") + self.details_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.social_links) + self.actions = QtWidgets.QWidget(DetailsWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.actions.sizePolicy().hasHeightForWidth()) + self.actions.setSizePolicy(sizePolicy) + self.actions.setMinimumSize(QtCore.QSize(250, 0)) + self.actions.setObjectName("actions") + self.actions_layout = QtWidgets.QVBoxLayout(self.actions) + self.actions_layout.setContentsMargins(0, 0, 0, 0) + self.actions_layout.setObjectName("actions_layout") + self.store_button = QtWidgets.QPushButton(self.actions) + self.store_button.setObjectName("store_button") + self.actions_layout.addWidget(self.store_button) + self.wishlist_button = QtWidgets.QPushButton(self.actions) + self.wishlist_button.setObjectName("wishlist_button") + self.actions_layout.addWidget(self.wishlist_button) + self.details_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.actions) + self.price = QtWidgets.QWidget(DetailsWidget) + self.price.setObjectName("price") + self.price_layout = QtWidgets.QHBoxLayout(self.price) + self.price_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) + self.price_layout.setContentsMargins(0, 0, 0, 0) + self.price_layout.setObjectName("price_layout") + self.original_price = QtWidgets.QLabel(self.price) + self.original_price.setText("orignal") + self.original_price.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.original_price.setObjectName("original_price") + self.price_layout.addWidget(self.original_price) + self.discount_price = QtWidgets.QLabel(self.price) + self.discount_price.setText("discount") + self.discount_price.setObjectName("discount_price") + self.price_layout.addWidget(self.discount_price) + self.details_layout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.price) + self.right_layout.addLayout(self.details_layout) + self.requirements_frame = QtWidgets.QFrame(DetailsWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.requirements_frame.sizePolicy().hasHeightForWidth()) + self.requirements_frame.setSizePolicy(sizePolicy) + self.requirements_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.requirements_frame.setFrameShadow(QtWidgets.QFrame.Sunken) + self.requirements_frame.setObjectName("requirements_frame") + self.requirements_layout = QtWidgets.QHBoxLayout(self.requirements_frame) + self.requirements_layout.setContentsMargins(0, 0, 0, 0) + self.requirements_layout.setObjectName("requirements_layout") + self.right_layout.addWidget(self.requirements_frame) + self.description_label = QtWidgets.QTextBrowser(DetailsWidget) + self.description_label.setOpenExternalLinks(True) + self.description_label.setObjectName("description_label") + self.right_layout.addWidget(self.description_label) + self.main_layout.addLayout(self.right_layout) + self.main_layout.setStretch(1, 1) + + self.retranslateUi(DetailsWidget) + + def retranslateUi(self, DetailsWidget): + _translate = QtCore.QCoreApplication.translate + self.title_label.setText(_translate("DetailsWidget", "Title")) + self.developer_label.setText(_translate("DetailsWidget", "Developer")) + self.publisher_label.setText(_translate("DetailsWidget", "Publisher")) + self.status_label.setText(_translate("DetailsWidget", "Status")) + self.status.setText(_translate("DetailsWidget", "You already own this game")) + self.price_label.setText(_translate("DetailsWidget", "Price")) + self.tags_label.setText(_translate("DetailsWidget", "Tags")) + self.social_links_label.setText(_translate("DetailsWidget", "Links")) + self.actions_label.setText(_translate("DetailsWidget", "Actions")) + self.store_button.setText(_translate("DetailsWidget", "Buy in Epic Games Store")) + self.wishlist_button.setText(_translate("DetailsWidget", "Add to wishlist")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + DetailsWidget = QtWidgets.QWidget() + ui = Ui_DetailsWidget() + ui.setupUi(DetailsWidget) + DetailsWidget.show() + sys.exit(app.exec_()) diff --git a/rare/ui/components/tabs/store/details.ui b/rare/ui/components/tabs/store/details.ui new file mode 100644 index 00000000..0ccbdef5 --- /dev/null +++ b/rare/ui/components/tabs/store/details.ui @@ -0,0 +1,394 @@ + + + DetailsWidget + + + + 0 + 0 + 630 + 371 + + + + DetailsWidget + + + + + + + + + 0 + 0 + + + + + + + + 32 + 32 + + + + true + + + + + + + + + + + QLayout::SetFixedSize + + + QFormLayout::FieldsStayAtSizeHint + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 12 + + + 12 + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + 75 + true + + + + Title + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + title + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 75 + true + + + + Developer + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + developer + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 75 + true + + + + Publisher + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + publisher + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 75 + true + + + + Status + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + You already own this game + + + + + + + + 75 + true + + + + Price + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 75 + true + + + + Tags + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + tags + + + + + + + + 75 + true + + + + Links + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 75 + true + + + + Actions + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + QLayout::SetFixedSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 0 + 0 + + + + + 250 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Buy in Epic Games Store + + + + + + + Add to wishlist + + + + + + + + + + + QLayout::SetFixedSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + orignal + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + discount + + + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + true + + + + + + + + + + diff --git a/rare/ui/components/tabs/store/landing.py b/rare/ui/components/tabs/store/landing.py new file mode 100644 index 00000000..c2aa0099 --- /dev/null +++ b/rare/ui/components/tabs/store/landing.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'rare/ui/components/tabs/store/landing.ui' +# +# Created by: PyQt5 UI code generator 5.15.10 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_LandingWidget(object): + def setupUi(self, LandingWidget): + LandingWidget.setObjectName("LandingWidget") + LandingWidget.resize(788, 662) + LandingWidget.setWindowTitle("LandingWidget") + self.main_layout = QtWidgets.QHBoxLayout(LandingWidget) + self.main_layout.setObjectName("main_layout") + self.left_layout = QtWidgets.QVBoxLayout() + self.left_layout.setObjectName("left_layout") + self.main_layout.addLayout(self.left_layout) + self.right_layout = QtWidgets.QVBoxLayout() + self.right_layout.setObjectName("right_layout") + self.reset_button = QtWidgets.QPushButton(LandingWidget) + self.reset_button.setObjectName("reset_button") + self.right_layout.addWidget(self.reset_button) + self.filter_scrollarea = QtWidgets.QScrollArea(LandingWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.filter_scrollarea.sizePolicy().hasHeightForWidth()) + self.filter_scrollarea.setSizePolicy(sizePolicy) + self.filter_scrollarea.setFrameShape(QtWidgets.QFrame.NoFrame) + self.filter_scrollarea.setFrameShadow(QtWidgets.QFrame.Plain) + self.filter_scrollarea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.filter_scrollarea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.filter_scrollarea.setWidgetResizable(True) + self.filter_scrollarea.setObjectName("filter_scrollarea") + self.filter_container = QtWidgets.QWidget() + self.filter_container.setGeometry(QtCore.QRect(0, 0, 142, 390)) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.filter_container.sizePolicy().hasHeightForWidth()) + self.filter_container.setSizePolicy(sizePolicy) + self.filter_container.setObjectName("filter_container") + self.filter_container_layout = QtWidgets.QVBoxLayout(self.filter_container) + self.filter_container_layout.setContentsMargins(0, 0, 3, 0) + self.filter_container_layout.setObjectName("filter_container_layout") + self.price_group = QtWidgets.QGroupBox(self.filter_container) + self.price_group.setObjectName("price_group") + self.price_layout = QtWidgets.QVBoxLayout(self.price_group) + self.price_layout.setObjectName("price_layout") + self.none_price = QtWidgets.QRadioButton(self.price_group) + self.none_price.setChecked(True) + self.none_price.setObjectName("none_price") + self.price_layout.addWidget(self.none_price) + self.free_button = QtWidgets.QRadioButton(self.price_group) + self.free_button.setObjectName("free_button") + self.price_layout.addWidget(self.free_button) + self.under10 = QtWidgets.QRadioButton(self.price_group) + self.under10.setObjectName("under10") + self.price_layout.addWidget(self.under10) + self.under20 = QtWidgets.QRadioButton(self.price_group) + self.under20.setObjectName("under20") + self.price_layout.addWidget(self.under20) + self.under30 = QtWidgets.QRadioButton(self.price_group) + self.under30.setObjectName("under30") + self.price_layout.addWidget(self.under30) + self.above = QtWidgets.QRadioButton(self.price_group) + self.above.setObjectName("above") + self.price_layout.addWidget(self.above) + self.on_discount = QtWidgets.QCheckBox(self.price_group) + self.on_discount.setObjectName("on_discount") + self.price_layout.addWidget(self.on_discount) + self.filter_container_layout.addWidget(self.price_group) + self.platform_group = QtWidgets.QGroupBox(self.filter_container) + self.platform_group.setObjectName("platform_group") + self.platfrom_layout = QtWidgets.QVBoxLayout(self.platform_group) + self.platfrom_layout.setObjectName("platfrom_layout") + self.filter_container_layout.addWidget(self.platform_group) + self.genre_group = QtWidgets.QGroupBox(self.filter_container) + self.genre_group.setObjectName("genre_group") + self.genre_layout = QtWidgets.QVBoxLayout(self.genre_group) + self.genre_layout.setObjectName("genre_layout") + self.filter_container_layout.addWidget(self.genre_group) + self.type_group = QtWidgets.QGroupBox(self.filter_container) + self.type_group.setObjectName("type_group") + self.type_layout = QtWidgets.QVBoxLayout(self.type_group) + self.type_layout.setObjectName("type_layout") + self.filter_container_layout.addWidget(self.type_group) + self.others_group = QtWidgets.QGroupBox(self.filter_container) + self.others_group.setObjectName("others_group") + self.others_layout = QtWidgets.QVBoxLayout(self.others_group) + self.others_layout.setObjectName("others_layout") + self.filter_container_layout.addWidget(self.others_group) + self.filter_scrollarea.setWidget(self.filter_container) + self.right_layout.addWidget(self.filter_scrollarea) + self.main_layout.addLayout(self.right_layout) + self.main_layout.setStretch(0, 1) + + self.retranslateUi(LandingWidget) + + def retranslateUi(self, LandingWidget): + _translate = QtCore.QCoreApplication.translate + self.reset_button.setText(_translate("LandingWidget", "Reset filters")) + self.price_group.setTitle(_translate("LandingWidget", "Price")) + self.none_price.setText(_translate("LandingWidget", "None")) + self.free_button.setText(_translate("LandingWidget", "Free")) + self.under10.setText(_translate("LandingWidget", "Under 10")) + self.under20.setText(_translate("LandingWidget", "Under 20")) + self.under30.setText(_translate("LandingWidget", "Under 30")) + self.above.setText(_translate("LandingWidget", "14.99 and above")) + self.on_discount.setText(_translate("LandingWidget", "Discount")) + self.platform_group.setTitle(_translate("LandingWidget", "Platform")) + self.genre_group.setTitle(_translate("LandingWidget", "Genre")) + self.type_group.setTitle(_translate("LandingWidget", "Type")) + self.others_group.setTitle(_translate("LandingWidget", "Other tags")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + LandingWidget = QtWidgets.QWidget() + ui = Ui_LandingWidget() + ui.setupUi(LandingWidget) + LandingWidget.show() + sys.exit(app.exec_()) diff --git a/rare/ui/components/tabs/store/landing.ui b/rare/ui/components/tabs/store/landing.ui new file mode 100644 index 00000000..2e3b75b4 --- /dev/null +++ b/rare/ui/components/tabs/store/landing.ui @@ -0,0 +1,183 @@ + + + LandingWidget + + + + 0 + 0 + 788 + 662 + + + + LandingWidget + + + + + + + + + + + Reset filters + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 142 + 390 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 3 + + + 0 + + + + + Price + + + + + + None + + + true + + + + + + + Free + + + + + + + Under 10 + + + + + + + Under 20 + + + + + + + Under 30 + + + + + + + 14.99 and above + + + + + + + Discount + + + + + + + + + + Platform + + + + + + + + Genre + + + + + + + + Type + + + + + + + + Other tags + + + + + + + + + + + + + + + diff --git a/rare/ui/components/tabs/store/search.py b/rare/ui/components/tabs/store/search.py new file mode 100644 index 00000000..d7b78c95 --- /dev/null +++ b/rare/ui/components/tabs/store/search.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'rare/ui/components/tabs/store/search.ui' +# +# Created by: PyQt5 UI code generator 5.15.10 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_SearchWidget(object): + def setupUi(self, SearchWidget): + SearchWidget.setObjectName("SearchWidget") + SearchWidget.resize(491, 382) + SearchWidget.setWindowTitle("SearchWidget") + self.main_layout = QtWidgets.QHBoxLayout(SearchWidget) + self.main_layout.setObjectName("main_layout") + self.left_layout = QtWidgets.QVBoxLayout() + self.left_layout.setObjectName("left_layout") + self.main_layout.addLayout(self.left_layout) + self.right_layout = QtWidgets.QVBoxLayout() + self.right_layout.setObjectName("right_layout") + self.reset_button = QtWidgets.QPushButton(SearchWidget) + self.reset_button.setObjectName("reset_button") + self.right_layout.addWidget(self.reset_button) + self.filter_scrollarea = QtWidgets.QScrollArea(SearchWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.filter_scrollarea.sizePolicy().hasHeightForWidth()) + self.filter_scrollarea.setSizePolicy(sizePolicy) + self.filter_scrollarea.setFrameShape(QtWidgets.QFrame.NoFrame) + self.filter_scrollarea.setFrameShadow(QtWidgets.QFrame.Plain) + self.filter_scrollarea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.filter_scrollarea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.filter_scrollarea.setWidgetResizable(True) + self.filter_scrollarea.setObjectName("filter_scrollarea") + self.filter_container = QtWidgets.QWidget() + self.filter_container.setGeometry(QtCore.QRect(0, 0, 142, 390)) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.filter_container.sizePolicy().hasHeightForWidth()) + self.filter_container.setSizePolicy(sizePolicy) + self.filter_container.setObjectName("filter_container") + self.filter_container_layout = QtWidgets.QVBoxLayout(self.filter_container) + self.filter_container_layout.setContentsMargins(0, 0, 3, 0) + self.filter_container_layout.setObjectName("filter_container_layout") + self.price_group = QtWidgets.QGroupBox(self.filter_container) + self.price_group.setObjectName("price_group") + self.price_layout = QtWidgets.QVBoxLayout(self.price_group) + self.price_layout.setObjectName("price_layout") + self.none_price = QtWidgets.QRadioButton(self.price_group) + self.none_price.setChecked(True) + self.none_price.setObjectName("none_price") + self.price_layout.addWidget(self.none_price) + self.free_button = QtWidgets.QRadioButton(self.price_group) + self.free_button.setObjectName("free_button") + self.price_layout.addWidget(self.free_button) + self.under10 = QtWidgets.QRadioButton(self.price_group) + self.under10.setObjectName("under10") + self.price_layout.addWidget(self.under10) + self.under20 = QtWidgets.QRadioButton(self.price_group) + self.under20.setObjectName("under20") + self.price_layout.addWidget(self.under20) + self.under30 = QtWidgets.QRadioButton(self.price_group) + self.under30.setObjectName("under30") + self.price_layout.addWidget(self.under30) + self.above = QtWidgets.QRadioButton(self.price_group) + self.above.setObjectName("above") + self.price_layout.addWidget(self.above) + self.on_discount = QtWidgets.QCheckBox(self.price_group) + self.on_discount.setObjectName("on_discount") + self.price_layout.addWidget(self.on_discount) + self.filter_container_layout.addWidget(self.price_group) + self.platform_group = QtWidgets.QGroupBox(self.filter_container) + self.platform_group.setObjectName("platform_group") + self.platfrom_layout = QtWidgets.QVBoxLayout(self.platform_group) + self.platfrom_layout.setObjectName("platfrom_layout") + self.filter_container_layout.addWidget(self.platform_group) + self.genre_group = QtWidgets.QGroupBox(self.filter_container) + self.genre_group.setObjectName("genre_group") + self.genre_layout = QtWidgets.QVBoxLayout(self.genre_group) + self.genre_layout.setObjectName("genre_layout") + self.filter_container_layout.addWidget(self.genre_group) + self.type_group = QtWidgets.QGroupBox(self.filter_container) + self.type_group.setObjectName("type_group") + self.type_layout = QtWidgets.QVBoxLayout(self.type_group) + self.type_layout.setObjectName("type_layout") + self.filter_container_layout.addWidget(self.type_group) + self.others_group = QtWidgets.QGroupBox(self.filter_container) + self.others_group.setObjectName("others_group") + self.others_layout = QtWidgets.QVBoxLayout(self.others_group) + self.others_layout.setObjectName("others_layout") + self.filter_container_layout.addWidget(self.others_group) + self.filter_scrollarea.setWidget(self.filter_container) + self.right_layout.addWidget(self.filter_scrollarea) + self.main_layout.addLayout(self.right_layout) + self.main_layout.setStretch(0, 1) + + self.retranslateUi(SearchWidget) + + def retranslateUi(self, SearchWidget): + _translate = QtCore.QCoreApplication.translate + self.reset_button.setText(_translate("SearchWidget", "Reset filters")) + self.price_group.setTitle(_translate("SearchWidget", "Price")) + self.none_price.setText(_translate("SearchWidget", "None")) + self.free_button.setText(_translate("SearchWidget", "Free")) + self.under10.setText(_translate("SearchWidget", "Under 10")) + self.under20.setText(_translate("SearchWidget", "Under 20")) + self.under30.setText(_translate("SearchWidget", "Under 30")) + self.above.setText(_translate("SearchWidget", "14.99 and above")) + self.on_discount.setText(_translate("SearchWidget", "Discount")) + self.platform_group.setTitle(_translate("SearchWidget", "Platform")) + self.genre_group.setTitle(_translate("SearchWidget", "Genre")) + self.type_group.setTitle(_translate("SearchWidget", "Type")) + self.others_group.setTitle(_translate("SearchWidget", "Other tags")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + SearchWidget = QtWidgets.QWidget() + ui = Ui_SearchWidget() + ui.setupUi(SearchWidget) + SearchWidget.show() + sys.exit(app.exec_()) diff --git a/rare/ui/components/tabs/store/search.ui b/rare/ui/components/tabs/store/search.ui new file mode 100644 index 00000000..516433f4 --- /dev/null +++ b/rare/ui/components/tabs/store/search.ui @@ -0,0 +1,183 @@ + + + SearchWidget + + + + 0 + 0 + 491 + 382 + + + + SearchWidget + + + + + + + + + + + Reset filters + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 142 + 390 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 3 + + + 0 + + + + + Price + + + + + + None + + + true + + + + + + + Free + + + + + + + Under 10 + + + + + + + Under 20 + + + + + + + Under 30 + + + + + + + 14.99 and above + + + + + + + Discount + + + + + + + + + + Platform + + + + + + + + Genre + + + + + + + + Type + + + + + + + + Other tags + + + + + + + + + + + + + + + diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py deleted file mode 100644 index 9e010bc7..00000000 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'rare/ui/components/tabs/store/shop_game_info.ui' -# -# Created by: PyQt5 UI code generator 5.15.6 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtGui, QtWidgets - - -class Ui_shop_info(object): - def setupUi(self, shop_info): - shop_info.setObjectName("shop_info") - shop_info.resize(702, 468) - shop_info.setWindowTitle("Form") - self.verticalLayout = QtWidgets.QVBoxLayout(shop_info) - self.verticalLayout.setObjectName("verticalLayout") - self.back_button = QtWidgets.QPushButton(shop_info) - self.back_button.setObjectName("back_button") - self.verticalLayout.addWidget(self.back_button) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.image_stack = QtWidgets.QStackedWidget(shop_info) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.image_stack.sizePolicy().hasHeightForWidth()) - self.image_stack.setSizePolicy(sizePolicy) - self.image_stack.setObjectName("image_stack") - self.horizontalLayout.addWidget(self.image_stack) - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.title = QtWidgets.QLabel(shop_info) - font = QtGui.QFont() - font.setPointSize(18) - self.title.setFont(font) - self.title.setText("") - self.title.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextSelectableByMouse) - self.title.setObjectName("title") - self.verticalLayout_2.addWidget(self.title) - self.dev = QtWidgets.QLabel(shop_info) - font = QtGui.QFont() - font.setPointSize(14) - self.dev.setFont(font) - self.dev.setText("") - self.dev.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextSelectableByMouse) - self.dev.setObjectName("dev") - self.verticalLayout_2.addWidget(self.dev) - self.owned_label = QtWidgets.QLabel(shop_info) - self.owned_label.setObjectName("owned_label") - self.verticalLayout_2.addWidget(self.owned_label) - self.price = QtWidgets.QLabel(shop_info) - self.price.setText("") - self.price.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse | QtCore.Qt.TextSelectableByMouse) - self.price.setObjectName("price") - self.verticalLayout_2.addWidget(self.price) - self.discount_price = QtWidgets.QLabel(shop_info) - self.discount_price.setText("") - self.discount_price.setObjectName("discount_price") - self.verticalLayout_2.addWidget(self.discount_price) - self.tags = QtWidgets.QLabel(shop_info) - self.tags.setText("") - self.tags.setObjectName("tags") - self.verticalLayout_2.addWidget(self.tags) - self.open_store_button = QtWidgets.QPushButton(shop_info) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.open_store_button.sizePolicy().hasHeightForWidth()) - self.open_store_button.setSizePolicy(sizePolicy) - self.open_store_button.setObjectName("open_store_button") - self.verticalLayout_2.addWidget(self.open_store_button) - self.wishlist_button = QtWidgets.QPushButton(shop_info) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.wishlist_button.sizePolicy().hasHeightForWidth()) - self.wishlist_button.setSizePolicy(sizePolicy) - self.wishlist_button.setObjectName("wishlist_button") - self.verticalLayout_2.addWidget(self.wishlist_button) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_2.addItem(spacerItem) - self.horizontalLayout.addLayout(self.verticalLayout_2) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem1) - self.verticalLayout.addLayout(self.horizontalLayout) - self.req_group_box = QtWidgets.QGroupBox(shop_info) - self.req_group_box.setObjectName("req_group_box") - self.gridLayout_2 = QtWidgets.QGridLayout(self.req_group_box) - self.gridLayout_2.setObjectName("gridLayout_2") - self.verticalLayout.addWidget(self.req_group_box) - self.social_link_gb = QtWidgets.QGroupBox(shop_info) - self.social_link_gb.setObjectName("social_link_gb") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.social_link_gb) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.verticalLayout.addWidget(self.social_link_gb) - spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem2) - - self.retranslateUi(shop_info) - self.image_stack.setCurrentIndex(-1) - - def retranslateUi(self, shop_info): - _translate = QtCore.QCoreApplication.translate - self.back_button.setText(_translate("shop_info", "Back")) - self.owned_label.setText(_translate("shop_info", "You already own this game")) - self.open_store_button.setText(_translate("shop_info", "Buy Game in Epic Games Store")) - self.wishlist_button.setText(_translate("shop_info", "Add to wishlist")) - self.req_group_box.setTitle(_translate("shop_info", "Requirements")) - self.social_link_gb.setTitle(_translate("shop_info", "Social Links")) - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - shop_info = QtWidgets.QWidget() - ui = Ui_shop_info() - ui.setupUi(shop_info) - shop_info.show() - sys.exit(app.exec_()) diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui deleted file mode 100644 index b1dd7dfb..00000000 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ /dev/null @@ -1,191 +0,0 @@ - - - shop_info - - - - 0 - 0 - 702 - 468 - - - - Form - - - - - - Back - - - - - - - - - - 0 - 0 - - - - -1 - - - - - - - - - - 18 - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - 14 - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - You already own this game - - - - - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - - - - - - - - - 0 - 0 - - - - Buy Game in Epic Games Store - - - - - - - - 0 - 0 - - - - Add to wishlist - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Requirements - - - - - - - - Social Links - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py deleted file mode 100644 index 75e5e72e..00000000 --- a/rare/ui/components/tabs/store/store.py +++ /dev/null @@ -1,172 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'rare/ui/components/tabs/store/store.ui' -# -# Created by: PyQt5 UI code generator 5.15.6 -# -# WARNING: Any manual changes made to this file will be lost when pyuic5 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt5 import QtCore, QtWidgets - - -class Ui_ShopWidget(object): - def setupUi(self, ShopWidget): - ShopWidget.setObjectName("ShopWidget") - ShopWidget.resize(850, 572) - ShopWidget.setWindowTitle("Form") - self.verticalLayout_7 = QtWidgets.QVBoxLayout(ShopWidget) - self.verticalLayout_7.setObjectName("verticalLayout_7") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.scrollArea = QtWidgets.QScrollArea(ShopWidget) - self.scrollArea.setWidgetResizable(True) - self.scrollArea.setObjectName("scrollArea") - self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 828, 550)) - self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.scrollAreaWidgetContents) - self.horizontalLayout.setObjectName("horizontalLayout") - self.widget = QtWidgets.QWidget(self.scrollAreaWidgetContents) - self.widget.setObjectName("widget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.widget) - self.verticalLayout.setObjectName("verticalLayout") - self.free_game_group_box = QtWidgets.QGroupBox(self.widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.free_game_group_box.sizePolicy().hasHeightForWidth()) - self.free_game_group_box.setSizePolicy(sizePolicy) - self.free_game_group_box.setObjectName("free_game_group_box") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.free_game_group_box) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.free_stack = QtWidgets.QStackedWidget(self.free_game_group_box) - self.free_stack.setObjectName("free_stack") - self.free_widget = QtWidgets.QWidget() - self.free_widget.setObjectName("free_widget") - self.free_stack.addWidget(self.free_widget) - self.verticalLayout_3.addWidget(self.free_stack) - self.verticalLayout.addWidget(self.free_game_group_box) - self.discounts_gb = QtWidgets.QGroupBox(self.widget) - self.discounts_gb.setObjectName("discounts_gb") - self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.discounts_gb) - self.verticalLayout_6.setObjectName("verticalLayout_6") - self.discount_stack = QtWidgets.QStackedWidget(self.discounts_gb) - self.discount_stack.setObjectName("discount_stack") - self.discount_widget = QtWidgets.QWidget() - self.discount_widget.setObjectName("discount_widget") - self.discount_stack.addWidget(self.discount_widget) - self.verticalLayout_6.addWidget(self.discount_stack) - self.verticalLayout.addWidget(self.discounts_gb) - self.filter_game_gb = QtWidgets.QGroupBox(self.widget) - self.filter_game_gb.setObjectName("filter_game_gb") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.filter_game_gb) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.game_stack = QtWidgets.QStackedWidget(self.filter_game_gb) - self.game_stack.setObjectName("game_stack") - self.game_widget = QtWidgets.QWidget() - self.game_widget.setObjectName("game_widget") - self.game_stack.addWidget(self.game_widget) - self.verticalLayout_4.addWidget(self.game_stack) - self.verticalLayout.addWidget(self.filter_game_gb) - self.horizontalLayout.addWidget(self.widget) - self.filter_gb = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.filter_gb.sizePolicy().hasHeightForWidth()) - self.filter_gb.setSizePolicy(sizePolicy) - self.filter_gb.setMinimumSize(QtCore.QSize(150, 0)) - self.filter_gb.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.filter_gb.setObjectName("filter_gb") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.filter_gb) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.reset_button = QtWidgets.QPushButton(self.filter_gb) - self.reset_button.setObjectName("reset_button") - self.verticalLayout_2.addWidget(self.reset_button) - self.price_gb = QtWidgets.QGroupBox(self.filter_gb) - self.price_gb.setObjectName("price_gb") - self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.price_gb) - self.verticalLayout_9.setObjectName("verticalLayout_9") - self.none_price = QtWidgets.QRadioButton(self.price_gb) - self.none_price.setChecked(True) - self.none_price.setObjectName("none_price") - self.verticalLayout_9.addWidget(self.none_price) - self.free_button = QtWidgets.QRadioButton(self.price_gb) - self.free_button.setObjectName("free_button") - self.verticalLayout_9.addWidget(self.free_button) - self.under10 = QtWidgets.QRadioButton(self.price_gb) - self.under10.setObjectName("under10") - self.verticalLayout_9.addWidget(self.under10) - self.under20 = QtWidgets.QRadioButton(self.price_gb) - self.under20.setObjectName("under20") - self.verticalLayout_9.addWidget(self.under20) - self.under30 = QtWidgets.QRadioButton(self.price_gb) - self.under30.setObjectName("under30") - self.verticalLayout_9.addWidget(self.under30) - self.above = QtWidgets.QRadioButton(self.price_gb) - self.above.setObjectName("above") - self.verticalLayout_9.addWidget(self.above) - self.on_discount = QtWidgets.QCheckBox(self.price_gb) - self.on_discount.setObjectName("on_discount") - self.verticalLayout_9.addWidget(self.on_discount) - self.verticalLayout_2.addWidget(self.price_gb) - self.platform_gb = QtWidgets.QGroupBox(self.filter_gb) - self.platform_gb.setObjectName("platform_gb") - self.verticalLayout_13 = QtWidgets.QVBoxLayout(self.platform_gb) - self.verticalLayout_13.setObjectName("verticalLayout_13") - self.verticalLayout_2.addWidget(self.platform_gb) - self.genre_gb = QtWidgets.QGroupBox(self.filter_gb) - self.genre_gb.setObjectName("genre_gb") - self.verticalLayout_12 = QtWidgets.QVBoxLayout(self.genre_gb) - self.verticalLayout_12.setObjectName("verticalLayout_12") - self.verticalLayout_2.addWidget(self.genre_gb) - self.type_gb = QtWidgets.QGroupBox(self.filter_gb) - self.type_gb.setObjectName("type_gb") - self.verticalLayout_11 = QtWidgets.QVBoxLayout(self.type_gb) - self.verticalLayout_11.setObjectName("verticalLayout_11") - self.verticalLayout_2.addWidget(self.type_gb) - self.others_gb = QtWidgets.QGroupBox(self.filter_gb) - self.others_gb.setObjectName("others_gb") - self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.others_gb) - self.verticalLayout_10.setObjectName("verticalLayout_10") - self.verticalLayout_2.addWidget(self.others_gb) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_2.addItem(spacerItem) - self.horizontalLayout.addWidget(self.filter_gb) - self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.horizontalLayout_2.addWidget(self.scrollArea) - self.verticalLayout_7.addLayout(self.horizontalLayout_2) - - self.retranslateUi(ShopWidget) - - def retranslateUi(self, ShopWidget): - _translate = QtCore.QCoreApplication.translate - self.free_game_group_box.setTitle(_translate("ShopWidget", "Free Games")) - self.discounts_gb.setTitle(_translate("ShopWidget", "Discounts from your wishlist")) - self.filter_game_gb.setTitle(_translate("ShopWidget", "Games")) - self.filter_gb.setTitle(_translate("ShopWidget", "Filter")) - self.reset_button.setText(_translate("ShopWidget", "Reset")) - self.price_gb.setTitle(_translate("ShopWidget", "Price")) - self.none_price.setText(_translate("ShopWidget", "None")) - self.free_button.setText(_translate("ShopWidget", "Free")) - self.under10.setText(_translate("ShopWidget", "Under 10")) - self.under20.setText(_translate("ShopWidget", "Under 20")) - self.under30.setText(_translate("ShopWidget", "Under 30")) - self.above.setText(_translate("ShopWidget", "14.99 and above")) - self.on_discount.setText(_translate("ShopWidget", "Discount")) - self.platform_gb.setTitle(_translate("ShopWidget", "Platform")) - self.genre_gb.setTitle(_translate("ShopWidget", "Genre")) - self.type_gb.setTitle(_translate("ShopWidget", "Type")) - self.others_gb.setTitle(_translate("ShopWidget", "Other Tags")) - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - ShopWidget = QtWidgets.QWidget() - ui = Ui_ShopWidget() - ui.setupUi(ShopWidget) - ShopWidget.show() - sys.exit(app.exec_()) diff --git a/rare/ui/components/tabs/store/store.ui b/rare/ui/components/tabs/store/store.ui deleted file mode 100644 index 4f61053f..00000000 --- a/rare/ui/components/tabs/store/store.ui +++ /dev/null @@ -1,238 +0,0 @@ - - - ShopWidget - - - - 0 - 0 - 850 - 572 - - - - Form - - - - - - - - true - - - - - 0 - 0 - 828 - 550 - - - - - - - - - - - 0 - 0 - - - - Free Games - - - - - - - - - - - - - - Discounts from your wishlist - - - - - - - - - - - - - - Games - - - - - - - - - - - - - - - - - - 0 - 0 - - - - - 150 - 0 - - - - - 16777215 - 16777215 - - - - Filter - - - - - - Reset - - - - - - - Price - - - - - - None - - - true - - - - - - - Free - - - - - - - Under 10 - - - - - - - Under 20 - - - - - - - Under 30 - - - - - - - 14.99 and above - - - - - - - Discount - - - - - - - - - - Platform - - - - - - - - Genre - - - - - - - - Type - - - - - - - - Other Tags - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - - - - diff --git a/rare/ui/components/tabs/store/widgets/__init__.py b/rare/ui/components/tabs/store/widgets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rare/ui/components/tabs/store/wishlist_widget.py b/rare/ui/components/tabs/store/widgets/wishlist_widget.py similarity index 60% rename from rare/ui/components/tabs/store/wishlist_widget.py rename to rare/ui/components/tabs/store/widgets/wishlist_widget.py index 5fbb2d69..57fb2850 100644 --- a/rare/ui/components/tabs/store/wishlist_widget.py +++ b/rare/ui/components/tabs/store/widgets/wishlist_widget.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'rare/ui/components/tabs/store/wishlist_widget.ui' +# Form implementation generated from reading ui file 'rare/ui/components/tabs/store/widgets/wishlist_widget.ui' # -# Created by: PyQt5 UI code generator 5.15.5 +# Created by: PyQt5 UI code generator 5.15.10 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -14,20 +14,26 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_WishlistWidget(object): def setupUi(self, WishlistWidget): WishlistWidget.setObjectName("WishlistWidget") - WishlistWidget.resize(523, 172) - WishlistWidget.setWindowTitle("Form") - self.horizontalLayout = QtWidgets.QHBoxLayout(WishlistWidget) - self.horizontalLayout.setObjectName("horizontalLayout") - self.widget = QtWidgets.QWidget(WishlistWidget) + WishlistWidget.resize(202, 94) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(WishlistWidget.sizePolicy().hasHeightForWidth()) + WishlistWidget.setSizePolicy(sizePolicy) + WishlistWidget.setWindowTitle("WishlistWIdget") + self.main_layout = QtWidgets.QHBoxLayout(WishlistWidget) + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.setObjectName("main_layout") + self.info_widget = QtWidgets.QWidget(WishlistWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) - self.widget.setSizePolicy(sizePolicy) - self.widget.setObjectName("widget") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.widget) + sizePolicy.setHeightForWidth(self.info_widget.sizePolicy().hasHeightForWidth()) + self.info_widget.setSizePolicy(sizePolicy) + self.info_widget.setObjectName("info_widget") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.info_widget) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.title_label = QtWidgets.QLabel(self.widget) + self.title_label = QtWidgets.QLabel(self.info_widget) font = QtGui.QFont() font.setPointSize(16) self.title_label.setFont(font) @@ -35,16 +41,16 @@ class Ui_WishlistWidget(object): self.title_label.setWordWrap(True) self.title_label.setObjectName("title_label") self.verticalLayout_2.addWidget(self.title_label) - self.developer = QtWidgets.QLabel(self.widget) + self.developer = QtWidgets.QLabel(self.info_widget) font = QtGui.QFont() font.setPointSize(12) self.developer.setFont(font) self.developer.setText("TextLabel") self.developer.setObjectName("developer") self.verticalLayout_2.addWidget(self.developer) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.discount_price = QtWidgets.QLabel(self.widget) + self.price_layout = QtWidgets.QHBoxLayout() + self.price_layout.setObjectName("price_layout") + self.discount_price = QtWidgets.QLabel(self.info_widget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -52,15 +58,13 @@ class Ui_WishlistWidget(object): self.discount_price.setSizePolicy(sizePolicy) self.discount_price.setText("TextLabel") self.discount_price.setObjectName("discount_price") - self.horizontalLayout_2.addWidget(self.discount_price) - self.price = QtWidgets.QLabel(self.widget) + self.price_layout.addWidget(self.discount_price) + self.price = QtWidgets.QLabel(self.info_widget) self.price.setText("TextLabel") self.price.setObjectName("price") - self.horizontalLayout_2.addWidget(self.price) - self.verticalLayout_2.addLayout(self.horizontalLayout_2) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_2.addItem(spacerItem) - self.horizontalLayout.addWidget(self.widget) + self.price_layout.addWidget(self.price) + self.verticalLayout_2.addLayout(self.price_layout) + self.main_layout.addWidget(self.info_widget, 0, QtCore.Qt.AlignTop) self.delete_button = QtWidgets.QPushButton(WishlistWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) sizePolicy.setHorizontalStretch(0) @@ -69,7 +73,7 @@ class Ui_WishlistWidget(object): self.delete_button.setSizePolicy(sizePolicy) self.delete_button.setText("") self.delete_button.setObjectName("delete_button") - self.horizontalLayout.addWidget(self.delete_button) + self.main_layout.addWidget(self.delete_button) self.retranslateUi(WishlistWidget) diff --git a/rare/ui/components/tabs/store/wishlist_widget.ui b/rare/ui/components/tabs/store/widgets/wishlist_widget.ui similarity index 75% rename from rare/ui/components/tabs/store/wishlist_widget.ui rename to rare/ui/components/tabs/store/widgets/wishlist_widget.ui index 136ff704..5acbac6f 100644 --- a/rare/ui/components/tabs/store/wishlist_widget.ui +++ b/rare/ui/components/tabs/store/widgets/wishlist_widget.ui @@ -6,16 +6,34 @@ 0 0 - 523 - 172 + 202 + 94 - - Form + + + 0 + 0 + - - - + + WishlistWIdget + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 @@ -51,7 +69,7 @@ - + @@ -74,19 +92,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/rare/ui/components/tabs/store/wishlist.py b/rare/ui/components/tabs/store/wishlist.py index e45475f4..a34293d8 100644 --- a/rare/ui/components/tabs/store/wishlist.py +++ b/rare/ui/components/tabs/store/wishlist.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'rare/ui/components/tabs/store/wishlist.ui' # -# Created by: PyQt5 UI code generator 5.15.4 +# Created by: PyQt5 UI code generator 5.15.10 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -14,59 +14,24 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Wishlist(object): def setupUi(self, Wishlist): Wishlist.setObjectName("Wishlist") - Wishlist.resize(736, 398) - Wishlist.setWindowTitle("StackedWidget") - self.page = QtWidgets.QWidget() - self.page.setObjectName("page") - self.verticalLayout = QtWidgets.QVBoxLayout(self.page) - self.verticalLayout.setObjectName("verticalLayout") - self.scroll_area = QtWidgets.QScrollArea(self.page) - self.scroll_area.setWidgetResizable(True) - self.scroll_area.setObjectName("scroll_area") - self.scroll_widget = QtWidgets.QWidget() - self.scroll_widget.setGeometry(QtCore.QRect(0, 0, 716, 378)) - self.scroll_widget.setObjectName("scroll_widget") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scroll_widget) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.title_label = QtWidgets.QLabel(self.scroll_widget) - font = QtGui.QFont() - font.setPointSize(15) - self.title_label.setFont(font) - self.title_label.setObjectName("title_label") - self.verticalLayout_2.addWidget(self.title_label) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.sort_label = QtWidgets.QLabel(self.scroll_widget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sort_label.sizePolicy().hasHeightForWidth()) - self.sort_label.setSizePolicy(sizePolicy) - self.sort_label.setObjectName("sort_label") - self.horizontalLayout.addWidget(self.sort_label) - self.sort_cb = QtWidgets.QComboBox(self.scroll_widget) - self.sort_cb.setObjectName("sort_cb") - self.sort_cb.addItem("") - self.sort_cb.addItem("") - self.sort_cb.addItem("") - self.sort_cb.addItem("") - self.horizontalLayout.addWidget(self.sort_cb) - self.reverse = QtWidgets.QCheckBox(self.scroll_widget) - self.reverse.setObjectName("reverse") - self.horizontalLayout.addWidget(self.reverse) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem) - self.filter_label = QtWidgets.QLabel(self.scroll_widget) - self.filter_label.setObjectName("filter_label") - self.horizontalLayout.addWidget(self.filter_label) - self.filter_cb = QtWidgets.QComboBox(self.scroll_widget) - self.filter_cb.setObjectName("filter_cb") - self.filter_cb.addItem("") - self.filter_cb.addItem("") - self.horizontalLayout.addWidget(self.filter_cb) - spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem1) - self.reload_button = QtWidgets.QPushButton(self.scroll_widget) + Wishlist.resize(489, 165) + Wishlist.setWindowTitle("Wishlist") + self.main_layout = QtWidgets.QVBoxLayout(Wishlist) + self.main_layout.setObjectName("main_layout") + self.tool_layout = QtWidgets.QHBoxLayout() + self.tool_layout.setObjectName("tool_layout") + self.filter_combo = QtWidgets.QComboBox(Wishlist) + self.filter_combo.setObjectName("filter_combo") + self.tool_layout.addWidget(self.filter_combo) + self.order_combo = QtWidgets.QComboBox(Wishlist) + self.order_combo.setObjectName("order_combo") + self.tool_layout.addWidget(self.order_combo) + self.reverse_check = QtWidgets.QCheckBox(Wishlist) + self.reverse_check.setObjectName("reverse_check") + self.tool_layout.addWidget(self.reverse_check) + spacerItem = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.tool_layout.addItem(spacerItem) + self.reload_button = QtWidgets.QPushButton(Wishlist) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -74,41 +39,35 @@ class Ui_Wishlist(object): self.reload_button.setSizePolicy(sizePolicy) self.reload_button.setText("") self.reload_button.setObjectName("reload_button") - self.horizontalLayout.addWidget(self.reload_button) - self.verticalLayout_2.addLayout(self.horizontalLayout) - self.list_layout = QtWidgets.QVBoxLayout() - self.list_layout.setObjectName("list_layout") - self.verticalLayout_2.addLayout(self.list_layout) - self.no_games_label = QtWidgets.QLabel(self.scroll_widget) + self.tool_layout.addWidget(self.reload_button) + self.main_layout.addLayout(self.tool_layout) + self.scrollarea = QtWidgets.QScrollArea(Wishlist) + self.scrollarea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) + self.scrollarea.setWidgetResizable(True) + self.scrollarea.setObjectName("scrollarea") + self.container = QtWidgets.QWidget() + self.container.setGeometry(QtCore.QRect(0, 0, 473, 115)) + self.container.setObjectName("container") + self.container_layout = QtWidgets.QVBoxLayout(self.container) + self.container_layout.setObjectName("container_layout") + self.no_games_label = QtWidgets.QLabel(self.container) self.no_games_label.setObjectName("no_games_label") - self.verticalLayout_2.addWidget(self.no_games_label) - spacerItem2 = QtWidgets.QSpacerItem(379, 218, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_2.addItem(spacerItem2) - self.scroll_area.setWidget(self.scroll_widget) - self.verticalLayout.addWidget(self.scroll_area) - Wishlist.addWidget(self.page) + self.container_layout.addWidget(self.no_games_label, 0, QtCore.Qt.AlignTop) + self.scrollarea.setWidget(self.container) + self.main_layout.addWidget(self.scrollarea) self.retranslateUi(Wishlist) def retranslateUi(self, Wishlist): _translate = QtCore.QCoreApplication.translate - self.title_label.setText(_translate("Wishlist", "Wishlist")) - self.sort_label.setText(_translate("Wishlist", "Sort by")) - self.sort_cb.setItemText(0, _translate("Wishlist", "Name")) - self.sort_cb.setItemText(1, _translate("Wishlist", "Price")) - self.sort_cb.setItemText(2, _translate("Wishlist", "Developer")) - self.sort_cb.setItemText(3, _translate("Wishlist", "Discount")) - self.reverse.setText(_translate("Wishlist", "Reverse")) - self.filter_label.setText(_translate("Wishlist", "Filter:")) - self.filter_cb.setItemText(0, _translate("Wishlist", "None")) - self.filter_cb.setItemText(1, _translate("Wishlist", "Discount")) + self.reverse_check.setText(_translate("Wishlist", "Reverse")) self.no_games_label.setText(_translate("Wishlist", "No games matching your filter")) if __name__ == "__main__": import sys app = QtWidgets.QApplication(sys.argv) - Wishlist = QtWidgets.QStackedWidget() + Wishlist = QtWidgets.QWidget() ui = Ui_Wishlist() ui.setupUi(Wishlist) Wishlist.show() diff --git a/rare/ui/components/tabs/store/wishlist.ui b/rare/ui/components/tabs/store/wishlist.ui index b10eea91..d101e023 100644 --- a/rare/ui/components/tabs/store/wishlist.ui +++ b/rare/ui/components/tabs/store/wishlist.ui @@ -1,184 +1,92 @@ Wishlist - + 0 0 - 736 - 398 + 489 + 165 - StackedWidget + Wishlist - - - - - - true - - - - - 0 - 0 - 716 - 378 - + + + + + + + + + + + + + Reverse - - - - - - 15 - - - - Wishlist - - - - - - - - - - 0 - 0 - - - - Sort by - - - - - - - - Name - - - - - Price - - - - - Developer - - - - - Discount - - - - - - - - Reverse - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Filter: - - - - - - - - None - - - - - Discount - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - No games matching your filter - - - - - - - Qt::Vertical - - - - 379 - 218 - - - - - + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 473 + 115 + + + + + + + No games matching your filter + + + + - - - + + + diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index f126f769..a1719765 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -17,7 +17,7 @@ from rare.utils.qt_requests import QtRequests logger = getLogger("ExtraWidgets") - +# FIXME: move this? class WaitingSpinner(QLabel): def __init__(self, parent=None): super(WaitingSpinner, self).__init__(parent=parent) diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py index ebd1003f..02a9c5a2 100644 --- a/rare/utils/qt_requests.py +++ b/rare/utils/qt_requests.py @@ -5,7 +5,7 @@ from typing import Callable, Dict, TypeVar, List, Tuple from typing import Union import orjson -from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QUrlQuery, pyqtSlot +from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QUrlQuery, pyqtSlot, QJsonDocument from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QNetworkDiskCache USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" @@ -66,7 +66,7 @@ class QtRequests(QObject): def __post(self, item: RequestQueueItem): request = self.__prepare_request(item) - payload = orjson.dumps(item.payload) # pylint: disable=maybe-no-member + payload = orjson.dumps(item.payload) reply = self.manager.post(request, payload) reply.errorOccurred.connect(self.__on_error) self.__active_requests[reply] = item diff --git a/requirements-dev.txt b/requirements-dev.txt index 96eb2180..1435147b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,3 +7,4 @@ nuitka ordered-set PyQt5-stubs qstylizer +graphql-query diff --git a/requirements-full.txt b/requirements-full.txt index 55b8e26f..a2754f33 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -13,3 +13,4 @@ pythonnet>=3.0.0rc4; platform_system == "Windows" cefpython3; platform_system == "Windows" pywebview[cef]; platform_system == "Windows" pypresence + diff --git a/requirements.txt b/requirements.txt index 4a9abb8f..a83fe85c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system orjson vdf; platform_system == "Linux" or platform_system == "FreeBSD" pywin32; platform_system == "Windows" +