diff --git a/rare/components/tabs/store/__init__.py b/rare/components/tabs/store/__init__.py index 811fbe14..a0ed5ebc 100644 --- a/rare/components/tabs/store/__init__.py +++ b/rare/components/tabs/store/__init__.py @@ -1,12 +1,11 @@ from legendary.core import LegendaryCore -from rare.shared import RareCore from rare.utils.paths import cache_dir from rare.widgets.side_tab import SideTabWidget +from .api.models.response import CatalogOfferModel from .game_info import ShopGameInfo from .search_results import SearchResults from .shop_api_core import ShopApiCore -from .api.models.response import CatalogOfferModel from .shop_widget import ShopWidget from .wishlist import WishlistWidget, Wishlist @@ -28,11 +27,11 @@ class StoreTab(SideTabWidget): self.shop = ShopWidget(cache_dir(), self.core, self.api_core, parent=self) self.shop_index = self.addTab(self.shop, self.tr("Store")) self.shop.show_game.connect(self.show_game) - self.shop.show_info.connect(self.show_search) + # self.shop.show_info.connect(self.show_search) - self.search = SearchResults(self.api_core, parent=self) - self.search_index = self.addTab(self.search, self.tr("Search"), self.tr("Results")) - self.search.show_info.connect(self.show_game) + # self.search = SearchResults(self.api_core, parent=self) + # self.search_index = self.addTab(self.search, self.tr("Search"), self.tr("Results")) + # self.search.show_info.connect(self.show_game) # self.search.back_button.clicked.connect(lambda: self.setCurrentIndex(self.shop_index)) self.info = ShopGameInfo( @@ -67,6 +66,6 @@ class StoreTab(SideTabWidget): self.info.update_game(data) self.setCurrentIndex(self.info_index) - def show_search(self, text: str): - self.search.load_results(text) - self.setCurrentIndex(self.search_index) + # def show_search(self, text: str): + # self.search.load_results(text) + # self.setCurrentIndex(self.search_index) diff --git a/rare/components/tabs/store/__main__.py b/rare/components/tabs/store/__main__.py index a7366a13..b4936fd1 100644 --- a/rare/components/tabs/store/__main__.py +++ b/rare/components/tabs/store/__main__.py @@ -22,9 +22,9 @@ class StoreWindow(QDialog): if __name__ == "__main__": - from rare.utils.misc import set_style_sheet 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") diff --git a/rare/components/tabs/store/api/debug.py b/rare/components/tabs/store/api/debug.py index 083fcca2..24a20016 100644 --- a/rare/components/tabs/store/api/debug.py +++ b/rare/components/tabs/store/api/debug.py @@ -1,7 +1,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QTreeView, QDialog, QVBoxLayout -from utils.json_formatter import QJsonModel +from rare.utils.json_formatter import QJsonModel class DebugView(QTreeView): diff --git a/rare/components/tabs/store/api/graphql/schema.graphql b/rare/components/tabs/store/api/graphql/schema.graphql index 6f3edfda..676fcfe5 100644 --- a/rare/components/tabs/store/api/graphql/schema.graphql +++ b/rare/components/tabs/store/api/graphql/schema.graphql @@ -38,4 +38,39 @@ type LineOfferRes { 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/query.py b/rare/components/tabs/store/api/models/query.py index 925cc500..f3f21ca8 100644 --- a/rare/components/tabs/store/api/models/query.py +++ b/rare/components/tabs/store/api/models/query.py @@ -6,11 +6,11 @@ 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() + 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-%dT' + # 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)}]" diff --git a/rare/components/tabs/store/api/models/response.py b/rare/components/tabs/store/api/models/response.py index 182985a1..6a7672f2 100644 --- a/rare/components/tabs/store/api/models/response.py +++ b/rare/components/tabs/store/api/models/response.py @@ -1,8 +1,10 @@ import logging from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import datetime from typing import List, Dict, Any, Type, Optional +from .utils import parse_date + logger = logging.getLogger("StoreApiModels") # lk: Typing overloads for unimplemented types @@ -13,171 +15,11 @@ CategoryModel = Dict CustomAttributeModel = Dict ItemModel = Dict SellerModel = Dict -OfferMappingModel = Dict +PageSandboxModel = Dict TagModel = Dict PromotionsModel = Dict -def parse_date(date: str): - return datetime.fromisoformat(date[:-1]).replace(tzinfo=timezone.utc) - - -@dataclass -class DieselSystemDetailItem: - p_type: Optional[str] = None - minimum: Optional[str] = None - recommended: Optional[str] = None - title: Optional[str] = None - unmapped: Dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_dict(cls: Type["DieselSystemDetailItem"], src: Dict[str, Any]) -> "DieselSystemDetailItem": - d = src.copy() - tmp = cls( - p_type=d.pop("_type", ""), - minimum=d.pop("minimum", ""), - recommended=d.pop("recommended", ""), - title=d.pop("title", ""), - ) - tmp.unmapped = d - return tmp - - -@dataclass -class DieselSystemDetail: - p_type: Optional[str] = None - details: Optional[List[DieselSystemDetailItem]] = None - system_type: Optional[str] = None - unmapped: Dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_dict(cls: Type["DieselSystemDetail"], src: Dict[str, Any]) -> "DieselSystemDetail": - d = src.copy() - _details = d.pop("details", []) - details = [] if _details else None - for item in _details: - detail = DieselSystemDetailItem.from_dict(item) - details.append(detail) - tmp = cls( - p_type=d.pop("_type", ""), - details=details, - system_type=d.pop("systemType", ""), - ) - tmp.unmapped = d - return tmp - - -@dataclass -class DieselSystemDetails: - p_type: Optional[str] = None - languages: Optional[List[str]] = None - rating: Optional[Dict] = None - systems: Optional[List[DieselSystemDetail]] = None - unmapped: Dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_dict(cls: Type["DieselSystemDetails"], src: Dict[str, Any]) -> "DieselSystemDetails": - d = src.copy() - _systems = d.pop("systems", []) - systems = [] if _systems else None - for item in _systems: - system = DieselSystemDetail.from_dict(item) - systems.append(system) - tmp = cls( - p_type=d.pop("_type", ""), - languages=d.pop("languages", []), - rating=d.pop("rating", {}), - systems=systems, - ) - tmp.unmapped = d - return tmp - - -@dataclass -class DieselProductAbout: - p_type: Optional[str] = None - desciption: Optional[str] = None - developer_attribution: Optional[str] = None - publisher_attribution: Optional[str] = None - short_description: Optional[str] = None - unmapped: Dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_dict(cls: Type["DieselProductAbout"], src: Dict[str, Any]) -> "DieselProductAbout": - d = src.copy() - tmp = cls( - p_type=d.pop("_type", ""), - desciption=d.pop("description", ""), - developer_attribution=d.pop("developerAttribution", ""), - publisher_attribution=d.pop("publisherAttribution", ""), - short_description=d.pop("shortDescription", ""), - ) - tmp.unmapped = d - return tmp - - -@dataclass -class DieselProductDetail: - p_type: Optional[str] = None - about: Optional[DieselProductAbout] = None - requirements: Optional[DieselSystemDetails] = None - social_links: Optional[DieselSocialLinks] = None - unmapped: Dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_dict(cls: Type["DieselProductDetail"], src: Dict[str, Any]) -> "DieselProductDetail": - d = src.copy() - about = DieselProductAbout.from_dict(x) if (x := d.pop("about"), {}) else None - requirements = DieselSystemDetails.from_dict(x) if (x := d.pop("requirements", {})) else None - tmp = cls( - p_type=d.pop("_type", ""), - about=about, - requirements=requirements, - social_links=d.pop("socialLinks", {}), - ) - tmp.unmapped = d - return tmp - - -@dataclass -class DieselProduct: - p_id: Optional[str] = None - p_images_: Optional[List[str]] = None - p_locale: Optional[str] = None - p_slug: Optional[str] = None - p_title: Optional[str] = None - p_url_pattern: Optional[str] = None - namespace: Optional[str] = None - pages: Optional[List["DieselProduct"]] = None - data: Optional[DieselProductDetail] = None - product_name: Optional[str] = None - unmapped: Dict[str, Any] = field(default_factory=dict) - - @classmethod - def from_dict(cls: Type["DieselProduct"], src: Dict[str, Any]) -> "DieselProduct": - d = src.copy() - _pages = d.pop("pages", []) - pages = [] if _pages else None - for item in _pages: - page = DieselProduct.from_dict(item) - pages.append(page) - data = DieselProductDetail.from_dict(x) if (x := d.pop("data", {})) else None - tmp = cls( - p_id=d.pop("_id", ""), - p_images_=d.pop("_images_", []), - p_locale=d.pop("_locale", ""), - p_slug=d.pop("_slug", ""), - p_title=d.pop("_title", ""), - p_url_pattern=d.pop("_urlPattern", ""), - namespace=d.pop("namespace", ""), - pages=pages, - data=data, - product_name=d.pop("productName", ""), - ) - tmp.unmapped = d - return tmp - - @dataclass class ImageUrlModel: type: Optional[str] = None @@ -271,14 +113,14 @@ LineOffersModel = Dict @dataclass -class PriceModel: +class GetPriceResModel: total_price: Optional[TotalPriceModel] = None fmt_price: Optional[FmtPriceModel] = None line_offers: Optional[LineOffersModel] = None unmapped: Dict[str, Any] = field(default_factory=dict) @classmethod - def from_dict(cls: Type["PriceModel"], src: Dict[str, Any]) -> "PriceModel": + def from_dict(cls: Type["GetPriceResModel"], src: Dict[str, Any]) -> "GetPriceResModel": d = src.copy() tmp = cls( total_price=d.pop("totalPrice", {}), @@ -302,9 +144,9 @@ class CatalogOfferModel: items: Optional[List[ItemModel]] = None key_images: Optional[KeyImagesModel] = None namespace: Optional[str] = None - offer_mappings: Optional[List[OfferMappingModel]] = None + offer_mappings: Optional[List[PageSandboxModel]] = None offer_type: Optional[str] = None - price: Optional[PriceModel] = None + price: Optional[GetPriceResModel] = None product_slug: Optional[str] = None promotions: Optional[PromotionsModel] = None seller: Optional[SellerModel] = None @@ -322,7 +164,7 @@ class CatalogOfferModel: effective_date = parse_date(x) if (x := d.pop("effectiveDate", "")) else None expiry_date = parse_date(x) if (x := d.pop("expiryDate", "")) else None key_images = KeyImagesModel.from_list(d.pop("keyImages", [])) - price = PriceModel.from_dict(x) if (x := d.pop("price", {})) else None + price = GetPriceResModel.from_dict(x) if (x := d.pop("price", {})) else None viewable_date = parse_date(x) if (x := d.pop("viewableDate", "")) else None tmp = cls( catalog_ns=d.pop("catalogNs", {}), diff --git a/rare/components/tabs/store/game_info.py b/rare/components/tabs/store/game_info.py index 19a27524..af591120 100644 --- a/rare/components/tabs/store/game_info.py +++ b/rare/components/tabs/store/game_info.py @@ -1,5 +1,4 @@ import logging -from pprint import pprint from typing import List from PyQt5.QtCore import Qt, QUrl @@ -12,14 +11,14 @@ from PyQt5.QtWidgets import ( QSizePolicy, ) -from rare.components.tabs.store.api.models.response import CatalogOfferModel, DieselProduct, DieselProductDetail -from rare.shared import LegendaryCoreSingleton -from rare.shared.image_manager import ImageSize +from rare.models.image import ImageSize from rare.ui.components.tabs.store.shop_game_info import Ui_ShopInfo from rare.utils.misc import icon -from rare.widgets.side_tab import SideTabWidget, SideTabContents from rare.widgets.elide_label import ElideLabel +from rare.widgets.side_tab import SideTabWidget, SideTabContents from .api.debug import DebugDialog +from .api.models.diesel import DieselProduct, DieselProductDetail +from .api.models.response import CatalogOfferModel from .image_widget import ShopImageWidget logger = logging.getLogger("ShopInfo") @@ -107,7 +106,7 @@ class ShopGameInfo(QWidget, SideTabContents): # init API request if slug: - self.api_core.get_game(offer.product_slug, is_bundle, self.data_received) + self.api_core.get_game_config_cms(offer.product_slug, is_bundle, self.data_received) # else: # self.data_received({}) self.offer = offer diff --git a/rare/components/tabs/store/game_widgets.py b/rare/components/tabs/store/game_widgets.py index 92adbc6b..b5488212 100644 --- a/rare/components/tabs/store/game_widgets.py +++ b/rare/components/tabs/store/game_widgets.py @@ -3,10 +3,9 @@ import logging from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtGui import QMouseEvent from PyQt5.QtWidgets import QPushButton -from orjson import orjson from rare.components.tabs.store.api.models.response import CatalogOfferModel -from rare.shared.image_manager import ImageSize +from rare.models.image import ImageSize from rare.utils.misc import icon from rare.utils.qt_requests import QtRequestManager from .api.debug import DebugDialog diff --git a/rare/components/tabs/store/image_widget.py b/rare/components/tabs/store/image_widget.py index ec925943..5d02b4f4 100644 --- a/rare/components/tabs/store/image_widget.py +++ b/rare/components/tabs/store/image_widget.py @@ -1,10 +1,7 @@ -from PyQt5.QtCore import QEvent, QObject from PyQt5.QtCore import Qt from PyQt5.QtGui import ( QPixmap, QImage, - QMovie, - QShowEvent, ) from PyQt5.QtWidgets import ( QWidget, @@ -17,51 +14,7 @@ from PyQt5.QtWidgets import ( from rare.utils.qt_requests import QtRequestManager from rare.widgets.image_widget import ImageWidget - - -class WaitingSpinner(QLabel): - def __init__(self, autostart=False, parent=None): - super(WaitingSpinner, self).__init__(parent=parent) - self.setObjectName(type(self).__name__) - self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) - self.movie = QMovie(":/images/loader.gif", parent=self) - self.setFixedSize(128, 128) - self.setMovie(self.movie) - if self.parent() is not None: - self.parent().installEventFilter(self) - if autostart: - self.movie.start() - - def __center_on_parent(self): - rect = self.rect() - rect.moveCenter(self.parent().contentsRect().center()) - self.setGeometry(rect) - - def event(self, e: QEvent) -> bool: - if e.type() == QEvent.ParentAboutToChange: - if self.parent() is not None: - self.parent().removeEventFilter(self) - if e.type() == QEvent.ParentChange: - if self.parent() is not None: - self.parent().installEventFilter(self) - return super().event(e) - - def showEvent(self, a0: QShowEvent) -> None: - self.__center_on_parent() - - def eventFilter(self, a0: QObject, a1: QEvent) -> bool: - if a0 is self.parent() and a1.type() == QEvent.Resize: - self.__center_on_parent() - return a0.event(a1) - return False - - def start(self): - self.setVisible(True) - self.movie.start() - - def stop(self): - self.setVisible(False) - self.movie.stop() +from rare.widgets.loading_widget import LoadingWidget class IconWidget(object): @@ -76,7 +29,7 @@ class IconWidget(object): # on-hover popup self.mini_widget = QWidget(parent=widget) self.mini_widget.setObjectName(f"{type(self).__name__}MiniWidget") - self.mini_widget.setFixedHeight(widget.height() // 4) + self.mini_widget.setFixedHeight(int(widget.height() // 3)) # game title self.title_label = QLabel(parent=self.mini_widget) @@ -134,7 +87,7 @@ class ShopImageWidget(ImageWidget): def __init__(self, manager: QtRequestManager, parent=None): super(ShopImageWidget, self).__init__(parent=parent) self.ui = IconWidget() - self.spinner = WaitingSpinner(parent=self) + self.spinner = LoadingWidget(parent=self) self.spinner.setVisible(False) self.manager = manager diff --git a/rare/components/tabs/store/search_results.py b/rare/components/tabs/store/search_results.py index 24ec3fd1..bbd43403 100644 --- a/rare/components/tabs/store/search_results.py +++ b/rare/components/tabs/store/search_results.py @@ -7,26 +7,24 @@ from PyQt5.QtWidgets import ( QLabel, QScrollArea, ) -from rare.shared.image_manager import ImageSize +from rare.models.image import ImageSize from rare.utils.qt_requests import QtRequestManager from rare.widgets.flow_layout import FlowLayout from rare.widgets.side_tab import SideTabContents -from .image_widget import ShopImageWidget - from .api.debug import DebugDialog from .api.models.response import CatalogOfferModel +from .image_widget import ShopImageWidget -class SearchResults(QScrollArea, SideTabContents): +class SearchResultsWidget(QScrollArea): show_info = pyqtSignal(CatalogOfferModel) def __init__(self, api_core, parent=None): - super(SearchResults, self).__init__(parent=parent) - self.implements_scrollarea = True + super(SearchResultsWidget, self).__init__(parent=parent) self.api_core = api_core self.results_container = QWidget(self) - self.results_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.results_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.results_layout = FlowLayout(self.results_container) self.setWidget(self.results_container) self.setWidgetResizable(True) @@ -61,6 +59,12 @@ class SearchResults(QScrollArea, SideTabContents): self.setEnabled(True) +class SearchResults(SearchResultsWidget, SideTabContents): + def __init__(self, api_core, parent=None): + super(SearchResults, self).__init__(api_core, parent=parent) + self.implements_scrollarea = True + + class SearchResultItem(ShopImageWidget): show_info = pyqtSignal(CatalogOfferModel) diff --git a/rare/components/tabs/store/shop_api_core.py b/rare/components/tabs/store/shop_api_core.py index 0510d692..4c018a8b 100644 --- a/rare/components/tabs/store/shop_api_core.py +++ b/rare/components/tabs/store/shop_api_core.py @@ -14,8 +14,8 @@ from rare.components.tabs.store.constants import ( from rare.utils.paths import cache_dir from rare.utils.qt_requests import QtRequestManager from .api.models.query import SearchStoreQuery +from .api.models.diesel import DieselProduct from .api.models.response import ( - DieselProduct, ResponseModel, CatalogOfferModel, ) @@ -152,13 +152,13 @@ class ShopApiCore(QObject): "query": search_query, "variables": browse_model.to_dict() } - debug = DebugDialog(payload["variables"], None) - debug.exec() + # debug = DebugDialog(payload["variables"], None) + # debug.exec() self.manager.post(graphql_url, lambda data: self.__handle_browse_games(data, handle_func), payload) def __handle_browse_games(self, data, handle_func): - debug = DebugDialog(data, None) - debug.exec() + # debug = DebugDialog(data, None) + # debug.exec() self.browse_active = False if data is None: data = {} @@ -181,15 +181,29 @@ class ShopApiCore(QObject): 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): + 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): - debug = DebugDialog(data, None) - debug.exec() + # debug = DebugDialog(data, None) + # debug.exec() try: product = DieselProduct.from_dict(data) handle_func(product) @@ -213,8 +227,8 @@ class ShopApiCore(QObject): self.authed_manager.post(graphql_url, lambda data: self._handle_add_to_wishlist(data, handle_func), payload) def _handle_add_to_wishlist(self, data, handle_func): - debug = DebugDialog(data, None) - debug.exec() + # debug = DebugDialog(data, None) + # debug.exec() try: response = ResponseModel.from_dict(data) data = response.data.wishlist.add_to_wishlist @@ -239,8 +253,8 @@ class ShopApiCore(QObject): payload) def _handle_remove_from_wishlist(self, data, handle_func): - debug = DebugDialog(data, None) - debug.exec() + # debug = DebugDialog(data, None) + # debug.exec() try: response = ResponseModel.from_dict(data) data = response.data.wishlist.remove_from_wishlist diff --git a/rare/components/tabs/store/shop_widget.py b/rare/components/tabs/store/shop_widget.py index 3489f9bb..4f61a7f5 100644 --- a/rare/components/tabs/store/shop_widget.py +++ b/rare/components/tabs/store/shop_widget.py @@ -7,9 +7,8 @@ from PyQt5.QtWidgets import ( QGroupBox, QCheckBox, QLabel, - QPushButton, QHBoxLayout, - QWidget, QSizePolicy, QStackedLayout, + QWidget, QSizePolicy, QScrollArea, QVBoxLayout, QFrame, QSpacerItem, ) from legendary.core import LegendaryCore @@ -17,15 +16,16 @@ from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.utils.extra_widgets import ButtonLineEdit from rare.widgets.flow_layout import FlowLayout from rare.widgets.side_tab import SideTabContents +from rare.widgets.sliding_stack import SlidingStackedWidget +from .store_widgets import ShopGroupBox from .api.models.query import SearchStoreQuery from .api.models.response import CatalogOfferModel, WishlistItemModel +from .api.models.utils import parse_date from .constants import Constants from .game_widgets import GameWidget -from .image_widget import WaitingSpinner +from .search_results import SearchResults from .shop_api_core import ShopApiCore -from .api.models.utils import parse_date - logger = logging.getLogger("Shop") @@ -47,39 +47,56 @@ class ShopWidget(QWidget, SideTabContents): self.types = [] self.update_games_allowed = True - self.ui.free_scrollarea.setDisabled(True) - self.free_game_widgets = [] self.active_search_request = False self.next_search = "" self.wishlist: List = [] - self.discounts_layout = QStackedLayout(self.ui.discounts_group) - self.discounts_spinner = WaitingSpinner(self.ui.discounts_group) - self.discounts_flow = QWidget(self.ui.discounts_group) - self.discounts_flow.setLayout(FlowLayout(self.discounts_flow)) - self.discounts_flow.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.discounts_layout.addWidget(self.discounts_spinner) - self.discounts_layout.addWidget(self.discounts_flow) + self.browse_scrollarea = QScrollArea(self) + self.browse_scrollarea.setWidgetResizable(True) + self.browse_scrollarea.setFrameStyle(QFrame.NoFrame | QFrame.Plain) + self.browse_container = QWidget(self.browse_scrollarea) + browse_contailer_layout = QVBoxLayout(self.browse_container) + browse_contailer_layout.setContentsMargins(0, 0, 3, 0) + self.browse_container.setLayout(browse_contailer_layout) + self.browse_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.browse_scrollarea.setWidget(self.browse_container) - self.discounts_spinner.start() - self.discounts_layout.setCurrentWidget(self.discounts_spinner) + # self.free_scrollarea = QScrollArea(self.browse_container) + # self.free_scrollarea.setWidgetResizable(True) + # self.free_scrollarea.setFrameStyle(QFrame.NoFrame | QFrame.Plain) + # self.free_container = QWidget(self.free_scrollarea) + # free_container_layout = QHBoxLayout(self.free_container) + # free_container_layout.setContentsMargins(0, 0, 0, 3) + # self.free_container.setLayout(free_container_layout) + # self.free_container.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + # self.free_scrollarea.setWidget(self.free_container) + # + # self.browse_container.layout().addWidget(self.free_scrollarea) - self.games_layout = QStackedLayout(self.ui.games_group) - self.games_spinner = WaitingSpinner(self.ui.games_group) - self.games_flow = QWidget(self.ui.games_group) - self.games_flow.setLayout(FlowLayout(self.games_flow)) - self.games_flow.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - self.games_layout.addWidget(self.games_spinner) - self.games_layout.addWidget(self.games_flow) + self.discounts_group = ShopGroupBox(self.tr("Discounts from your wishlist"), FlowLayout, self) + self.browse_container.layout().addWidget(self.discounts_group) + self.discounts_group.loading(True) - self.games_spinner.start() - self.games_layout.setCurrentWidget(self.games_spinner) + self.games_group = ShopGroupBox(self.tr("Games"), FlowLayout, self) + self.browse_container.layout().addWidget(self.games_group) + self.games_group.loading(True) + self.games_group.setVisible(False) + + self.browse_container.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Expanding)) + + self.search_scrollarea = SearchResults(self.api_core, self) self.search_bar = ButtonLineEdit( "fa.search", placeholder_text=self.tr("Search Games") ) - self.ui.main_layout.addWidget(self.search_bar, 0, 0) + self.ui.left_layout.addWidget(self.search_bar) + + self.browse_stack = SlidingStackedWidget(self) + self.browse_stack.setDirection(Qt.Vertical) + self.browse_stack.addWidget(self.browse_scrollarea) + self.browse_stack.addWidget(self.search_scrollarea) + self.ui.left_layout.addWidget(self.browse_stack) # self.search_bar.textChanged.connect(self.search_games) @@ -102,8 +119,8 @@ class ShopWidget(QWidget, SideTabContents): self.api_core.get_wishlist(self.add_wishlist_items) def add_wishlist_items(self, wishlist: List[WishlistItemModel]): - for w in self.discounts_flow.findChildren(QWidget, options=Qt.FindDirectChildrenOnly): - self.discounts_flow.layout().removeWidget(w) + for w in self.discounts_group.findChildren(QWidget, options=Qt.FindDirectChildrenOnly): + self.discounts_group.layout().removeWidget(w) w.deleteLater() # if wishlist and wishlist[0] == "error": @@ -126,50 +143,46 @@ class ShopWidget(QWidget, SideTabContents): if game.offer.price.total_price["discount"] > 0: w = GameWidget(self.api_core.cached_manager, game.offer) w.show_info.connect(self.show_game) - self.discounts_flow.layout().addWidget(w) + self.discounts_group.layout().addWidget(w) discounts += 1 except Exception as e: logger.warning(f"{game} {e}") continue - self.ui.discounts_group.setVisible(discounts > 0) - self.discounts_layout.setCurrentWidget(self.discounts_flow) + self.discounts_group.setVisible(discounts > 0) + self.discounts_group.loading(False) # FIXME: FlowLayout doesn't update on adding widget - self.discounts_flow.layout().update() + self.discounts_group.layout().update() def add_free_games(self, free_games: List[CatalogOfferModel]): - for w in self.ui.free_container.layout().findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly): - self.ui.free_container.layout().removeWidget(w) + for w in self.browse_container.layout().findChildren(QGroupBox, options=Qt.FindDirectChildrenOnly): + self.browse_container.layout().removeWidget(w) w.deleteLater() - if free_games and free_games[0] == "error": - self.ui.free_container.layout().addWidget( - QLabel(self.tr("Failed to fetch free games: {}").format(free_games[1])) - ) - btn = QPushButton(self.tr("Reload")) - self.ui.free_container.layout().addWidget(btn) - btn.clicked.connect( - lambda: self.api_core.get_free_games(self.add_free_games) - ) - self.ui.free_container.setEnabled(True) - return + # if free_games and free_games[0] == "error": + # self.ui.free_container.layout().addWidget( + # QLabel(self.tr("Failed to fetch free games: {}").format(free_games[1])) + # ) + # btn = QPushButton(self.tr("Reload")) + # self.ui.free_container.layout().addWidget(btn) + # btn.clicked.connect( + # lambda: self.api_core.get_free_games(self.add_free_games) + # ) + # self.ui.free_container.setEnabled(True) + # return - self.free_games_now = QGroupBox(self.tr("Free now"), parent=self.ui.free_container) - free_games_now_layout = QHBoxLayout(self.free_games_now) - # free_games_now_layout.setContentsMargins(0, 0, 0, 0) - self.free_games_now.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - self.free_games_now.setLayout(free_games_now_layout) - self.ui.free_container.layout().addWidget(self.free_games_now) + free_games_now = ShopGroupBox(self.tr("Free now"), layouting=QHBoxLayout, parent=self.browse_container) + free_games_now.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # self.free_games_now.setLayout(free_games_now_layout) + self.browse_container.layout().insertWidget(0, free_games_now) - self.free_games_next = QGroupBox(self.tr("Free next week"), parent=self.ui.free_container) - free_games_next_layout = QHBoxLayout(self.free_games_next) - # free_games_next_layout.setContentsMargins(0, 0, 0, 0) - self.free_games_next.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - self.free_games_next.setLayout(free_games_next_layout) - self.ui.free_container.layout().addWidget(self.free_games_next) + free_games_next = ShopGroupBox(self.tr("Free next week"), layouting=QHBoxLayout, parent=self.browse_container) + free_games_next.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + # self.free_games_next.setLayout(free_games_next_layout) + self.browse_container.layout().insertWidget(1, free_games_next) date = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc) - free_games_now = [] - coming_free_games = [] + free_now_list = [] + free_next_list = [] for game in free_games: try: if ( @@ -177,11 +190,11 @@ class ShopWidget(QWidget, SideTabContents): and game.price.total_price["fmtPrice"]["originalPrice"] != game.price.total_price["fmtPrice"]["discountPrice"] ): - free_games_now.append(game) + free_now_list.append(game) continue if game.title == "Mystery Game": - coming_free_games.append(game) + free_next_list.append(game) continue except KeyError as e: logger.warning(str(e)) @@ -206,39 +219,43 @@ class ShopWidget(QWidget, SideTabContents): continue if start_date > date: - coming_free_games.append(game) + free_next_list.append(game) # free games now now_free = 0 - for free_game in free_games_now: + for free_game in free_now_list: w = GameWidget(self.api_core.cached_manager, free_game) w.show_info.connect(self.show_game) - self.free_games_now.layout().addWidget(w) - self.free_game_widgets.append(w) + 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( + free_games_now.layout().addWidget( QLabel(self.tr("Could not find current free game")) ) + free_games_now.loading(False) # free games next week - for free_game in coming_free_games: + for free_game in free_next_list: w = GameWidget(self.api_core.cached_manager, free_game) if free_game.title != "Mystery Game": w.show_info.connect(self.show_game) - self.free_games_next.layout().addWidget(w) + free_games_next.layout().addWidget(w) + free_games_next.loading(False) # self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300)) - self.ui.free_scrollarea.setMinimumHeight( - self.free_games_now.sizeHint().height() - + self.ui.free_container.layout().contentsMargins().top() - + self.ui.free_container.layout().contentsMargins().bottom() - + self.ui.free_scrollarea.horizontalScrollBar().sizeHint().height() - ) - self.ui.free_scrollarea.setEnabled(True) + # self.free_scrollarea.setMinimumHeight( + # free_games_now.sizeHint().height() + # + self.free_container.layout().contentsMargins().top() + # + self.free_container.layout().contentsMargins().bottom() + # + self.free_scrollarea.horizontalScrollBar().sizeHint().height() + # ) + # self.free_scrollarea.setEnabled(True) def show_search_results(self): if self.search_bar.text(): - self.show_info.emit(self.search_bar.text()) + self.browse_stack.slideInWidget(self.search_scrollarea) + self.search_scrollarea.load_results(self.search_bar.text()) + # self.show_info.emit(self.search_bar.text()) def init_filter(self): self.ui.none_price.toggled.connect( @@ -299,6 +316,7 @@ class ShopWidget(QWidget, SideTabContents): ) def reset_filters(self): + self.browse_stack.slideInWidget(self.browse_scrollarea) self.update_games_allowed = False for cb in self.checkboxes: cb.setChecked(False) @@ -334,14 +352,14 @@ class ShopWidget(QWidget, SideTabContents): 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.ui.free_scrollarea.setVisible(False) - self.ui.discounts_group.setVisible(False) + # self.free_scrollarea.setVisible(False) + self.discounts_group.setVisible(False) else: - self.ui.free_scrollarea.setVisible(True) - if len(self.ui.discounts_group.layout().children()) > 0: - self.ui.discounts_group.setVisible(True) + # self.free_scrollarea.setVisible(True) + if len(self.discounts_group.layout().children()) > 0: + self.discounts_group.setVisible(True) - self.games_layout.setCurrentWidget(self.games_spinner) + self.games_group.loading(True) browse_model = SearchStoreQuery( language=self.core.language_code, @@ -357,22 +375,22 @@ class ShopWidget(QWidget, SideTabContents): self.api_core.browse_games(browse_model, self.show_games) def show_games(self, data): - for w in self.games_flow.findChildren(QWidget, options=Qt.FindDirectChildrenOnly): - self.games_flow.layout().removeWidget(w) + for w in self.games_group.findChildren(QWidget, options=Qt.FindDirectChildrenOnly): + self.games_group.layout().removeWidget(w) w.deleteLater() if data: for game in data: w = GameWidget(self.api_core.cached_manager, game) w.show_info.connect(self.show_game) - self.games_flow.layout().addWidget(w) + self.games_group.layout().addWidget(w) else: - self.games_flow.layout().addWidget( + self.games_group.layout().addWidget( QLabel(self.tr("Could not get games matching the filter")) ) - self.games_layout.setCurrentWidget(self.games_flow) + self.games_group.loading(False) # FIXME: FlowLayout doesn't update on adding widget - self.games_flow.layout().update() + self.games_group.layout().update() class CheckBox(QCheckBox): diff --git a/rare/components/tabs/store/wishlist.py b/rare/components/tabs/store/wishlist.py index b8fe7277..abb9f0f2 100644 --- a/rare/components/tabs/store/wishlist.py +++ b/rare/components/tabs/store/wishlist.py @@ -5,11 +5,11 @@ from PyQt5.QtWidgets import QMessageBox, QWidget from rare.ui.components.tabs.store.wishlist import Ui_Wishlist from rare.utils.misc import icon -from rare.widgets.side_tab import SideTabContents from rare.widgets.flow_layout import FlowLayout -from .shop_api_core import ShopApiCore -from .game_widgets import WishlistWidget +from rare.widgets.side_tab import SideTabContents from .api.models.response import WishlistItemModel, CatalogOfferModel +from .game_widgets import WishlistWidget +from .shop_api_core import ShopApiCore class Wishlist(QWidget, SideTabContents): diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py index 14ea03df..f3b0ef4a 100644 --- a/rare/ui/components/tabs/store/store.py +++ b/rare/ui/components/tabs/store/store.py @@ -16,8 +16,16 @@ class Ui_ShopWidget(object): ShopWidget.setObjectName("ShopWidget") ShopWidget.resize(788, 662) ShopWidget.setWindowTitle("Store") - self.main_layout = QtWidgets.QGridLayout(ShopWidget) + self.main_layout = QtWidgets.QHBoxLayout(ShopWidget) 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(ShopWidget) + self.reset_button.setObjectName("reset_button") + self.right_layout.addWidget(self.reset_button) self.filter_scrollarea = QtWidgets.QScrollArea(ShopWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -89,59 +97,15 @@ class Ui_ShopWidget(object): self.others_layout.setObjectName("others_layout") self.filter_container_layout.addWidget(self.others_group) self.filter_scrollarea.setWidget(self.filter_container) - self.main_layout.addWidget(self.filter_scrollarea, 1, 1, 1, 1) - self.reset_button = QtWidgets.QPushButton(ShopWidget) - self.reset_button.setObjectName("reset_button") - self.main_layout.addWidget(self.reset_button, 0, 1, 1, 1) - self.games_scrollarea = QtWidgets.QScrollArea(ShopWidget) - self.games_scrollarea.setFrameShape(QtWidgets.QFrame.NoFrame) - self.games_scrollarea.setFrameShadow(QtWidgets.QFrame.Plain) - self.games_scrollarea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) - self.games_scrollarea.setWidgetResizable(True) - self.games_scrollarea.setObjectName("games_scrollarea") - self.games_container = QtWidgets.QWidget() - self.games_container.setGeometry(QtCore.QRect(0, 0, 628, 618)) - self.games_container.setObjectName("games_container") - self.games_container_layout = QtWidgets.QVBoxLayout(self.games_container) - self.games_container_layout.setContentsMargins(0, 0, 3, 0) - self.games_container_layout.setObjectName("games_container_layout") - self.free_scrollarea = QtWidgets.QScrollArea(self.games_container) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.free_scrollarea.sizePolicy().hasHeightForWidth()) - self.free_scrollarea.setSizePolicy(sizePolicy) - self.free_scrollarea.setFrameShape(QtWidgets.QFrame.NoFrame) - self.free_scrollarea.setFrameShadow(QtWidgets.QFrame.Plain) - self.free_scrollarea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.free_scrollarea.setWidgetResizable(True) - self.free_scrollarea.setObjectName("free_scrollarea") - self.free_container = QtWidgets.QWidget() - self.free_container.setGeometry(QtCore.QRect(0, 0, 16, 16)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.free_container.sizePolicy().hasHeightForWidth()) - self.free_container.setSizePolicy(sizePolicy) - self.free_container.setObjectName("free_container") - self.free_container_layout = QtWidgets.QHBoxLayout(self.free_container) - self.free_container_layout.setContentsMargins(0, 0, 0, 3) - self.free_container_layout.setObjectName("free_container_layout") - self.free_scrollarea.setWidget(self.free_container) - self.games_container_layout.addWidget(self.free_scrollarea) - self.discounts_group = QtWidgets.QGroupBox(self.games_container) - self.discounts_group.setObjectName("discounts_group") - self.games_container_layout.addWidget(self.discounts_group) - self.games_group = QtWidgets.QGroupBox(self.games_container) - self.games_group.setObjectName("games_group") - self.games_container_layout.addWidget(self.games_group) - self.games_scrollarea.setWidget(self.games_container) - self.main_layout.addWidget(self.games_scrollarea, 1, 0, 1, 1) + self.right_layout.addWidget(self.filter_scrollarea) + self.main_layout.addLayout(self.right_layout) + self.main_layout.setStretch(0, 1) self.retranslateUi(ShopWidget) def retranslateUi(self, ShopWidget): _translate = QtCore.QCoreApplication.translate + self.reset_button.setText(_translate("ShopWidget", "Reset filters")) self.price_group.setTitle(_translate("ShopWidget", "Price")) self.none_price.setText(_translate("ShopWidget", "None")) self.free_button.setText(_translate("ShopWidget", "Free")) @@ -154,9 +118,6 @@ class Ui_ShopWidget(object): self.genre_group.setTitle(_translate("ShopWidget", "Genre")) self.type_group.setTitle(_translate("ShopWidget", "Type")) self.others_group.setTitle(_translate("ShopWidget", "Other tags")) - self.reset_button.setText(_translate("ShopWidget", "Reset filters")) - self.discounts_group.setTitle(_translate("ShopWidget", "Discounts from your wishlist")) - self.games_group.setTitle(_translate("ShopWidget", "Games")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/store/store.ui b/rare/ui/components/tabs/store/store.ui index c46a8bf1..edd268a9 100644 --- a/rare/ui/components/tabs/store/store.ui +++ b/rare/ui/components/tabs/store/store.ui @@ -13,267 +13,168 @@ Store - - - - - - 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 - - - - - - - + + + - - - - Reset filters - - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - QAbstractScrollArea::AdjustToContents - - - true - - - - - 0 - 0 - 628 - 618 - - - - - 0 + + + + + + Reset filters - - 0 + + + + + + + 0 + 0 + - - 3 + + QFrame::NoFrame - - 0 + + QFrame::Plain - - - - - 0 - 0 - + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + true + + + + + 0 + 0 + 142 + 390 + + + + + 0 + 0 + + + + + 0 - - QFrame::NoFrame + + 0 - - QFrame::Plain + + 3 - - Qt::ScrollBarAlwaysOff + + 0 - - true - - - - - 0 - 0 - 16 - 16 - - - - - 0 - 0 - - - - - 0 + + + + Price - - 0 + + + + + None + + + true + + + + + + + Free + + + + + + + Under 10 + + + + + + + Under 20 + + + + + + + Under 30 + + + + + + + 14.99 and above + + + + + + + Discount + + + + + + + + + + Platform - - 0 + + + + + + + Genre - - 3 + + + + + + + Type - - - - - - - - Discounts from your wishlist - - - - - - - Games - - - - - - + + + + + + + Other tags + + + + + + + + + diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py index b859fa61..3a88903e 100644 --- a/rare/utils/qt_requests.py +++ b/rare/utils/qt_requests.py @@ -67,7 +67,7 @@ class QtRequestManager(QObject): def __post(self, item: RequestQueueItem): request = self.__prepare_request(item) - payload = orjson.dumps(item.payload) + payload = orjson.dumps(item.payload) # pylint: disable=maybe-no-member reply = self.manager.post(request, payload) reply.errorOccurred.connect(self.__on_error) self.__active_requests[reply] = item @@ -128,7 +128,7 @@ class QtRequestManager(QObject): maintype, subtype = mimetype.split("/") bin_data = reply.readAll().data() if mimetype == "application/json": - data = orjson.loads(bin_data) + data = orjson.loads(bin_data) # pylint: disable=maybe-no-member elif maintype == "image": data = bin_data else: