From b31080a5aeb7801d5ca274f9a3b368ccbe8a8c3e Mon Sep 17 00:00:00 2001 From: Dummerle Date: Thu, 3 Jun 2021 23:23:55 +0200 Subject: [PATCH 01/30] Add basic store --- custom_legendary/models/game.py | 36 +++ rare/components/tab_widget.py | 14 +- .../game_widgets/installed_icon_widget.py | 3 +- .../game_widgets/uninstalled_icon_widget.py | 3 +- rare/components/tabs/shop/__init__.py | 28 +++ rare/components/tabs/shop/shop_info.py | 35 +++ rare/components/tabs/shop/shop_widget.py | 209 ++++++++++++++++++ .../components/tabs/store/shop_game_info.py | 61 +++++ .../components/tabs/store/shop_game_info.ui | 78 +++++++ rare/ui/components/tabs/store/store.py | 80 +++++++ rare/ui/components/tabs/store/store.ui | 95 ++++++++ rare/utils/extra_widgets.py | 7 - rare/utils/models.py | 48 ++++ test.py | 20 ++ 14 files changed, 704 insertions(+), 13 deletions(-) create mode 100644 rare/components/tabs/shop/__init__.py create mode 100644 rare/components/tabs/shop/shop_info.py create mode 100644 rare/components/tabs/shop/shop_widget.py create mode 100644 rare/ui/components/tabs/store/shop_game_info.py create mode 100644 rare/ui/components/tabs/store/shop_game_info.ui create mode 100644 rare/ui/components/tabs/store/store.py create mode 100644 rare/ui/components/tabs/store/store.ui create mode 100644 test.py diff --git a/custom_legendary/models/game.py b/custom_legendary/models/game.py index 16e2d790..b2ea5682 100644 --- a/custom_legendary/models/game.py +++ b/custom_legendary/models/game.py @@ -144,3 +144,39 @@ class VerifyResult(Enum): HASH_MISMATCH = 1 FILE_MISSING = 2 OTHER_ERROR = 3 + + +x = {'title': 'Frostpunk', + 'id': 'b43c1e1e0ca14b6784b323c59c751136', 'namespace': 'd5241c76f178492ea1540fce45616757', + 'description': 'Frostpunk', 'effectiveDate': '2099-01-01T00:00:00.000Z', 'offerType': 'OTHERS', 'expiryDate': None, + 'status': 'ACTIVE', 'isCodeRedemptionOnly': True, 'keyImages': [{'type': 'VaultClosed', + 'url': 'https://cdn1.epicgames.com/d5241c76f178492ea1540fce45616757/offer/EpicVault_Clean_OPEN_V10_LightsON-1920x1080-75e6d0636a6083944570a1c6f94ead4f.png'}, + {'type': 'DieselStoreFrontWide', + 'url': 'https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_Frostpunk_wide_2560x1440-ef2f4d458120af0839dde35b1a022828'}, + {'type': 'DieselStoreFrontTall', + 'url': 'https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_Frostpunk_Tall_1200x1600-c71dc27cfe505c6c662c49011b36a0c5'}], + 'seller': {'id': 'o-ufmrk5furrrxgsp5tdngefzt5rxdcn', 'name': 'Epic Dev Test Account'}, 'productSlug': 'frostpunk', + 'urlSlug': 'free-games-06', 'url': None, + 'items': [{'id': '8341d7c7e4534db7848cc428aa4cbe5a', 'namespace': 'd5241c76f178492ea1540fce45616757'}], + 'customAttributes': [{'key': 'com.epicgames.app.freegames.vault.close', 'value': '[]'}, + {'key': 'com.epicgames.app.blacklist', 'value': '[]'}, + {'key': 'com.epicgames.app.freegames.vault.slug', + 'value': 'news/the-epic-mega-sale-returns-for-2021'}, + {'key': 'publisherName', 'value': '11 bit studios'}, {'key': 'dupe', 'value': '[]'}, + {'key': 'com.epicgames.app.freegames.vault.open', 'value': '[]'}, + {'key': 'developerName', 'value': '11 bit studios'}, + {'key': 'com.epicgames.app.productSlug', 'value': 'frostpunk'}], + 'categories': [{'path': 'freegames/vaulted'}, {'path': 'freegames'}, {'path': 'games'}, {'path': 'applications'}], + 'tags': [], 'price': {'totalPrice': {'discountPrice': 0, 'originalPrice': 0, 'voucherDiscount': 0, 'discount': 0, + 'currencyCode': 'USD', 'currencyInfo': {'decimals': 2}, + 'fmtPrice': {'originalPrice': '0', 'discountPrice': '0', + 'intermediatePrice': '0'}}, + 'lineOffers': [{'appliedRules': []}]}, 'promotions': {'promotionalOffers': [], + 'upcomingPromotionalOffers': [{ + 'promotionalOffers': [ + { + 'startDate': '2021-06-03T15:00:00.000Z', + 'endDate': '2021-06-10T15:00:00.000Z', + 'discountSetting': { + 'discountType': 'PERCENTAGE', + 'discountPercentage': 0}}]}]}} diff --git a/rare/components/tab_widget.py b/rare/components/tab_widget.py index fe0213f2..566e08bf 100644 --- a/rare/components/tab_widget.py +++ b/rare/components/tab_widget.py @@ -13,6 +13,7 @@ from rare.components.tabs.cloud_saves import SyncSaves from rare.components.tabs.downloads import DownloadTab from rare.components.tabs.games import GameTab from rare.components.tabs.settings import SettingsTab +from rare.components.tabs.shop import Shop from rare.utils import legendary_utils from rare.utils.models import InstallQueueItemModel, InstallOptionsModel @@ -22,7 +23,7 @@ class TabWidget(QTabWidget): def __init__(self, core: LegendaryCore, parent, offline): super(TabWidget, self).__init__(parent=parent) - disabled_tab = 3 if not offline else 1 + disabled_tab = 4 if not offline else 1 self.core = core self.setTabBar(TabBar(disabled_tab)) @@ -38,6 +39,9 @@ class TabWidget(QTabWidget): self.cloud_saves = SyncSaves(core, self) self.addTab(self.cloud_saves, "Cloud Saves") + self.store = Shop() + self.addTab(self.store, self.tr("Store")) + self.settings = SettingsTab(core, self) # Space Tab @@ -92,9 +96,15 @@ class TabWidget(QTabWidget): self.games_tab.default_widget.game_list.game_exited.connect(self.game_finished) # Open game list on click on Games tab button - self.tabBarClicked.connect(lambda x: self.games_tab.layout.setCurrentIndex(0) if x == 0 else None) + self.tabBarClicked.connect(self.mouse_clicked) self.setIconSize(QSize(25, 25)) + def mouse_clicked(self, tab_num): + if tab_num == 0: + self.games_tab.layout.setCurrentIndex(0) + if tab_num == 3: + self.store.load() + def install_game(self, app_name, disable_path=False): install_dialog = InstallDialog(self.core, InstallQueueItemModel(options=InstallOptionsModel(app_name=app_name)), diff --git a/rare/components/tabs/games/game_widgets/installed_icon_widget.py b/rare/components/tabs/games/game_widgets/installed_icon_widget.py index e78e753b..a52e86f0 100644 --- a/rare/components/tabs/games/game_widgets/installed_icon_widget.py +++ b/rare/components/tabs/games/game_widgets/installed_icon_widget.py @@ -8,7 +8,6 @@ from qtawesome import icon from custom_legendary.core import LegendaryCore from custom_legendary.models.game import InstalledGame from rare.components.tabs.games.game_widgets.base_installed_widget import BaseInstalledWidget -from rare.utils.extra_widgets import ClickableLabel logger = getLogger("GameWidgetInstalled") @@ -36,7 +35,7 @@ class GameWidgetInstalled(BaseInstalledWidget): if self.pixmap: w = 200 self.pixmap = self.pixmap.scaled(w, int(w * 4 / 3), transformMode=Qt.SmoothTransformation) - self.image = ClickableLabel() + self.image = QLabel() self.image.setObjectName("game_widget") self.image.setPixmap(self.pixmap) self.layout.addWidget(self.image) diff --git a/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py b/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py index afa5bbef..bdd8b14f 100644 --- a/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py +++ b/rare/components/tabs/games/game_widgets/uninstalled_icon_widget.py @@ -5,7 +5,6 @@ from PyQt5.QtWidgets import QVBoxLayout, QLabel from custom_legendary.core import LegendaryCore from custom_legendary.models.game import Game from rare.components.tabs.games.game_widgets.base_uninstalled_widget import BaseUninstalledWidget -from rare.utils.extra_widgets import ClickableLabel logger = getLogger("Uninstalled") @@ -19,7 +18,7 @@ class IconWidgetUninstalled(BaseUninstalledWidget): if self.pixmap: w = 200 self.pixmap = self.pixmap.scaled(w, int(w * 4 / 3)) - self.image = ClickableLabel() + self.image = QLabel() self.image.setPixmap(self.pixmap) self.layout.addWidget(self.image) diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py new file mode 100644 index 00000000..1b883bd7 --- /dev/null +++ b/rare/components/tabs/shop/__init__.py @@ -0,0 +1,28 @@ +from PyQt5.QtWidgets import QStackedWidget, QWidget, QVBoxLayout, QLabel + +from rare.components.tabs.shop.shop_info import ShopGameInfo +from rare.components.tabs.shop.shop_widget import ShopWidget + + +class Shop(QStackedWidget): + init = False + def __init__(self): + super(Shop, self).__init__() + + self.shop = ShopWidget() + self.addWidget(self.shop) + + self.info = ShopGameInfo() + self.addWidget(self.info) + + self.shop.show_info.connect(self.show_info) + + def load(self): + if not self.init: + self.init = True + self.shop.load() + + + def show_info(self, slug): + self.info.update_game(slug) + self.setCurrentIndex(1) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py new file mode 100644 index 00000000..a331a643 --- /dev/null +++ b/rare/components/tabs/shop/shop_info.py @@ -0,0 +1,35 @@ +import os +import webbrowser + +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtGui import QPixmap +from PyQt5.QtNetwork import QNetworkAccessManager +from PyQt5.QtWidgets import QWidget + +from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info +from rare.utils import api_utils + + +class ShopGameInfo(QWidget, Ui_shop_info): + slug = "" + + def __init__(self): + super(ShopGameInfo, self).__init__() + self.setupUi(self) + self.pushButton.clicked.connect(self.button_clicked) + self.manager = QNetworkAccessManager() + + def update_game(self, slug: str): + locale = QLocale.system().name().split("_")[0] + game = api_utils.get_product(slug, locale) + self.slug = slug + self.title.setText(game[0]["productName"]) + self.image.setPixmap( + QPixmap(os.path.expanduser(f"~/.cache/rare/cache/{game[0]['productName']}.png")).scaled(180, + int(180 * 9 / 16), + transformMode=Qt.SmoothTransformation)) + + self.dev.setText(game[0]["data"]["meta"]["developer"][0]) + + def button_clicked(self): + webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py new file mode 100644 index 00000000..efc0ce18 --- /dev/null +++ b/rare/components/tabs/shop/shop_widget.py @@ -0,0 +1,209 @@ +import datetime +import json +import logging +import os +from json import JSONDecodeError + +import requests +from PyQt5 import QtGui +from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QByteArray, QJsonDocument, QJsonParseError, QObjectCleanupHandler, \ + QStringListModel +from PyQt5.QtGui import QPixmap +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QCompleter + +from rare.ui.components.tabs.store.store import Ui_ShopWidget +from rare.utils.extra_widgets import WaitingSpinner +from rare.utils.utils import get_lang + + +class ShopWidget(QWidget, Ui_ShopWidget): + show_info = pyqtSignal(str) + free_game_widgets = [] + + def __init__(self): + super(ShopWidget, self).__init__() + self.setupUi(self) + self.search_results.setVisible(False) + self.manager = QNetworkAccessManager() + self.free_games_stack.addWidget(WaitingSpinner()) + self.free_games_stack.setCurrentIndex(1) + self.search.textChanged.connect(self.search_games) + self.completer = QCompleter() + self.completer.setCaseSensitivity(Qt.CaseInsensitive) + self.search.setCompleter(self.completer) + self.search.returnPressed.connect(self.show_game) + self.data = [] + + def load(self): + if not os.path.exists(p := os.path.expanduser(f"~/.cache/rare/cache/")): + os.makedirs(p) + url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" + self.free_game_request = self.manager.get(QNetworkRequest(QUrl(url))) + self.free_game_request.readyRead.connect(self.add_free_games) + self.free_game_request.finished.connect(self.free_game_request.deleteLater if self.free_game_request else None) + + # free_games = api_utils.get_free_games() + + def add_free_games(self): + if self.free_game_request: + if self.free_game_request.error() == QNetworkReply.NoError: + try: + free_games = json.loads(self.free_game_request.readAll().data().decode()) + except JSONDecodeError: + print(self.free_game_request.readAll().data().decode()) + return + else: + return + else: + return + free_games = free_games["data"]["Catalog"]["searchStore"]["elements"] + date = datetime.datetime.now() + free_games_now = [] + coming_free_games = [] + for game in free_games: + if game["title"] == "Mystery Game": + coming_free_games.append(game) + continue + try: + # parse datetime + end_date = datetime.datetime.strptime( + game["promotions"].get("promotionalOffers", game["promotions"].get("upcomingPromotionalOffers"))[0]["promotionalOffers"][0]["endDate"], + '%Y-%m-%dT%H:%M:%S.%fZ') + start_date = datetime.datetime.strptime( + game["promotions"].get("promotionalOffers", game["promotions"].get("upcomingPromotionalOffers"))[0][ + "promotionalOffers"][0]["startDate"], + '%Y-%m-%dT%H:%M:%S.%fZ') + except IndexError: + print("index error") + continue + except KeyError: + print("keyerror") + continue + except TypeError: + print("type error") + continue + if start_date < date < end_date: + free_games_now.append(game) + elif start_date > date: + coming_free_games.append(game) + + for free_game in free_games_now: + w = GameWidget(free_game) + w.show_info.connect(self.show_info) + self.free_game_now.layout().addWidget(w) + self.free_game_widgets.append(w) + self.free_game_group_box_2.setMinimumHeight(200) + + for free_game in coming_free_games: + w = GameWidget(free_game) + if free_game["title"] != "Mystery Game": + w.show_info.connect(self.show_info) + self.comming_free_game.layout().addWidget(w) + self.free_game_widgets.append(w) + self.free_games_stack.setCurrentIndex(0) + + def search_games(self, text): + if text == "": + self.search_results.setVisible(False) + else: + locale = get_lang() + payload = QByteArray() + payload.append( + "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") + + payload = json.dumps({ + "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", + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 10, + "country": "DE", "keywords": text, "locale": locale, "sortDir": "DESC", + "allowCountries": locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + }).encode() + request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self.search_request = self.manager.post(request, payload) + # self.search_request = self.manager.post(QNetworkRequest(QUrl("https://www.epicgames.com/graphql")), payload) + self.search_request.readyRead.connect(self.show_search_results) + self.search_request.finished.connect( + self.search_request.deleteLater if self.search_request else None) + + def show_search_results(self): + if self.search_request: + if self.search_request.error() == QNetworkReply.NoError: + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) + if QJsonParseError.NoError == error.error: + data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"]["elements"] + else: + logging.error(error.errorString()) + self.search_results.setVisible(False) + return + #response = .decode(encoding="utf-8") + #print(response) + #results = json.loads(response) + else: + return + else: + return + self.data = data + titles = [i.get("title") for i in data] + model = QStringListModel() + model.setStringList(titles) + self.completer.setModel(model) + self.completer.popup() + # self.search_results.setLayout(layout) + # self.search_results.setVisible(True) + + def show_game(self): + if self.data: + slug = self.data[0].get("productSlug") + self.show_info.emit(slug) + + +class SearchResultItem(QWidget): + def __init__(self, json_info): + super(SearchResultItem, self).__init__() + self.layout = QHBoxLayout() + self.title = QLabel(json_info.get("title", "undefined")) + self.layout.addWidget(self.title) + self.slug = json_info.get("productSlug", "undefined") + + self.setLayout(self.layout) + + +class GameWidget(QWidget): + show_info = pyqtSignal(str) + + def __init__(self, json_info): + super(GameWidget, self).__init__() + self.layout = QVBoxLayout() + self.image = QLabel() + self.slug = json_info["productSlug"] + if not os.path.exists(path := os.path.expanduser(f"~/.cache/rare/cache/{json_info['title']}.png")): + for img in json_info["keyImages"]: + if json_info["title"] != "Mystery Game": + if img["type"] == "DieselStoreFrontWide": + with open(path, "wb") as img_file: + content = requests.get(img["url"]).content + img_file.write(content) + break + else: + if img["type"] == "VaultClosed": + with open(path, "wb") as img_file: + content = requests.get(img["url"]).content + img_file.write(content) + break + else: + print("No image found") + width = 300 + self.image.setPixmap(QPixmap(os.path.expanduser(f"~/.cache/rare/cache/{json_info['title']}.png")) + .scaled(width, int(width * 9 / 16), transformMode=Qt.SmoothTransformation)) + self.layout.addWidget(self.image) + + self.title = QLabel(json_info["title"]) + self.layout.addWidget(self.title) + + self.setLayout(self.layout) + + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + self.show_info.emit(self.slug) diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py new file mode 100644 index 00000000..d0acab66 --- /dev/null +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'shop_game_info.ui' +# +# Created by: PyQt5 UI code generator 5.15.4 +# +# 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) + self.gridLayout = QtWidgets.QGridLayout(shop_info) + self.gridLayout.setObjectName("gridLayout") + self.pushButton = QtWidgets.QPushButton(shop_info) + self.pushButton.setObjectName("pushButton") + self.gridLayout.addWidget(self.pushButton, 3, 1, 1, 1) + self.price = QtWidgets.QLabel(shop_info) + self.price.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.price.setObjectName("price") + self.gridLayout.addWidget(self.price, 1, 1, 1, 1) + self.title = QtWidgets.QLabel(shop_info) + self.title.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.title.setObjectName("title") + self.gridLayout.addWidget(self.title, 0, 1, 1, 1) + self.image = QtWidgets.QLabel(shop_info) + self.image.setObjectName("image") + self.gridLayout.addWidget(self.image, 0, 0, 1, 1) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem, 4, 1, 1, 1) + self.dev = QtWidgets.QLabel(shop_info) + self.dev.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.dev.setObjectName("dev") + self.gridLayout.addWidget(self.dev, 2, 1, 1, 1) + + self.retranslateUi(shop_info) + QtCore.QMetaObject.connectSlotsByName(shop_info) + + def retranslateUi(self, shop_info): + _translate = QtCore.QCoreApplication.translate + shop_info.setWindowTitle(_translate("shop_info", "Form")) + self.pushButton.setText(_translate("shop_info", "Buy Game in Epic Games Store")) + self.price.setText(_translate("shop_info", "TextLabel")) + self.title.setText(_translate("shop_info", "Error")) + self.image.setText(_translate("shop_info", "TextLabel")) + self.dev.setText(_translate("shop_info", "TextLabel")) + + +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 new file mode 100644 index 00000000..c3235210 --- /dev/null +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -0,0 +1,78 @@ + + + shop_info + + + + 0 + 0 + 702 + 468 + + + + Form + + + + + + Buy Game in Epic Games Store + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Error + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + TextLabel + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py new file mode 100644 index 00000000..ce8adcfb --- /dev/null +++ b/rare/ui/components/tabs/store/store.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'store.ui' +# +# Created by: PyQt5 UI code generator 5.15.4 +# +# 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_ShopWidget(object): + def setupUi(self, ShopWidget): + ShopWidget.setObjectName("ShopWidget") + ShopWidget.resize(697, 362) + self.verticalLayout = QtWidgets.QVBoxLayout(ShopWidget) + self.verticalLayout.setObjectName("verticalLayout") + self.search = QtWidgets.QLineEdit(ShopWidget) + self.search.setObjectName("search") + self.verticalLayout.addWidget(self.search) + self.search_results = QtWidgets.QGroupBox(ShopWidget) + self.search_results.setFlat(False) + self.search_results.setObjectName("search_results") + self.verticalLayout.addWidget(self.search_results) + self.free_game_group_box_2 = QtWidgets.QGroupBox(ShopWidget) + self.free_game_group_box_2.setObjectName("free_game_group_box_2") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.free_game_group_box_2) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.free_games_stack = QtWidgets.QStackedWidget(self.free_game_group_box_2) + self.free_games_stack.setObjectName("free_games_stack") + self.free_games_page = QtWidgets.QWidget() + self.free_games_page.setObjectName("free_games_page") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.free_games_page) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.free_game_now = QtWidgets.QGroupBox(self.free_games_page) + self.free_game_now.setObjectName("free_game_now") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.free_game_now) + self.horizontalLayout.setObjectName("horizontalLayout") + self.horizontalLayout_3.addWidget(self.free_game_now) + self.comming_free_game = QtWidgets.QGroupBox(self.free_games_page) + self.comming_free_game.setObjectName("comming_free_game") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.comming_free_game) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.horizontalLayout_3.addWidget(self.comming_free_game) + self.free_games_stack.addWidget(self.free_games_page) + self.verticalLayout_2.addWidget(self.free_games_stack) + self.verticalLayout.addWidget(self.free_game_group_box_2) + self.games_groupbox_2 = QtWidgets.QGroupBox(ShopWidget) + self.games_groupbox_2.setObjectName("games_groupbox_2") + self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.games_groupbox_2) + self.horizontalLayout_7.setObjectName("horizontalLayout_7") + self.verticalLayout.addWidget(self.games_groupbox_2) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + + self.retranslateUi(ShopWidget) + self.free_games_stack.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(ShopWidget) + + def retranslateUi(self, ShopWidget): + _translate = QtCore.QCoreApplication.translate + ShopWidget.setWindowTitle(_translate("ShopWidget", "Form")) + self.search.setPlaceholderText(_translate("ShopWidget", "Search Games")) + self.search_results.setTitle(_translate("ShopWidget", "Search results")) + self.free_game_group_box_2.setTitle(_translate("ShopWidget", "Free Games")) + self.free_game_now.setTitle(_translate("ShopWidget", "Free Game")) + self.comming_free_game.setTitle(_translate("ShopWidget", "Comming Free Game")) + self.games_groupbox_2.setTitle(_translate("ShopWidget", "Other nice games")) + + +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 new file mode 100644 index 00000000..1e470192 --- /dev/null +++ b/rare/ui/components/tabs/store/store.ui @@ -0,0 +1,95 @@ + + + ShopWidget + + + + 0 + 0 + 697 + 362 + + + + Form + + + + + + Search Games + + + + + + + Search results + + + false + + + + + + + Free Games + + + + + + 0 + + + + + + + Free Game + + + + + + + + Comming Free Game + + + + + + + + + + + + + + + Other nice games + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 121ab59d..cfc66800 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -114,13 +114,6 @@ class FlowLayout(QLayout): return parent.spacing() -class ClickableLabel(QLabel): - clicked = pyqtSignal() - - def __init__(self): - super(ClickableLabel, self).__init__() - - class PathEdit(QWidget, Ui_PathEdit): def __init__(self, text: str = "", diff --git a/rare/utils/models.py b/rare/utils/models.py index 44400ddc..790868d1 100644 --- a/rare/utils/models.py +++ b/rare/utils/models.py @@ -34,3 +34,51 @@ class InstallQueueItemModel: def __bool__(self): return (self.status_q is not None) and (self.download is not None) and (self.options is not None) + + +class ShopGame: + # TODO: Copyrights etc + def __init__(self, title: str = "", image_urls: dict = None, social_links: dict = None, + langs: list = None, reqs: list = None, publisher: str = "", developer: 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 # {"Betriebssystem":win7, processor:i9 9900k, ram...}; Note: name from language + self.publisher = publisher + self.developer = developer + + @classmethod + def from_json(cls, data: dict): + if isinstance(data, list): + for product in data: + if product["_title"] == "home": + data = product + break + tmp = cls() + tmp.title = data.get("productName", "undefined") + tmp.img_urls = { + "DieselImage": data["data"]["about"]["image"]["src"], + "banner": data["data"]["hero"]["backgroundImageUrl"] + } + links = 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 = data["data"]["requirements"]["languages"] + tmp.reqs = {} + for system in data["data"]["requirements"]["systems"]: + tmp.reqs[system] = {} + for i in system: + tmp.reqs[i[system]["title"]] = tuple((i[system]["minimum"], i[system]["recommend"])) + + tmp.publisher = data["data"]["meta"].get("publisher", "undefined") + tmp.developer = data["data"]["meta"].get("developer", "undefined") + return tmp diff --git a/test.py b/test.py new file mode 100644 index 00000000..de073353 --- /dev/null +++ b/test.py @@ -0,0 +1,20 @@ +import sys +from PyQt5.QtCore import Qt, QStringListModel +from PyQt5.QtWidgets import QApplication, QCompleter, QLineEdit + +def get_data(model): + model.setStringList(["completion", "data", "goes", "here"]) + +if __name__ == "__main__": + + app = QApplication(sys.argv) + edit = QLineEdit() + completer = QCompleter() + edit.setCompleter(completer) + + model = QStringListModel() + completer.setModel(model) + get_data(model) + + edit.show() + sys.exit(app.exec_()) \ No newline at end of file From 78474d18c3574bffb8fa9aa46468704420f1986f Mon Sep 17 00:00:00 2001 From: Dummerle Date: Thu, 3 Jun 2021 23:35:19 +0200 Subject: [PATCH 02/30] Add __init__ to store ui --- rare/ui/components/tabs/store/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 rare/ui/components/tabs/store/__init__.py diff --git a/rare/ui/components/tabs/store/__init__.py b/rare/ui/components/tabs/store/__init__.py new file mode 100644 index 00000000..e69de29b From 654788410abbfc2949a139391abe066e7d756693 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Fri, 4 Jun 2021 00:01:54 +0200 Subject: [PATCH 03/30] Shop info does not work, but the app does not crash --- rare/components/tabs/shop/shop_info.py | 41 +++++++++++++++++------- rare/components/tabs/shop/shop_widget.py | 2 +- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index a331a643..cb1680dd 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -1,18 +1,21 @@ +import json +import logging import os import webbrowser +from json import JSONDecodeError -from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtCore import QLocale, Qt, QUrl from PyQt5.QtGui import QPixmap -from PyQt5.QtNetwork import QNetworkAccessManager +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info -from rare.utils import api_utils class ShopGameInfo(QWidget, Ui_shop_info): slug = "" + # TODO GANZ VIEL def __init__(self): super(ShopGameInfo, self).__init__() self.setupUi(self) @@ -20,16 +23,32 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.manager = QNetworkAccessManager() def update_game(self, slug: str): - locale = QLocale.system().name().split("_")[0] - game = api_utils.get_product(slug, locale) self.slug = slug - self.title.setText(game[0]["productName"]) - self.image.setPixmap( - QPixmap(os.path.expanduser(f"~/.cache/rare/cache/{game[0]['productName']}.png")).scaled(180, - int(180 * 9 / 16), - transformMode=Qt.SmoothTransformation)) + locale = QLocale.system().name().split("_")[0] + url = f"https://store-content.ak.epicgames.com/api/{locale}/content/products/{slug}" + # game = api_utils.get_product(slug, locale) + self.request = self.manager.get(QNetworkRequest(QUrl(url))) + self.request.readyRead.connect(self.data_received) + self.request.finished.connect(self.request.deleteLater if self.request else None) - self.dev.setText(game[0]["data"]["meta"]["developer"][0]) + def data_received(self): + logging.info(f"Data of game {self.slug} received") + if self.request: + if self.request.error() == QNetworkReply.NoError: + try: + game = json.loads(self.request.readAll().data().decode())[0] + except JSONDecodeError: + return + else: + return + else: + return + self.title.setText(game["productName"]) + self.image.setPixmap( + QPixmap(os.path.expanduser(f"~/.cache/rare/cache/{game['productName']}.png")).scaled(180, + int(180 * 9 / 16), + transformMode=Qt.SmoothTransformation)) + self.dev.setText(game["data"]["meta"]["developer"][0]) def button_clicked(self): webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index efc0ce18..06ad233b 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -6,7 +6,7 @@ from json import JSONDecodeError import requests from PyQt5 import QtGui -from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QByteArray, QJsonDocument, QJsonParseError, QObjectCleanupHandler, \ +from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QByteArray, QJsonDocument, QJsonParseError, \ QStringListModel from PyQt5.QtGui import QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply From bcce9487cd22da008373fdefb6865e34e43000b4 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Fri, 4 Jun 2021 18:16:27 +0200 Subject: [PATCH 04/30] Shop info works now --- rare/components/tabs/shop/__init__.py | 5 +- rare/components/tabs/shop/shop_info.py | 45 ++++--- .../components/tabs/store/shop_game_info.py | 50 +++++--- .../components/tabs/store/shop_game_info.ui | 112 +++++++++++------- rare/utils/models.py | 22 ++-- 5 files changed, 146 insertions(+), 88 deletions(-) diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index 1b883bd7..b7d85f0d 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QStackedWidget, QWidget, QVBoxLayout, QLabel +from PyQt5.QtWidgets import QStackedWidget from rare.components.tabs.shop.shop_info import ShopGameInfo from rare.components.tabs.shop.shop_widget import ShopWidget @@ -6,6 +6,7 @@ from rare.components.tabs.shop.shop_widget import ShopWidget class Shop(QStackedWidget): init = False + def __init__(self): super(Shop, self).__init__() @@ -14,6 +15,7 @@ class Shop(QStackedWidget): self.info = ShopGameInfo() self.addWidget(self.info) + self.info.back_button.clicked.connect(lambda: self.setCurrentIndex(0)) self.shop.show_info.connect(self.show_info) @@ -22,7 +24,6 @@ class Shop(QStackedWidget): self.init = True self.shop.load() - def show_info(self, slug): self.info.update_game(slug) self.setCurrentIndex(1) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index cb1680dd..8bdc92bd 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -2,12 +2,13 @@ import json import logging import os import webbrowser -from json import JSONDecodeError -from PyQt5.QtCore import QLocale, Qt, QUrl +import requests +from PyQt5.QtCore import QLocale, QUrl, QJsonDocument, QJsonParseError, Qt from PyQt5.QtGui import QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget +from rare.utils import models from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info @@ -19,36 +20,52 @@ class ShopGameInfo(QWidget, Ui_shop_info): def __init__(self): super(ShopGameInfo, self).__init__() self.setupUi(self) - self.pushButton.clicked.connect(self.button_clicked) + self.open_store_button.clicked.connect(self.button_clicked) self.manager = QNetworkAccessManager() def update_game(self, slug: str): + self.title.setText(self.tr("Loading")) + self.price.setText(self.tr("Loading")) + self.dev.setText(self.tr("Loading")) + self.image.setPixmap(QPixmap()) self.slug = slug locale = QLocale.system().name().split("_")[0] url = f"https://store-content.ak.epicgames.com/api/{locale}/content/products/{slug}" # game = api_utils.get_product(slug, locale) self.request = self.manager.get(QNetworkRequest(QUrl(url))) - self.request.readyRead.connect(self.data_received) - self.request.finished.connect(self.request.deleteLater if self.request else None) + self.request.finished.connect(self.data_received) + # self.request.finished.connect(self.request.deleteLater if self.request else None) def data_received(self): logging.info(f"Data of game {self.slug} received") if self.request: if self.request.error() == QNetworkReply.NoError: - try: - game = json.loads(self.request.readAll().data().decode())[0] - except JSONDecodeError: + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.request.readAll().data(), error) + + if error.error == error.NoError: + game = json.loads(json_data.toJson().data().decode()) + else: + logging.info(self.slug, error.errorString()) return else: return else: return - self.title.setText(game["productName"]) - self.image.setPixmap( - QPixmap(os.path.expanduser(f"~/.cache/rare/cache/{game['productName']}.png")).scaled(180, - int(180 * 9 / 16), - transformMode=Qt.SmoothTransformation)) - self.dev.setText(game["data"]["meta"]["developer"][0]) + self.game = models.ShopGame.from_json(game) + # print(game) + self.title.setText(self.game.title) + + if not os.path.exists(path := os.path.expanduser(f"~/.cache/rare/cache/{self.game.title}.png")): + url = game["pages"][0]["_images_"][0] + open(os.path.expanduser(path), "wb").write(requests.get(url).content) + width = 360 + self.image.setPixmap(QPixmap(path).scaled(width, int(width * 9 / 16), transformMode=Qt.SmoothTransformation)) + try: + self.dev.setText(",".join(self.game.developer)) + except KeyError: + pass + self.request.deleteLater() def button_clicked(self): webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index d0acab66..5ae1d466 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -15,28 +15,39 @@ class Ui_shop_info(object): def setupUi(self, shop_info): shop_info.setObjectName("shop_info") shop_info.resize(702, 468) - self.gridLayout = QtWidgets.QGridLayout(shop_info) - self.gridLayout.setObjectName("gridLayout") - self.pushButton = QtWidgets.QPushButton(shop_info) - self.pushButton.setObjectName("pushButton") - self.gridLayout.addWidget(self.pushButton, 3, 1, 1, 1) - self.price = QtWidgets.QLabel(shop_info) - self.price.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) - self.price.setObjectName("price") - self.gridLayout.addWidget(self.price, 1, 1, 1, 1) + 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 = QtWidgets.QLabel(shop_info) + self.image.setObjectName("image") + self.horizontalLayout.addWidget(self.image) + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") self.title = QtWidgets.QLabel(shop_info) self.title.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) self.title.setObjectName("title") - self.gridLayout.addWidget(self.title, 0, 1, 1, 1) - self.image = QtWidgets.QLabel(shop_info) - self.image.setObjectName("image") - self.gridLayout.addWidget(self.image, 0, 0, 1, 1) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout.addItem(spacerItem, 4, 1, 1, 1) + self.verticalLayout_2.addWidget(self.title) self.dev = QtWidgets.QLabel(shop_info) self.dev.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) self.dev.setObjectName("dev") - self.gridLayout.addWidget(self.dev, 2, 1, 1, 1) + self.verticalLayout_2.addWidget(self.dev) + self.price = QtWidgets.QLabel(shop_info) + self.price.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.price.setObjectName("price") + self.verticalLayout_2.addWidget(self.price) + self.horizontalLayout.addLayout(self.verticalLayout_2) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem) + self.verticalLayout.addLayout(self.horizontalLayout) + self.open_store_button = QtWidgets.QPushButton(shop_info) + self.open_store_button.setObjectName("open_store_button") + self.verticalLayout.addWidget(self.open_store_button) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem1) self.retranslateUi(shop_info) QtCore.QMetaObject.connectSlotsByName(shop_info) @@ -44,11 +55,12 @@ class Ui_shop_info(object): def retranslateUi(self, shop_info): _translate = QtCore.QCoreApplication.translate shop_info.setWindowTitle(_translate("shop_info", "Form")) - self.pushButton.setText(_translate("shop_info", "Buy Game in Epic Games Store")) - self.price.setText(_translate("shop_info", "TextLabel")) - self.title.setText(_translate("shop_info", "Error")) + self.back_button.setText(_translate("shop_info", "Back")) self.image.setText(_translate("shop_info", "TextLabel")) + self.title.setText(_translate("shop_info", "Error")) self.dev.setText(_translate("shop_info", "TextLabel")) + self.price.setText(_translate("shop_info", "TextLabel")) + self.open_store_button.setText(_translate("shop_info", "Buy Game in Epic Games Store")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index c3235210..960a1f87 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -13,43 +13,81 @@ Form - - - + + + + + Back + + + + + + + + + TextLabel + + + + + + + + + Error + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Buy Game in Epic Games Store - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Error - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - TextLabel - - - - - + + Qt::Vertical @@ -61,16 +99,6 @@ - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - diff --git a/rare/utils/models.py b/rare/utils/models.py index 790868d1..cd2f6685 100644 --- a/rare/utils/models.py +++ b/rare/utils/models.py @@ -64,21 +64,21 @@ class ShopGame: tmp = cls() tmp.title = data.get("productName", "undefined") tmp.img_urls = { - "DieselImage": data["data"]["about"]["image"]["src"], - "banner": data["data"]["hero"]["backgroundImageUrl"] + "DieselImage": data["pages"][0]["data"]["about"]["image"]["src"], + "banner": data["pages"][0]["data"]["hero"]["backgroundImageUrl"] } - links = data["data"]["socialLinks"] + links = data["pages"][0]["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 = data["data"]["requirements"]["languages"] - tmp.reqs = {} - for system in data["data"]["requirements"]["systems"]: - tmp.reqs[system] = {} + tmp.available_voice_langs = data["pages"][0]["data"]["requirements"]["languages"] + tmp.reqs = [] + """for system in data["pages"][0]["data"]["requirements"]["systems"]: + tmp.reqs.append({"name": system, "value": []}) for i in system: - tmp.reqs[i[system]["title"]] = tuple((i[system]["minimum"], i[system]["recommend"])) - - tmp.publisher = data["data"]["meta"].get("publisher", "undefined") - tmp.developer = data["data"]["meta"].get("developer", "undefined") + tmp.reqs[system].append(tuple((i[system]["minimum"], i[system]["recommend"]))) + """ + tmp.publisher = data["pages"][0]["data"]["meta"].get("publisher", "undefined") + tmp.developer = data["pages"][0]["data"]["meta"].get("developer", "undefined") return tmp From 89afebd9fd7160f7ebfb7a291c9187e0d426525c Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 9 Jun 2021 00:00:00 +0200 Subject: [PATCH 05/30] Show search results: Not pretty: no images --- rare/components/tabs/shop/__init__.py | 15 +++- rare/components/tabs/shop/search_results.py | 49 ++++++++++++ rare/components/tabs/shop/shop_info.py | 33 +++++--- rare/components/tabs/shop/shop_widget.py | 84 ++++++++++----------- rare/utils/models.py | 45 ++++++----- 5 files changed, 153 insertions(+), 73 deletions(-) create mode 100644 rare/components/tabs/shop/search_results.py diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index b7d85f0d..d363e6b6 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -1,5 +1,6 @@ from PyQt5.QtWidgets import QStackedWidget +from rare.components.tabs.shop.search_results import SearchResults from rare.components.tabs.shop.shop_info import ShopGameInfo from rare.components.tabs.shop.shop_widget import ShopWidget @@ -13,17 +14,27 @@ class Shop(QStackedWidget): self.shop = ShopWidget() self.addWidget(self.shop) + self.search_results = SearchResults() + self.addWidget(self.search_results) + self.search_results.show_info.connect(self.show_game_info) + self.info = ShopGameInfo() self.addWidget(self.info) self.info.back_button.clicked.connect(lambda: self.setCurrentIndex(0)) self.shop.show_info.connect(self.show_info) + self.shop.show_game.connect(self.show_game_info) def load(self): if not self.init: self.init = True self.shop.load() - def show_info(self, slug): - self.info.update_game(slug) + def show_game_info(self, data): + self.info.update_game(data) + self.setCurrentIndex(2) + + def show_info(self, data): + self.search_results.show_results(data) self.setCurrentIndex(1) + diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py new file mode 100644 index 00000000..2cedd48d --- /dev/null +++ b/rare/components/tabs/shop/search_results.py @@ -0,0 +1,49 @@ +from PyQt5 import QtGui +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea + + +class SearchResults(QScrollArea): + show_info = pyqtSignal(dict) + + def __init__(self): + super(SearchResults, self).__init__() + self.widget = QWidget() + self.layout = QVBoxLayout() + self.widget.setLayout(self.layout) + self.setWidget(self.widget) + + def show_results(self, results: list): + QVBoxLayout().addWidget(self.widget) + self.widget = QWidget() + self.layout = QVBoxLayout() + for i in range(self.layout.count()): + self.layout.removeItem(i) + for res in results: + w = _SearchResultItem(res) + w.show_info.connect(self.show_info.emit) + self.layout.addWidget(w) + self.layout.addStretch(1) + self.widget.setLayout(self.layout) + self.setWidget(self.widget) + + +class _SearchResultItem(QWidget): + res: dict + show_info = pyqtSignal(dict) + + def __init__(self, result: dict): + super(_SearchResultItem, self).__init__() + self.layout = QHBoxLayout() + self.res = result + self.title = QLabel(self.res["title"]) + self.layout.addWidget(self.title) + original_price = result['price']['totalPrice']['fmtPrice']['originalPrice'] + self.price = QLabel(f"{self.tr('Original price: ')}{original_price}") + self.layout.addWidget(self.price) + + self.setLayout(self.layout) + + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + if a0.button() == 1: + self.show_info.emit(self.res) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index 8bdc92bd..fbe970bc 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -1,20 +1,19 @@ import json import logging -import os import webbrowser -import requests -from PyQt5.QtCore import QLocale, QUrl, QJsonDocument, QJsonParseError, Qt +from PyQt5.QtCore import QLocale, QUrl, QJsonDocument, QJsonParseError from PyQt5.QtGui import QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget -from rare.utils import models from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info +from rare.utils import models class ShopGameInfo(QWidget, Ui_shop_info): - slug = "" + game: models.ShopGame + data: dict # TODO GANZ VIEL def __init__(self): @@ -23,12 +22,18 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.open_store_button.clicked.connect(self.button_clicked) self.manager = QNetworkAccessManager() - def update_game(self, slug: str): - self.title.setText(self.tr("Loading")) - self.price.setText(self.tr("Loading")) + def update_game(self, data: dict): + slug = data["productSlug"] + if "/home" in slug: + slug = slug.replace("/home", "") + self.slug = slug + self.title.setText(data["title"]) + self.price.setText(data['price']['totalPrice']['fmtPrice']['originalPrice']) self.dev.setText(self.tr("Loading")) self.image.setPixmap(QPixmap()) - self.slug = slug + self.data = data + + # init API request locale = QLocale.system().name().split("_")[0] url = f"https://store-content.ak.epicgames.com/api/{locale}/content/products/{slug}" # game = api_utils.get_product(slug, locale) @@ -37,7 +42,7 @@ class ShopGameInfo(QWidget, Ui_shop_info): # self.request.finished.connect(self.request.deleteLater if self.request else None) def data_received(self): - logging.info(f"Data of game {self.slug} received") + logging.info(f"Data of game {self.data['title']} received") if self.request: if self.request.error() == QNetworkReply.NoError: error = QJsonParseError() @@ -52,19 +57,23 @@ class ShopGameInfo(QWidget, Ui_shop_info): return else: return - self.game = models.ShopGame.from_json(game) + self.game = models.ShopGame.from_json(game, self.data) # print(game) self.title.setText(self.game.title) - + """ if not os.path.exists(path := os.path.expanduser(f"~/.cache/rare/cache/{self.game.title}.png")): url = game["pages"][0]["_images_"][0] open(os.path.expanduser(path), "wb").write(requests.get(url).content) width = 360 self.image.setPixmap(QPixmap(path).scaled(width, int(width * 9 / 16), transformMode=Qt.SmoothTransformation)) + """ try: self.dev.setText(",".join(self.game.developer)) except KeyError: pass + + # self.price.setText(self.game.price) + self.request.deleteLater() def button_clicked(self): diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 06ad233b..3be1a464 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -18,7 +18,8 @@ from rare.utils.utils import get_lang class ShopWidget(QWidget, Ui_ShopWidget): - show_info = pyqtSignal(str) + show_info = pyqtSignal(list) + show_game = pyqtSignal(dict) free_game_widgets = [] def __init__(self): @@ -32,12 +33,16 @@ class ShopWidget(QWidget, Ui_ShopWidget): self.completer = QCompleter() self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.search.setCompleter(self.completer) - self.search.returnPressed.connect(self.show_game) + self.search.returnPressed.connect(self.show_search_result) self.data = [] def load(self): - if not os.path.exists(p := os.path.expanduser(f"~/.cache/rare/cache/")): - os.makedirs(p) + if p := os.getenv("XDG_CACHE_HOME"): + self.path = p + else: + self.path = os.path.expanduser("~/.cache/rare/cache/") + if not os.path.exists(self.path): + os.makedirs(self.path) url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" self.free_game_request = self.manager.get(QNetworkRequest(QUrl(url))) self.free_game_request.readyRead.connect(self.add_free_games) @@ -50,8 +55,8 @@ class ShopWidget(QWidget, Ui_ShopWidget): if self.free_game_request.error() == QNetworkReply.NoError: try: free_games = json.loads(self.free_game_request.readAll().data().decode()) + print(free_games) except JSONDecodeError: - print(self.free_game_request.readAll().data().decode()) return else: return @@ -68,7 +73,8 @@ class ShopWidget(QWidget, Ui_ShopWidget): try: # parse datetime end_date = datetime.datetime.strptime( - game["promotions"].get("promotionalOffers", game["promotions"].get("upcomingPromotionalOffers"))[0]["promotionalOffers"][0]["endDate"], + game["promotions"].get("promotionalOffers", game["promotions"].get("upcomingPromotionalOffers"))[0][ + "promotionalOffers"][0]["endDate"], '%Y-%m-%dT%H:%M:%S.%fZ') start_date = datetime.datetime.strptime( game["promotions"].get("promotionalOffers", game["promotions"].get("upcomingPromotionalOffers"))[0][ @@ -89,21 +95,21 @@ class ShopWidget(QWidget, Ui_ShopWidget): coming_free_games.append(game) for free_game in free_games_now: - w = GameWidget(free_game) - w.show_info.connect(self.show_info) + w = GameWidget(free_game, self.path) + w.show_info.connect(lambda x: self.search_games(x, True)) self.free_game_now.layout().addWidget(w) self.free_game_widgets.append(w) self.free_game_group_box_2.setMinimumHeight(200) for free_game in coming_free_games: - w = GameWidget(free_game) + w = GameWidget(free_game, self.path) if free_game["title"] != "Mystery Game": w.show_info.connect(self.show_info) self.comming_free_game.layout().addWidget(w) self.free_game_widgets.append(w) self.free_games_stack.setCurrentIndex(0) - def search_games(self, text): + def search_games(self, text, show_direct=False): if text == "": self.search_results.setVisible(False) else: @@ -123,11 +129,9 @@ class ShopWidget(QWidget, Ui_ShopWidget): request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") self.search_request = self.manager.post(request, payload) # self.search_request = self.manager.post(QNetworkRequest(QUrl("https://www.epicgames.com/graphql")), payload) - self.search_request.readyRead.connect(self.show_search_results) - self.search_request.finished.connect( - self.search_request.deleteLater if self.search_request else None) + self.search_request.finished.connect(lambda: self.show_search_results(show_direct)) - def show_search_results(self): + def show_search_results(self, show_direct=False): if self.search_request: if self.search_request.error() == QNetworkReply.NoError: error = QJsonParseError() @@ -138,72 +142,68 @@ class ShopWidget(QWidget, Ui_ShopWidget): logging.error(error.errorString()) self.search_results.setVisible(False) return - #response = .decode(encoding="utf-8") - #print(response) - #results = json.loads(response) + # response = .decode(encoding="utf-8") + # print(response) + # results = json.loads(response) else: return else: return self.data = data + if show_direct: + self.show_search_result(True) + return titles = [i.get("title") for i in data] model = QStringListModel() model.setStringList(titles) self.completer.setModel(model) - self.completer.popup() + # self.completer.popup() # self.search_results.setLayout(layout) # self.search_results.setVisible(True) + if self.search_request: + self.search_request.deleteLater() - def show_game(self): - if self.data: - slug = self.data[0].get("productSlug") - self.show_info.emit(slug) - - -class SearchResultItem(QWidget): - def __init__(self, json_info): - super(SearchResultItem, self).__init__() - self.layout = QHBoxLayout() - self.title = QLabel(json_info.get("title", "undefined")) - self.layout.addWidget(self.title) - self.slug = json_info.get("productSlug", "undefined") - - self.setLayout(self.layout) + def show_search_result(self, show_direct=False): + if not show_direct: + if self.data: + self.show_info.emit(self.data) + else: + self.show_game.emit(self.data[0]) class GameWidget(QWidget): show_info = pyqtSignal(str) - def __init__(self, json_info): + def __init__(self, json_info, path: str): super(GameWidget, self).__init__() self.layout = QVBoxLayout() self.image = QLabel() self.slug = json_info["productSlug"] - if not os.path.exists(path := os.path.expanduser(f"~/.cache/rare/cache/{json_info['title']}.png")): + self.title = json_info["title"] + if not os.path.exists(p := os.path.join(path, f"{json_info['title']}.png")): for img in json_info["keyImages"]: if json_info["title"] != "Mystery Game": if img["type"] == "DieselStoreFrontWide": - with open(path, "wb") as img_file: + with open(p, "wb") as img_file: content = requests.get(img["url"]).content img_file.write(content) break else: if img["type"] == "VaultClosed": - with open(path, "wb") as img_file: + with open(p, "wb") as img_file: content = requests.get(img["url"]).content img_file.write(content) break else: print("No image found") width = 300 - self.image.setPixmap(QPixmap(os.path.expanduser(f"~/.cache/rare/cache/{json_info['title']}.png")) + self.image.setPixmap(QPixmap(os.path.join(path, f"{json_info['title']}.png")) .scaled(width, int(width * 9 / 16), transformMode=Qt.SmoothTransformation)) self.layout.addWidget(self.image) - self.title = QLabel(json_info["title"]) - self.layout.addWidget(self.title) - + self.title_label = QLabel(json_info["title"]) + self.layout.addWidget(self.title_label) self.setLayout(self.layout) def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - self.show_info.emit(self.slug) + self.show_info.emit(self.title) diff --git a/rare/utils/models.py b/rare/utils/models.py index cd2f6685..416c90d5 100644 --- a/rare/utils/models.py +++ b/rare/utils/models.py @@ -39,7 +39,8 @@ class InstallQueueItemModel: class ShopGame: # TODO: Copyrights etc def __init__(self, title: str = "", image_urls: dict = None, social_links: dict = None, - langs: list = None, reqs: list = None, publisher: str = "", developer: str = ""): + langs: list = None, reqs: list = None, publisher: str = "", developer: str = "", + price: str = ""): self.title = title self.image_urls = image_urls self.links = [] @@ -53,32 +54,42 @@ class ShopGame: self.reqs = reqs # {"Betriebssystem":win7, processor:i9 9900k, ram...}; Note: name from language self.publisher = publisher self.developer = developer + self.price = price @classmethod - def from_json(cls, data: dict): - if isinstance(data, list): - for product in data: + def from_json(cls, api_data: dict, search_data: dict): + print(api_data) + if isinstance(api_data, list): + for product in api_data: if product["_title"] == "home": - data = product + api_data = product + print("home") break + tmp = cls() - tmp.title = data.get("productName", "undefined") - tmp.img_urls = { + tmp.title = api_data.get("productName", "undefined") + """tmp.img_urls = { "DieselImage": data["pages"][0]["data"]["about"]["image"]["src"], "banner": data["pages"][0]["data"]["hero"]["backgroundImageUrl"] - } - links = data["pages"][0]["data"]["socialLinks"] + }""" + links = api_data["pages"][0]["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 = data["pages"][0]["data"]["requirements"]["languages"] + tmp.available_voice_langs = api_data["pages"][0]["data"]["requirements"]["languages"] tmp.reqs = [] - """for system in data["pages"][0]["data"]["requirements"]["systems"]: - tmp.reqs.append({"name": system, "value": []}) - for i in system: - tmp.reqs[system].append(tuple((i[system]["minimum"], i[system]["recommend"]))) - """ - tmp.publisher = data["pages"][0]["data"]["meta"].get("publisher", "undefined") - tmp.developer = data["pages"][0]["data"]["meta"].get("developer", "undefined") + for i, system in enumerate(api_data["pages"][0]["data"]["requirements"]["systems"]): + tmp.reqs.append({"name": system["systemType"], "value": []}) + for req in system["details"]: + tmp.reqs[i]["value"].append(tuple((req["minimum"], req["recommended"]))) + + tmp.publisher = api_data["pages"][0]["data"]["meta"].get("publisher", "undefined") + tmp.developer = api_data["pages"][0]["data"]["meta"].get("developer", "undefined") + tmp.price = { + "normal": search_data["price"]["totalPrice"]["originalPrice"] + } + if price := search_data["price"]["totalPrice"].get("discountPrice"): + tmp.price["discount"] = price + return tmp From 44b7a0a37f3721cf290df910770504342be9a59d Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 9 Jun 2021 13:08:25 +0200 Subject: [PATCH 06/30] Show images on shop game widget; move shop models to shop_models.py --- rare/components/tabs/shop/shop_info.py | 38 ++++++---- rare/components/tabs/shop/shop_models.py | 75 +++++++++++++++++++ .../components/tabs/store/shop_game_info.py | 4 + .../components/tabs/store/shop_game_info.ui | 7 ++ rare/utils/models.py | 59 --------------- 5 files changed, 110 insertions(+), 73 deletions(-) create mode 100644 rare/components/tabs/shop/shop_models.py diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index fbe970bc..ff73cbc5 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -2,20 +2,22 @@ import json import logging import webbrowser -from PyQt5.QtCore import QLocale, QUrl, QJsonDocument, QJsonParseError +from PyQt5.QtCore import QLocale, QUrl, QJsonDocument, QJsonParseError, Qt from PyQt5.QtGui import QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget +from rare.components.tabs.shop.shop_models import ShopGame from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info -from rare.utils import models + +logger = logging.getLogger("ShopInfo") class ShopGameInfo(QWidget, Ui_shop_info): - game: models.ShopGame + game: ShopGame data: dict - # TODO GANZ VIEL + # TODO Design; More information(requirements, more images); bundles (eg EA triple) def __init__(self): super(ShopGameInfo, self).__init__() self.setupUi(self) @@ -28,7 +30,7 @@ class ShopGameInfo(QWidget, Ui_shop_info): slug = slug.replace("/home", "") self.slug = slug self.title.setText(data["title"]) - self.price.setText(data['price']['totalPrice']['fmtPrice']['originalPrice']) + self.dev.setText(self.tr("Loading")) self.image.setPixmap(QPixmap()) self.data = data @@ -39,7 +41,6 @@ class ShopGameInfo(QWidget, Ui_shop_info): # game = api_utils.get_product(slug, locale) self.request = self.manager.get(QNetworkRequest(QUrl(url))) self.request.finished.connect(self.data_received) - # self.request.finished.connect(self.request.deleteLater if self.request else None) def data_received(self): logging.info(f"Data of game {self.data['title']} received") @@ -57,16 +58,16 @@ class ShopGameInfo(QWidget, Ui_shop_info): return else: return - self.game = models.ShopGame.from_json(game, self.data) + self.game = ShopGame.from_json(game, self.data) # print(game) self.title.setText(self.game.title) - """ - if not os.path.exists(path := os.path.expanduser(f"~/.cache/rare/cache/{self.game.title}.png")): - url = game["pages"][0]["_images_"][0] - open(os.path.expanduser(path), "wb").write(requests.get(url).content) - width = 360 - self.image.setPixmap(QPixmap(path).scaled(width, int(width * 9 / 16), transformMode=Qt.SmoothTransformation)) - """ + + self.price.setText(self.game.price) + self.discount_price.setText(self.game.discount_price) + + self.image_request = self.manager.get(QNetworkRequest(QUrl(self.game.image_urls.offer_image_tall))) + self.image_request.finished.connect(self.image_loaded) + try: self.dev.setText(",".join(self.game.developer)) except KeyError: @@ -76,5 +77,14 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.request.deleteLater() + def image_loaded(self): + if self.image_request and self.image_request.error() == QNetworkReply.NoError: + data = self.image_request.readAll().data() + pixmap = QPixmap() + pixmap.loadFromData(data) + self.image.setPixmap(pixmap.scaled(240, 320, transformMode=Qt.SmoothTransformation)) + else: + logger.error("Load image failed") + def button_clicked(self): webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py new file mode 100644 index 00000000..37a25b98 --- /dev/null +++ b/rare/components/tabs/shop/shop_models.py @@ -0,0 +1,75 @@ + + +class _ImageUrlModel: + def __init__(self, front_tall: str = "", offer_image_tall: str = "", + thumbnail: str = "", front_wide: str = ""): + self.front_tall = front_tall + self.offer_image_tall = offer_image_tall + self.thumbnail = thumbnail + self.front_wide = front_wide + + @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"] + return tmp + + +class ShopGame: + # TODO: Copyrights etc + def __init__(self, title: str = "", image_urls: _ImageUrlModel = None, social_links: dict = None, + langs: list = None, reqs: list = None, publisher: str = "", developer: str = "", + original_price: str = "", discount_price: 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 # {"Betriebssystem":win7, processor:i9 9900k, ram...}; Note: name from language + self.publisher = publisher + self.developer = developer + self.price = original_price + self.discount_price = discount_price + + @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 + + tmp = cls() + tmp.title = api_data.get("productName", "undefined") + tmp.image_urls = _ImageUrlModel.from_json(search_data["keyImages"]) + links = api_data["pages"][0]["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["pages"][0]["data"]["requirements"]["languages"] + tmp.reqs = [] + for i, system in enumerate(api_data["pages"][0]["data"]["requirements"]["systems"]): + tmp.reqs.append({"name": system["systemType"], "value": []}) + for req in system["details"]: + tmp.reqs[i]["value"].append(tuple((req["minimum"], req["recommended"]))) + + tmp.publisher = api_data["pages"][0]["data"]["meta"].get("publisher", "undefined") + tmp.developer = api_data["pages"][0]["data"]["meta"].get("developer", "undefined") + tmp.price = search_data['price']['totalPrice']['fmtPrice']['originalPrice'] + tmp.discount_price = search_data['price']['totalPrice']['fmtPrice']['discountPrice'] + + return tmp diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index 5ae1d466..5fa9bcba 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -39,6 +39,9 @@ class Ui_shop_info(object): 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.setObjectName("discount_price") + self.verticalLayout_2.addWidget(self.discount_price) self.horizontalLayout.addLayout(self.verticalLayout_2) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) @@ -60,6 +63,7 @@ class Ui_shop_info(object): self.title.setText(_translate("shop_info", "Error")) self.dev.setText(_translate("shop_info", "TextLabel")) self.price.setText(_translate("shop_info", "TextLabel")) + self.discount_price.setText(_translate("shop_info", "TextLabel")) self.open_store_button.setText(_translate("shop_info", "Buy Game in Epic Games Store")) diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index 960a1f87..5ca98cc4 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -62,6 +62,13 @@ + + + + TextLabel + + + diff --git a/rare/utils/models.py b/rare/utils/models.py index 416c90d5..44400ddc 100644 --- a/rare/utils/models.py +++ b/rare/utils/models.py @@ -34,62 +34,3 @@ class InstallQueueItemModel: def __bool__(self): return (self.status_q is not None) and (self.download is not None) and (self.options is not None) - - -class ShopGame: - # TODO: Copyrights etc - def __init__(self, title: str = "", image_urls: dict = None, social_links: dict = None, - langs: list = None, reqs: list = None, publisher: str = "", developer: str = "", - price: 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 # {"Betriebssystem":win7, processor:i9 9900k, ram...}; Note: name from language - self.publisher = publisher - self.developer = developer - self.price = price - - @classmethod - def from_json(cls, api_data: dict, search_data: dict): - print(api_data) - if isinstance(api_data, list): - for product in api_data: - if product["_title"] == "home": - api_data = product - print("home") - break - - tmp = cls() - tmp.title = api_data.get("productName", "undefined") - """tmp.img_urls = { - "DieselImage": data["pages"][0]["data"]["about"]["image"]["src"], - "banner": data["pages"][0]["data"]["hero"]["backgroundImageUrl"] - }""" - links = api_data["pages"][0]["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["pages"][0]["data"]["requirements"]["languages"] - tmp.reqs = [] - for i, system in enumerate(api_data["pages"][0]["data"]["requirements"]["systems"]): - tmp.reqs.append({"name": system["systemType"], "value": []}) - for req in system["details"]: - tmp.reqs[i]["value"].append(tuple((req["minimum"], req["recommended"]))) - - tmp.publisher = api_data["pages"][0]["data"]["meta"].get("publisher", "undefined") - tmp.developer = api_data["pages"][0]["data"]["meta"].get("developer", "undefined") - tmp.price = { - "normal": search_data["price"]["totalPrice"]["originalPrice"] - } - if price := search_data["price"]["totalPrice"].get("discountPrice"): - tmp.price["discount"] = price - - return tmp From 6355dd47c12d5f1b2a992e62f8b1e0f1dc2348bf Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 9 Jun 2021 13:25:57 +0200 Subject: [PATCH 07/30] Add support for bundles --- rare/components/tabs/shop/shop_info.py | 8 +++++++- rare/components/tabs/shop/shop_models.py | 15 +++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index ff73cbc5..e76f934f 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -34,10 +34,14 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.dev.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 locale = QLocale.system().name().split("_")[0] - url = f"https://store-content.ak.epicgames.com/api/{locale}/content/products/{slug}" + url = f"https://store-content.ak.epicgames.com/api/{locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" # game = api_utils.get_product(slug, locale) self.request = self.manager.get(QNetworkRequest(QUrl(url))) self.request.finished.connect(self.data_received) @@ -55,6 +59,7 @@ class ShopGameInfo(QWidget, Ui_shop_info): logging.info(self.slug, error.errorString()) return else: + logger.error("Data failed") return else: return @@ -88,3 +93,4 @@ class ShopGameInfo(QWidget, Ui_shop_info): def button_clicked(self): webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) + diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index 37a25b98..b9b9b594 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -46,6 +46,7 @@ class ShopGame: @classmethod def from_json(cls, api_data: dict, search_data: dict): + print(api_data) if isinstance(api_data, list): for product in api_data: if product["_title"] == "home": @@ -53,22 +54,24 @@ class ShopGame: break tmp = cls() - tmp.title = api_data.get("productName", "undefined") + if "pages" in api_data.keys(): + api_data = api_data["pages"][0] + tmp.title = api_data.get("productName", api_data.get("_title", "fail")) tmp.image_urls = _ImageUrlModel.from_json(search_data["keyImages"]) - links = api_data["pages"][0]["data"]["socialLinks"] + 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["pages"][0]["data"]["requirements"]["languages"] + tmp.available_voice_langs = api_data["data"]["requirements"]["languages"] tmp.reqs = [] - for i, system in enumerate(api_data["pages"][0]["data"]["requirements"]["systems"]): + for i, system in enumerate(api_data["data"]["requirements"]["systems"]): tmp.reqs.append({"name": system["systemType"], "value": []}) for req in system["details"]: tmp.reqs[i]["value"].append(tuple((req["minimum"], req["recommended"]))) - tmp.publisher = api_data["pages"][0]["data"]["meta"].get("publisher", "undefined") - tmp.developer = api_data["pages"][0]["data"]["meta"].get("developer", "undefined") + tmp.publisher = api_data["data"]["meta"].get("publisher", "undefined") + tmp.developer = api_data["data"]["meta"].get("developer", "undefined") tmp.price = search_data['price']['totalPrice']['fmtPrice']['originalPrice'] tmp.discount_price = search_data['price']['totalPrice']['fmtPrice']['discountPrice'] From 9d02187d2f58188d4b402c81d436b27c32309a40 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 9 Jun 2021 23:12:49 +0200 Subject: [PATCH 08/30] Add requirements for shop --- rare/components/tabs/shop/search_results.py | 7 ++- rare/components/tabs/shop/shop_info.py | 40 +++++++++++++-- rare/components/tabs/shop/shop_models.py | 12 ++--- .../components/tabs/store/shop_game_info.py | 36 +++++++++---- .../components/tabs/store/shop_game_info.ui | 50 ++++++++++++++++--- 5 files changed, 116 insertions(+), 29 deletions(-) diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index 2cedd48d..9b2fb361 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -1,17 +1,19 @@ from PyQt5 import QtGui from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QGroupBox class SearchResults(QScrollArea): show_info = pyqtSignal(dict) + # TODO nice look def __init__(self): super(SearchResults, self).__init__() self.widget = QWidget() self.layout = QVBoxLayout() self.widget.setLayout(self.layout) self.setWidget(self.widget) + self.setWidgetResizable(True) def show_results(self, results: list): QVBoxLayout().addWidget(self.widget) @@ -28,13 +30,14 @@ class SearchResults(QScrollArea): self.setWidget(self.widget) -class _SearchResultItem(QWidget): +class _SearchResultItem(QGroupBox): res: dict show_info = pyqtSignal(dict) def __init__(self, result: dict): super(_SearchResultItem, self).__init__() self.layout = QHBoxLayout() + self.res = result self.title = QLabel(self.res["title"]) self.layout.addWidget(self.title) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index e76f934f..ab9334ce 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -3,9 +3,12 @@ import logging import webbrowser from PyQt5.QtCore import QLocale, QUrl, QJsonDocument, QJsonParseError, Qt -from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QPixmap, QFont from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply -from PyQt5.QtWidgets import QWidget +from PyQt5.QtWidgets import QWidget, QLabel +from rare.utils.utils import get_lang + +from rare.utils.extra_widgets import WaitingSpinner from rare.components.tabs.shop.shop_models import ShopGame from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info @@ -17,14 +20,18 @@ class ShopGameInfo(QWidget, Ui_shop_info): game: ShopGame data: dict - # TODO Design; More information(requirements, more images); bundles (eg EA triple) + # TODO Design def __init__(self): super(ShopGameInfo, self).__init__() self.setupUi(self) self.open_store_button.clicked.connect(self.button_clicked) + self.image_stack.addWidget(WaitingSpinner()) self.manager = QNetworkAccessManager() def update_game(self, data: dict): + self.image_stack.setCurrentIndex(1) + for i in reversed(range(self.req_group_box.layout().count())): + self.req_group_box.layout().itemAt(i).widget().setParent(None) slug = data["productSlug"] if "/home" in slug: slug = slug.replace("/home", "") @@ -40,7 +47,8 @@ class ShopGameInfo(QWidget, Ui_shop_info): is_bundle = True # init API request - locale = QLocale.system().name().split("_")[0] + locale = get_lang() + locale = "en" url = f"https://store-content.ak.epicgames.com/api/{locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" # game = api_utils.get_product(slug, locale) self.request = self.manager.get(QNetworkRequest(QUrl(url))) @@ -69,12 +77,33 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.price.setText(self.game.price) self.discount_price.setText(self.game.discount_price) + # print(self.game.reqs) + bold_font = QFont() + bold_font.setBold(True) + min_label = QLabel(self.tr("Minimum")) + min_label.setFont(bold_font) + rec_label = QLabel(self.tr("Recommend")) + rec_label.setFont(bold_font) + self.req_group_box.layout().addWidget(min_label, 0, 1) + self.req_group_box.layout().addWidget(rec_label, 0, 2) + + for i, (key, value) in enumerate(self.game.reqs["Windows"].items()): + self.req_group_box.layout().addWidget(QLabel(key), i+1, 0) + min_label = QLabel(value[0]) + min_label.setWordWrap(True) + self.req_group_box.layout().addWidget(min_label, i+1, 1) + rec_label = QLabel(value[1]) + rec_label.setWordWrap(True) + self.req_group_box.layout().addWidget(rec_label, i+1, 2) self.image_request = self.manager.get(QNetworkRequest(QUrl(self.game.image_urls.offer_image_tall))) self.image_request.finished.connect(self.image_loaded) try: - self.dev.setText(",".join(self.game.developer)) + if isinstance(self.game.developer, list): + self.dev.setText(", ".join(self.game.developer)) + else: + self.dev.setText(self.game.developer) except KeyError: pass @@ -88,6 +117,7 @@ class ShopGameInfo(QWidget, Ui_shop_info): pixmap = QPixmap() pixmap.loadFromData(data) self.image.setPixmap(pixmap.scaled(240, 320, transformMode=Qt.SmoothTransformation)) + self.image_stack.setCurrentIndex(0) else: logger.error("Load image failed") diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index b9b9b594..e6942f2d 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -1,5 +1,3 @@ - - class _ImageUrlModel: def __init__(self, front_tall: str = "", offer_image_tall: str = "", thumbnail: str = "", front_wide: str = ""): @@ -26,8 +24,8 @@ class _ImageUrlModel: class ShopGame: # TODO: Copyrights etc def __init__(self, title: str = "", image_urls: _ImageUrlModel = None, social_links: dict = None, - langs: list = None, reqs: list = None, publisher: str = "", developer: str = "", - original_price: str = "", discount_price: str=""): + langs: list = None, reqs: dict = None, publisher: str = "", developer: str = "", + original_price: str = "", discount_price: str = ""): self.title = title self.image_urls = image_urls self.links = [] @@ -64,11 +62,11 @@ class ShopGame: if item.startswith("link"): tmp.links.append(tuple((item.replace("link", ""), links[item]))) tmp.available_voice_langs = api_data["data"]["requirements"]["languages"] - tmp.reqs = [] + tmp.reqs = {} for i, system in enumerate(api_data["data"]["requirements"]["systems"]): - tmp.reqs.append({"name": system["systemType"], "value": []}) + tmp.reqs[system["systemType"]] = {} for req in system["details"]: - tmp.reqs[i]["value"].append(tuple((req["minimum"], req["recommended"]))) + tmp.reqs[system["systemType"]][req["title"]] = (req["minimum"], req["recommended"]) tmp.publisher = api_data["data"]["meta"].get("publisher", "undefined") tmp.developer = api_data["data"]["meta"].get("developer", "undefined") diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index 5fa9bcba..ca03f6b5 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -22,9 +22,18 @@ class Ui_shop_info(object): self.verticalLayout.addWidget(self.back_button) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") - self.image = QtWidgets.QLabel(shop_info) + self.image_stack = QtWidgets.QStackedWidget(shop_info) + self.image_stack.setObjectName("image_stack") + self.page = QtWidgets.QWidget() + self.page.setObjectName("page") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.page) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.image = QtWidgets.QLabel(self.page) + self.image.setMinimumSize(QtCore.QSize(240, 320)) self.image.setObjectName("image") - self.horizontalLayout.addWidget(self.image) + self.verticalLayout_3.addWidget(self.image) + self.image_stack.addWidget(self.page) + self.horizontalLayout.addWidget(self.image_stack) self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2.setObjectName("verticalLayout_2") self.title = QtWidgets.QLabel(shop_info) @@ -42,17 +51,25 @@ class Ui_shop_info(object): self.discount_price = QtWidgets.QLabel(shop_info) self.discount_price.setObjectName("discount_price") self.verticalLayout_2.addWidget(self.discount_price) - self.horizontalLayout.addLayout(self.verticalLayout_2) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem) - self.verticalLayout.addLayout(self.horizontalLayout) self.open_store_button = QtWidgets.QPushButton(shop_info) self.open_store_button.setObjectName("open_store_button") - self.verticalLayout.addWidget(self.open_store_button) - spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem1) + self.verticalLayout_2.addWidget(self.open_store_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) + spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem2) self.retranslateUi(shop_info) + self.image_stack.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(shop_info) def retranslateUi(self, shop_info): @@ -65,6 +82,7 @@ class Ui_shop_info(object): self.price.setText(_translate("shop_info", "TextLabel")) self.discount_price.setText(_translate("shop_info", "TextLabel")) self.open_store_button.setText(_translate("shop_info", "Buy Game in Epic Games Store")) + self.req_group_box.setTitle(_translate("shop_info", "Requirements")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index 5ca98cc4..9bdfe083 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -24,10 +24,27 @@ - - - TextLabel + + + 0 + + + + + + + 240 + 320 + + + + TextLabel + + + + + @@ -69,6 +86,26 @@ + + + + Buy Game in Epic Games Store + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -87,10 +124,11 @@ - - - Buy Game in Epic Games Store + + + Requirements + From 337b738599db47b41fc0187a99cbad61d4809435 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Thu, 10 Jun 2021 14:13:28 +0200 Subject: [PATCH 09/30] Nice look of search results; better look for shop info; added ImageLabel in extra_widgets.py --- rare/components/tabs/shop/search_results.py | 64 ++++++++++++++----- rare/components/tabs/shop/shop_info.py | 40 ++++++------ rare/components/tabs/shop/shop_widget.py | 13 +++- .../components/tabs/store/shop_game_info.py | 12 +--- .../components/tabs/store/shop_game_info.ui | 19 +----- rare/utils/extra_widgets.py | 43 ++++++++++++- 6 files changed, 120 insertions(+), 71 deletions(-) diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index 9b2fb361..bfcad500 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -1,33 +1,43 @@ from PyQt5 import QtGui -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QGroupBox +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QGroupBox, QPushButton + +from rare.utils.extra_widgets import ImageLabel, FlowLayout -class SearchResults(QScrollArea): +class SearchResults(QWidget): show_info = pyqtSignal(dict) # TODO nice look def __init__(self): super(SearchResults, self).__init__() + self.main_layout = QVBoxLayout() + self.back_button = QPushButton() + self.main_layout.addWidget(self.back_button) + self.main_layout.addWidget(self.back_button) + self.result_area = QScrollArea() self.widget = QWidget() - self.layout = QVBoxLayout() + 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.setWidget(self.widget) - self.setWidgetResizable(True) + + self.setLayout(self.main_layout) def show_results(self, results: list): QVBoxLayout().addWidget(self.widget) self.widget = QWidget() - self.layout = QVBoxLayout() - for i in range(self.layout.count()): - self.layout.removeItem(i) + self.layout = FlowLayout() for res in results: w = _SearchResultItem(res) w.show_info.connect(self.show_info.emit) self.layout.addWidget(w) - self.layout.addStretch(1) self.widget.setLayout(self.layout) - self.setWidget(self.widget) + self.result_area.setWidget(self.widget) class _SearchResultItem(QGroupBox): @@ -36,17 +46,41 @@ class _SearchResultItem(QGroupBox): def __init__(self, result: dict): super(_SearchResultItem, self).__init__() - self.layout = QHBoxLayout() + 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) - original_price = result['price']['totalPrice']['fmtPrice']['originalPrice'] - self.price = QLabel(f"{self.tr('Original price: ')}{original_price}") - self.layout.addWidget(self.price) + price = result['price']['totalPrice']['fmtPrice']['originalPrice'] + discount_price = result['price']['totalPrice']['fmtPrice']['discountPrice'] + price_layout = QHBoxLayout() + price = QLabel(price) + price_layout.addWidget(price) + if price != discount_price: + font = QFont() + font.setStrikeOut(True) + price.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/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index ab9334ce..c3bb6f11 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -2,16 +2,15 @@ import json import logging import webbrowser -from PyQt5.QtCore import QLocale, QUrl, QJsonDocument, QJsonParseError, Qt +from PyQt5.QtCore import QUrl, QJsonDocument, QJsonParseError from PyQt5.QtGui import QPixmap, QFont from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget, QLabel -from rare.utils.utils import get_lang - -from rare.utils.extra_widgets import WaitingSpinner from rare.components.tabs.shop.shop_models import ShopGame from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info +from rare.utils.extra_widgets import WaitingSpinner, ImageLabel +from rare.utils.utils import get_lang logger = logging.getLogger("ShopInfo") @@ -25,6 +24,8 @@ class ShopGameInfo(QWidget, Ui_shop_info): super(ShopGameInfo, self).__init__() self.setupUi(self) self.open_store_button.clicked.connect(self.button_clicked) + self.image = ImageLabel() + self.image_stack.addWidget(self.image) self.image_stack.addWidget(WaitingSpinner()) self.manager = QNetworkAccessManager() @@ -48,7 +49,6 @@ class ShopGameInfo(QWidget, Ui_shop_info): # init API request locale = get_lang() - locale = "en" url = f"https://store-content.ak.epicgames.com/api/{locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" # game = api_utils.get_product(slug, locale) self.request = self.manager.get(QNetworkRequest(QUrl(url))) @@ -76,7 +76,11 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.title.setText(self.game.title) self.price.setText(self.game.price) - self.discount_price.setText(self.game.discount_price) + if self.game.price != self.game.discount_price: + self.discount_price.setText(self.game.discount_price) + self.discount_price.setVisible(True) + else: + self.discount_price.setVisible(False) # print(self.game.reqs) bold_font = QFont() bold_font.setBold(True) @@ -88,16 +92,19 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.req_group_box.layout().addWidget(rec_label, 0, 2) for i, (key, value) in enumerate(self.game.reqs["Windows"].items()): - self.req_group_box.layout().addWidget(QLabel(key), i+1, 0) + self.req_group_box.layout().addWidget(QLabel(key), i + 1, 0) min_label = QLabel(value[0]) min_label.setWordWrap(True) - self.req_group_box.layout().addWidget(min_label, i+1, 1) + self.req_group_box.layout().addWidget(min_label, i + 1, 1) rec_label = QLabel(value[1]) rec_label.setWordWrap(True) - self.req_group_box.layout().addWidget(rec_label, i+1, 2) + self.req_group_box.layout().addWidget(rec_label, i + 1, 2) - self.image_request = self.manager.get(QNetworkRequest(QUrl(self.game.image_urls.offer_image_tall))) - self.image_request.finished.connect(self.image_loaded) + self.image.update_image(self.game.image_urls.front_tall, self.game.title, (240, 320)) + + self.image_stack.setCurrentIndex(0) + # self.image_request = self.manager.get(QNetworkRequest(QUrl(self.game.image_urls.offer_image_tall))) + # self.image_request.finished.connect(self.image_loaded) try: if isinstance(self.game.developer, list): @@ -111,16 +118,5 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.request.deleteLater() - def image_loaded(self): - if self.image_request and self.image_request.error() == QNetworkReply.NoError: - data = self.image_request.readAll().data() - pixmap = QPixmap() - pixmap.loadFromData(data) - self.image.setPixmap(pixmap.scaled(240, 320, transformMode=Qt.SmoothTransformation)) - self.image_stack.setCurrentIndex(0) - else: - logger.error("Load image failed") - def button_clicked(self): webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) - diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 3be1a464..86340e95 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -13,7 +13,7 @@ from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkRepl from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QCompleter from rare.ui.components.tabs.store.store import Ui_ShopWidget -from rare.utils.extra_widgets import WaitingSpinner +from rare.utils.extra_widgets import WaitingSpinner, ImageLabel from rare.utils.utils import get_lang @@ -55,7 +55,6 @@ class ShopWidget(QWidget, Ui_ShopWidget): if self.free_game_request.error() == QNetworkReply.NoError: try: free_games = json.loads(self.free_game_request.readAll().data().decode()) - print(free_games) except JSONDecodeError: return else: @@ -177,7 +176,15 @@ class GameWidget(QWidget): def __init__(self, json_info, path: str): super(GameWidget, self).__init__() self.layout = QVBoxLayout() - self.image = QLabel() + self.image = ImageLabel() + for img in json_info["keyImages"]: + if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: + width = 300 + self.image.update_image(img["url"], json_info["title"], (width, int(width * 9 / 16))) + break + else: + print("No image found") + self.slug = json_info["productSlug"] self.title = json_info["title"] if not os.path.exists(p := os.path.join(path, f"{json_info['title']}.png")): diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index ca03f6b5..f259a955 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -24,15 +24,6 @@ class Ui_shop_info(object): self.horizontalLayout.setObjectName("horizontalLayout") self.image_stack = QtWidgets.QStackedWidget(shop_info) self.image_stack.setObjectName("image_stack") - self.page = QtWidgets.QWidget() - self.page.setObjectName("page") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.page) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.image = QtWidgets.QLabel(self.page) - self.image.setMinimumSize(QtCore.QSize(240, 320)) - self.image.setObjectName("image") - self.verticalLayout_3.addWidget(self.image) - self.image_stack.addWidget(self.page) self.horizontalLayout.addWidget(self.image_stack) self.verticalLayout_2 = QtWidgets.QVBoxLayout() self.verticalLayout_2.setObjectName("verticalLayout_2") @@ -69,14 +60,13 @@ class Ui_shop_info(object): self.verticalLayout.addItem(spacerItem2) self.retranslateUi(shop_info) - self.image_stack.setCurrentIndex(0) + self.image_stack.setCurrentIndex(-1) QtCore.QMetaObject.connectSlotsByName(shop_info) def retranslateUi(self, shop_info): _translate = QtCore.QCoreApplication.translate shop_info.setWindowTitle(_translate("shop_info", "Form")) self.back_button.setText(_translate("shop_info", "Back")) - self.image.setText(_translate("shop_info", "TextLabel")) self.title.setText(_translate("shop_info", "Error")) self.dev.setText(_translate("shop_info", "TextLabel")) self.price.setText(_translate("shop_info", "TextLabel")) diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index 9bdfe083..17ebd5a8 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -26,25 +26,8 @@ - 0 + -1 - - - - - - - 240 - 320 - - - - TextLabel - - - - - diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index cfc66800..47c6f731 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -1,7 +1,8 @@ import os -from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal -from PyQt5.QtGui import QMovie +from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QUrl +from PyQt5.QtGui import QMovie, QPixmap +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, QHBoxLayout, QWidget, QPushButton, \ QStyleOptionTab, QStylePainter, QTabBar from qtawesome import icon @@ -250,3 +251,41 @@ class SelectViewWidget(QWidget): self.list_view.setIcon(icon("fa5s.list", color="orange")) self.icon_view = True self.toggled.emit() + + +class ImageLabel(QLabel): + + def __init__(self): + super(ImageLabel, self).__init__() + path = os.path.expanduser("~/.cache/rare/cache") + if p := os.environ.get("XDG_CACHE_HOME"): + path = p + self.path = path + self.manager = QNetworkAccessManager() + + def update_image(self, url, name, size: tuple = (240, 320)): + self.setFixedSize(*size) + self.img_size = size + self.name = name + for c in r'<>?":|\/* ': + self.name = self.name.replace(c, "") + if not os.path.exists(os.path.join(self.path, self.name+".png")): + self.request = self.manager.get(QNetworkRequest(QUrl(url))) + self.request.finished.connect(self.image_ready) + else: + self.show_image() + + def image_ready(self): + if self.request: + if self.request.error() == QNetworkReply.NoError: + with open(os.path.join(self.path, self.name + ".png"), "wb") as file: + file.write(self.request.readAll().data()) + file.close() + self.show_image() + else: + return + + def show_image(self): + self.image = QPixmap(os.path.join(self.path, self.name + ".png")).scaled(*self.img_size, + transformMode=Qt.SmoothTransformation) + self.setPixmap(self.image) From 676c253a13916baff1ffb79300ed8bf4b049882d Mon Sep 17 00:00:00 2001 From: Dummerle Date: Thu, 10 Jun 2021 19:58:35 +0200 Subject: [PATCH 10/30] Some improvements --- rare/components/tab_widget.py | 18 +-- rare/components/tabs/shop/__init__.py | 2 +- rare/components/tabs/shop/search_results.py | 2 +- rare/components/tabs/shop/shop_models.py | 9 +- rare/components/tabs/shop/shop_widget.py | 125 ++++++++++++++------ rare/ui/components/tabs/store/store.py | 46 ++----- rare/ui/components/tabs/store/store.ui | 36 +----- test.py | 20 ---- 8 files changed, 114 insertions(+), 144 deletions(-) delete mode 100644 test.py diff --git a/rare/components/tab_widget.py b/rare/components/tab_widget.py index 566e08bf..238e39f1 100644 --- a/rare/components/tab_widget.py +++ b/rare/components/tab_widget.py @@ -1,5 +1,3 @@ -import webbrowser - from PyQt5.QtCore import QSize, pyqtSignal from PyQt5.QtWidgets import QMenu, QTabWidget, QWidget, QWidgetAction from qtawesome import icon @@ -26,32 +24,23 @@ class TabWidget(QTabWidget): disabled_tab = 4 if not offline else 1 self.core = core self.setTabBar(TabBar(disabled_tab)) - # Generate Tabs self.games_tab = GameTab(core, self, offline) self.addTab(self.games_tab, self.tr("Games")) - if not offline: updates = self.games_tab.default_widget.game_list.updates self.downloadTab = DownloadTab(core, updates, self) self.addTab(self.downloadTab, "Downloads" + (" (" + str(len(updates)) + ")" if len(updates) != 0 else "")) - self.cloud_saves = SyncSaves(core, self) self.addTab(self.cloud_saves, "Cloud Saves") - self.store = Shop() - self.addTab(self.store, self.tr("Store")) - + self.addTab(self.store, self.tr("Store (Beta)")) self.settings = SettingsTab(core, self) # Space Tab self.addTab(QWidget(), "") self.setTabEnabled(disabled_tab, False) - # Buttons - store_button = TabButtonWidget(core, 'fa.shopping-cart', 'Epic Games Store') - store_button.pressed.connect(lambda: webbrowser.open("https://www.epicgames.com/store")) - self.tabBar().setTabButton(disabled_tab, self.tabBar().RightSide, store_button) - + # Button self.account = QWidget() self.addTab(self.account, "") self.setTabEnabled(disabled_tab + 1, False) @@ -151,8 +140,7 @@ class TabWidget(QTabWidget): self.setTabText(1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")) self.downloadTab.update_text.setVisible(len(self.downloadTab.update_widgets) == 0) - # Update gamelist and set text of Downlaods to "Downloads" - + # Update gamelist and set text of Downloads to "Downloads" def dl_finished(self, update_list): if update_list[0]: self.games_tab.default_widget.game_list.update_list(update_list[1]) diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index d363e6b6..64269786 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -22,6 +22,7 @@ class Shop(QStackedWidget): 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_info) self.shop.show_game.connect(self.show_game_info) @@ -37,4 +38,3 @@ class Shop(QStackedWidget): def show_info(self, data): self.search_results.show_results(data) self.setCurrentIndex(1) - diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index bfcad500..f9c035e8 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -13,7 +13,7 @@ class SearchResults(QWidget): def __init__(self): super(SearchResults, self).__init__() self.main_layout = QVBoxLayout() - self.back_button = QPushButton() + 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() diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index e6942f2d..f5f7b485 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -36,7 +36,7 @@ class ShopGame: else: self.links = [] self.languages = langs - self.reqs = reqs # {"Betriebssystem":win7, processor:i9 9900k, ram...}; Note: name from language + self.reqs = reqs self.publisher = publisher self.developer = developer self.price = original_price @@ -44,7 +44,6 @@ class ShopGame: @classmethod def from_json(cls, api_data: dict, search_data: dict): - print(api_data) if isinstance(api_data, list): for product in api_data: if product["_title"] == "home": @@ -66,8 +65,10 @@ class ShopGame: for i, system in enumerate(api_data["data"]["requirements"]["systems"]): tmp.reqs[system["systemType"]] = {} for req in system["details"]: - tmp.reqs[system["systemType"]][req["title"]] = (req["minimum"], req["recommended"]) - + try: + tmp.reqs[system["systemType"]][req["title"]] = (req["minimum"], req["recommended"]) + except KeyError: + pass tmp.publisher = api_data["data"]["meta"].get("publisher", "undefined") tmp.developer = api_data["data"]["meta"].get("developer", "undefined") tmp.price = search_data['price']['totalPrice']['fmtPrice']['originalPrice'] diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 86340e95..05bba24c 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -6,17 +6,18 @@ from json import JSONDecodeError import requests from PyQt5 import QtGui -from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QByteArray, QJsonDocument, QJsonParseError, \ +from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QJsonDocument, QJsonParseError, \ QStringListModel from PyQt5.QtGui import QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QCompleter +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QCompleter, QGroupBox, QHBoxLayout from rare.ui.components.tabs.store.store import Ui_ShopWidget -from rare.utils.extra_widgets import WaitingSpinner, ImageLabel +from rare.utils.extra_widgets import WaitingSpinner, ImageLabel, FlowLayout from rare.utils.utils import get_lang +# noinspection PyAttributeOutsideInit,PyBroadException class ShopWidget(QWidget, Ui_ShopWidget): show_info = pyqtSignal(list) show_game = pyqtSignal(dict) @@ -25,10 +26,17 @@ class ShopWidget(QWidget, Ui_ShopWidget): def __init__(self): super(ShopWidget, self).__init__() self.setupUi(self) - self.search_results.setVisible(False) self.manager = QNetworkAccessManager() + self.free_games_widget = QWidget() + self.free_games_widget.setLayout(FlowLayout()) + self.free_games_now = QGroupBox(self.tr("Free Games")) + self.free_games_now.setLayout(QHBoxLayout()) + self.free_games_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_games_widget.layout().addWidget(self.coming_free_games) self.free_games_stack.addWidget(WaitingSpinner()) - self.free_games_stack.setCurrentIndex(1) + self.free_games_stack.addWidget(self.free_games_widget) self.search.textChanged.connect(self.search_games) self.completer = QCompleter() self.completer.setCaseSensitivity(Qt.CaseInsensitive) @@ -45,8 +53,7 @@ class ShopWidget(QWidget, Ui_ShopWidget): os.makedirs(self.path) url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" self.free_game_request = self.manager.get(QNetworkRequest(QUrl(url))) - self.free_game_request.readyRead.connect(self.add_free_games) - self.free_game_request.finished.connect(self.free_game_request.deleteLater if self.free_game_request else None) + self.free_game_request.finished.connect(self.add_free_games) # free_games = api_utils.get_free_games() @@ -71,20 +78,30 @@ class ShopWidget(QWidget, Ui_ShopWidget): continue try: # parse datetime - end_date = datetime.datetime.strptime( - game["promotions"].get("promotionalOffers", game["promotions"].get("upcomingPromotionalOffers"))[0][ - "promotionalOffers"][0]["endDate"], - '%Y-%m-%dT%H:%M:%S.%fZ') - start_date = datetime.datetime.strptime( - game["promotions"].get("promotionalOffers", game["promotions"].get("upcomingPromotionalOffers"))[0][ - "promotionalOffers"][0]["startDate"], - '%Y-%m-%dT%H:%M:%S.%fZ') - except IndexError: - print("index error") - continue - except KeyError: - print("keyerror") - continue + try: + end_date = datetime.datetime.strptime( + game["promotions"]["upcomingPromotionalOffers"][0]["promotionalOffers"][0]["endDate"], + '%Y-%m-%dT%H:%M:%S.%fZ') + except Exception: + try: + end_date = datetime.datetime.strptime( + game["promotions"]["promotionalOffers"][0]["promotionalOffers"][0]["endDate"], + '%Y-%m-%dT%H:%M:%S.%fZ') + except Exception: + continue + 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: + print(e) + continue + except TypeError: print("type error") continue @@ -95,30 +112,29 @@ class ShopWidget(QWidget, Ui_ShopWidget): for free_game in free_games_now: w = GameWidget(free_game, self.path) - w.show_info.connect(lambda x: self.search_games(x, True)) - self.free_game_now.layout().addWidget(w) + w.show_info.connect(self.show_game.emit) + self.free_games_now.layout().addWidget(w) self.free_game_widgets.append(w) - self.free_game_group_box_2.setMinimumHeight(200) + self.free_games_now.layout().addStretch(1) for free_game in coming_free_games: w = GameWidget(free_game, self.path) if free_game["title"] != "Mystery Game": - w.show_info.connect(self.show_info) - self.comming_free_game.layout().addWidget(w) + w.show_info.connect(self.show_game.emit) + self.coming_free_games.layout().addWidget(w) self.free_game_widgets.append(w) - self.free_games_stack.setCurrentIndex(0) + self.coming_free_games.layout().addStretch(1) + # self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300)) + self.free_games_stack.setCurrentIndex(1) + self.free_game_request.deleteLater() def search_games(self, text, show_direct=False): if text == "": self.search_results.setVisible(False) else: locale = get_lang() - payload = QByteArray() - payload.append( - "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") - payload = json.dumps({ - "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", + "query": query, "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 10, "country": "DE", "keywords": text, "locale": locale, "sortDir": "DESC", "allowCountries": locale.upper(), @@ -127,7 +143,6 @@ class ShopWidget(QWidget, Ui_ShopWidget): request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") self.search_request = self.manager.post(request, payload) - # self.search_request = self.manager.post(QNetworkRequest(QUrl("https://www.epicgames.com/graphql")), payload) self.search_request.finished.connect(lambda: self.show_search_results(show_direct)) def show_search_results(self, show_direct=False): @@ -167,16 +182,22 @@ class ShopWidget(QWidget, Ui_ShopWidget): if self.data: self.show_info.emit(self.data) else: - self.show_game.emit(self.data[0]) + try: + result = self.data[0] + except IndexError: + print("error") + return + self.show_game.emit(result) class GameWidget(QWidget): - show_info = pyqtSignal(str) + show_info = pyqtSignal(dict) def __init__(self, json_info, path: str): super(GameWidget, self).__init__() self.layout = QVBoxLayout() self.image = ImageLabel() + self.json_info = json_info for img in json_info["keyImages"]: if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: width = 300 @@ -213,4 +234,36 @@ class GameWidget(QWidget): self.setLayout(self.layout) def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - self.show_info.emit(self.title) + self.show_info.emit(self.json_info) + + +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 " diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py index ce8adcfb..ccbf9aa0 100644 --- a/rare/ui/components/tabs/store/store.py +++ b/rare/ui/components/tabs/store/store.py @@ -20,54 +20,32 @@ class Ui_ShopWidget(object): self.search = QtWidgets.QLineEdit(ShopWidget) self.search.setObjectName("search") self.verticalLayout.addWidget(self.search) - self.search_results = QtWidgets.QGroupBox(ShopWidget) - self.search_results.setFlat(False) - self.search_results.setObjectName("search_results") - self.verticalLayout.addWidget(self.search_results) - self.free_game_group_box_2 = QtWidgets.QGroupBox(ShopWidget) - self.free_game_group_box_2.setObjectName("free_game_group_box_2") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.free_game_group_box_2) + self.free_game_group_box = QtWidgets.QGroupBox(ShopWidget) + self.free_game_group_box.setObjectName("free_game_group_box") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.free_game_group_box) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.free_games_stack = QtWidgets.QStackedWidget(self.free_game_group_box_2) + self.free_games_stack = QtWidgets.QStackedWidget(self.free_game_group_box) self.free_games_stack.setObjectName("free_games_stack") - self.free_games_page = QtWidgets.QWidget() - self.free_games_page.setObjectName("free_games_page") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.free_games_page) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.free_game_now = QtWidgets.QGroupBox(self.free_games_page) - self.free_game_now.setObjectName("free_game_now") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.free_game_now) - self.horizontalLayout.setObjectName("horizontalLayout") - self.horizontalLayout_3.addWidget(self.free_game_now) - self.comming_free_game = QtWidgets.QGroupBox(self.free_games_page) - self.comming_free_game.setObjectName("comming_free_game") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.comming_free_game) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.horizontalLayout_3.addWidget(self.comming_free_game) - self.free_games_stack.addWidget(self.free_games_page) self.verticalLayout_2.addWidget(self.free_games_stack) - self.verticalLayout.addWidget(self.free_game_group_box_2) - self.games_groupbox_2 = QtWidgets.QGroupBox(ShopWidget) - self.games_groupbox_2.setObjectName("games_groupbox_2") - self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.games_groupbox_2) + self.verticalLayout.addWidget(self.free_game_group_box) + self.games_groupbox = QtWidgets.QGroupBox(ShopWidget) + self.games_groupbox.setObjectName("games_groupbox") + self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.games_groupbox) self.horizontalLayout_7.setObjectName("horizontalLayout_7") - self.verticalLayout.addWidget(self.games_groupbox_2) + self.verticalLayout.addWidget(self.games_groupbox) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout.addItem(spacerItem) self.retranslateUi(ShopWidget) - self.free_games_stack.setCurrentIndex(0) + self.free_games_stack.setCurrentIndex(-1) QtCore.QMetaObject.connectSlotsByName(ShopWidget) def retranslateUi(self, ShopWidget): _translate = QtCore.QCoreApplication.translate ShopWidget.setWindowTitle(_translate("ShopWidget", "Form")) self.search.setPlaceholderText(_translate("ShopWidget", "Search Games")) - self.search_results.setTitle(_translate("ShopWidget", "Search results")) - self.free_game_group_box_2.setTitle(_translate("ShopWidget", "Free Games")) - self.free_game_now.setTitle(_translate("ShopWidget", "Free Game")) - self.comming_free_game.setTitle(_translate("ShopWidget", "Comming Free Game")) - self.games_groupbox_2.setTitle(_translate("ShopWidget", "Other nice games")) + self.free_game_group_box.setTitle(_translate("ShopWidget", "Free Games")) + self.games_groupbox.setTitle(_translate("ShopWidget", "Other nice games")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/store/store.ui b/rare/ui/components/tabs/store/store.ui index 1e470192..587130de 100644 --- a/rare/ui/components/tabs/store/store.ui +++ b/rare/ui/components/tabs/store/store.ui @@ -22,17 +22,7 @@ - - - Search results - - - false - - - - - + Free Games @@ -40,35 +30,15 @@ - 0 + -1 - - - - - - Free Game - - - - - - - - Comming Free Game - - - - - - - + Other nice games diff --git a/test.py b/test.py deleted file mode 100644 index de073353..00000000 --- a/test.py +++ /dev/null @@ -1,20 +0,0 @@ -import sys -from PyQt5.QtCore import Qt, QStringListModel -from PyQt5.QtWidgets import QApplication, QCompleter, QLineEdit - -def get_data(model): - model.setStringList(["completion", "data", "goes", "here"]) - -if __name__ == "__main__": - - app = QApplication(sys.argv) - edit = QLineEdit() - completer = QCompleter() - edit.setCompleter(completer) - - model = QStringListModel() - completer.setModel(model) - get_data(model) - - edit.show() - sys.exit(app.exec_()) \ No newline at end of file From bd6e195e377c8fb8921ab2933dc3298b93d2af72 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Fri, 11 Jun 2021 12:56:25 +0200 Subject: [PATCH 11/30] Fix some bugs with images --- rare/components/tabs/shop/shop_widget.py | 9 ++------- rare/utils/extra_widgets.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 05bba24c..d63efe54 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -129,13 +129,11 @@ class ShopWidget(QWidget, Ui_ShopWidget): self.free_game_request.deleteLater() def search_games(self, text, show_direct=False): - if text == "": - self.search_results.setVisible(False) - else: + if text != "": locale = get_lang() payload = json.dumps({ "query": query, - "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 10, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 20, "country": "DE", "keywords": text, "locale": locale, "sortDir": "DESC", "allowCountries": locale.upper(), "start": 0, "tag": "", "withMapping": False, "withPrice": True} @@ -154,7 +152,6 @@ class ShopWidget(QWidget, Ui_ShopWidget): data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"]["elements"] else: logging.error(error.errorString()) - self.search_results.setVisible(False) return # response = .decode(encoding="utf-8") # print(response) @@ -172,8 +169,6 @@ class ShopWidget(QWidget, Ui_ShopWidget): model.setStringList(titles) self.completer.setModel(model) # self.completer.popup() - # self.search_results.setLayout(layout) - # self.search_results.setVisible(True) if self.search_request: self.search_request.deleteLater() diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 47c6f731..c47b7fb3 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -269,7 +269,12 @@ class ImageLabel(QLabel): self.name = name for c in r'<>?":|\/* ': self.name = self.name.replace(c, "") - if not os.path.exists(os.path.join(self.path, self.name+".png")): + if self.img_size[0] > self.img_size[1]: + name_extension = "wide" + else: + name_extension = "tall" + self.name = f"{self.name}_{name_extension}.png" + if not os.path.exists(os.path.join(self.path, self.name)): self.request = self.manager.get(QNetworkRequest(QUrl(url))) self.request.finished.connect(self.image_ready) else: @@ -278,7 +283,8 @@ class ImageLabel(QLabel): def image_ready(self): if self.request: if self.request.error() == QNetworkReply.NoError: - with open(os.path.join(self.path, self.name + ".png"), "wb") as file: + + with open(os.path.join(self.path, self.name), "wb") as file: file.write(self.request.readAll().data()) file.close() self.show_image() @@ -286,6 +292,6 @@ class ImageLabel(QLabel): return def show_image(self): - self.image = QPixmap(os.path.join(self.path, self.name + ".png")).scaled(*self.img_size, + self.image = QPixmap(os.path.join(self.path, self.name)).scaled(*self.img_size, transformMode=Qt.SmoothTransformation) self.setPixmap(self.image) From bc6b9e5ae2363acefa9935f882e891237fb44b0c Mon Sep 17 00:00:00 2001 From: Dummerle Date: Fri, 11 Jun 2021 13:37:09 +0200 Subject: [PATCH 12/30] Added other games,set type to scrollarea --- rare/components/tabs/shop/search_results.py | 7 +- rare/components/tabs/shop/shop_models.py | 2 +- rare/components/tabs/shop/shop_widget.py | 69 +++++++++++++-- rare/ui/components/tabs/store/store.py | 26 ++++-- rare/ui/components/tabs/store/store.ui | 98 ++++++++++++--------- rare/utils/extra_widgets.py | 2 +- 6 files changed, 142 insertions(+), 62 deletions(-) diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index f9c035e8..87ea7f85 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -67,12 +67,13 @@ class _SearchResultItem(QGroupBox): price = result['price']['totalPrice']['fmtPrice']['originalPrice'] discount_price = result['price']['totalPrice']['fmtPrice']['discountPrice'] price_layout = QHBoxLayout() - price = QLabel(price) - price_layout.addWidget(price) + price_label = QLabel(price) + price_layout.addWidget(price_label) + if price != discount_price: font = QFont() font.setStrikeOut(True) - price.setFont(font) + 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) diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index f5f7b485..23e367fb 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -60,7 +60,7 @@ class ShopGame: 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"]["languages"] + tmp.available_voice_langs = api_data["data"]["requirements"].get("languages", "Failed") tmp.reqs = {} for i, system in enumerate(api_data["data"]["requirements"]["systems"]): tmp.reqs[system["systemType"]] = {} diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index d63efe54..8bd37eb6 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QJsonDocument, QJsonParseError, \ QStringListModel from PyQt5.QtGui import QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QCompleter, QGroupBox, QHBoxLayout +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QCompleter, QGroupBox, QHBoxLayout, QScrollArea from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.utils.extra_widgets import WaitingSpinner, ImageLabel, FlowLayout @@ -18,13 +18,14 @@ from rare.utils.utils import get_lang # noinspection PyAttributeOutsideInit,PyBroadException -class ShopWidget(QWidget, Ui_ShopWidget): +class ShopWidget(QScrollArea, Ui_ShopWidget): show_info = pyqtSignal(list) show_game = pyqtSignal(dict) free_game_widgets = [] def __init__(self): super(ShopWidget, self).__init__() + self.setWidgetResizable(True) self.setupUi(self) self.manager = QNetworkAccessManager() self.free_games_widget = QWidget() @@ -44,6 +45,8 @@ class ShopWidget(QWidget, Ui_ShopWidget): self.search.returnPressed.connect(self.show_search_result) self.data = [] + self.games_groupbox.setLayout(FlowLayout()) + def load(self): if p := os.getenv("XDG_CACHE_HOME"): self.path = p @@ -55,7 +58,12 @@ class ShopWidget(QWidget, Ui_ShopWidget): self.free_game_request = self.manager.get(QNetworkRequest(QUrl(url))) self.free_game_request.finished.connect(self.add_free_games) - # free_games = api_utils.get_free_games() + game_list = ["Satisfactory", "Among Us", "Star Wars Jedi Fallen Order", "Watch Dogs", "Subnautica Below Zero"] + # TODO read from api + for game in game_list: + w = GameWidget.from_request(game, self.path) + self.games_groupbox.layout().addWidget(w) + w.show_info.connect(self.show_game.emit) def add_free_games(self): if self.free_game_request: @@ -111,14 +119,14 @@ class ShopWidget(QWidget, Ui_ShopWidget): coming_free_games.append(game) for free_game in free_games_now: - w = GameWidget(free_game, self.path) + 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) self.free_games_now.layout().addStretch(1) for free_game in coming_free_games: - w = GameWidget(free_game, self.path) + 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) @@ -188,8 +196,14 @@ class ShopWidget(QWidget, Ui_ShopWidget): class GameWidget(QWidget): show_info = pyqtSignal(dict) - def __init__(self, json_info, path: str): + def __init__(self, path, json_info=None): super(GameWidget, self).__init__() + if json_info: + self.init_ui(json_info, path) + self.path = path + + def init_ui(self, json_info, path): + self.path = path self.layout = QVBoxLayout() self.image = ImageLabel() self.json_info = json_info @@ -203,7 +217,7 @@ class GameWidget(QWidget): self.slug = json_info["productSlug"] self.title = json_info["title"] - if not os.path.exists(p := os.path.join(path, f"{json_info['title']}.png")): + if not os.path.exists(p := os.path.join(self.path, f"{json_info['title']}.png")): for img in json_info["keyImages"]: if json_info["title"] != "Mystery Game": if img["type"] == "DieselStoreFrontWide": @@ -220,17 +234,56 @@ class GameWidget(QWidget): else: print("No image found") width = 300 - self.image.setPixmap(QPixmap(os.path.join(path, f"{json_info['title']}.png")) + self.image.setPixmap(QPixmap(os.path.join(self.path, f"{json_info['title']}.png")) .scaled(width, int(width * 9 / 16), transformMode=Qt.SmoothTransformation)) self.layout.addWidget(self.image) self.title_label = QLabel(json_info["title"]) + self.title_label.setWordWrap(True) self.layout.addWidget(self.title_label) self.setLayout(self.layout) def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: self.show_info.emit(self.json_info) + @classmethod + def from_request(cls, name, path): + c = cls(path) + c.manager = QNetworkAccessManager() + c.request = c.manager.get(QNetworkRequest()) + + locale = get_lang() + payload = json.dumps({ + "query": query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 1, + "country": "DE", "keywords": name, "locale": locale, "sortDir": "DESC", + "allowCountries": locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + }).encode() + request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + c.search_request = c.manager.post(request, payload) + c.search_request.finished.connect(lambda: c.handle_response(path)) + return c + + def handle_response(self, path): + if self.search_request: + if self.search_request.error() == QNetworkReply.NoError: + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) + if QJsonParseError.NoError == error.error: + data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ + "elements"][0] + self.init_ui(data, path) + else: + logging.error(error.errorString()) + return + + else: + return + else: + return + query = "query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, " \ "$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, " \ diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py index ccbf9aa0..6193e3a9 100644 --- a/rare/ui/components/tabs/store/store.py +++ b/rare/ui/components/tabs/store/store.py @@ -17,24 +17,32 @@ class Ui_ShopWidget(object): ShopWidget.resize(697, 362) self.verticalLayout = QtWidgets.QVBoxLayout(ShopWidget) self.verticalLayout.setObjectName("verticalLayout") - self.search = QtWidgets.QLineEdit(ShopWidget) + self.scrollArea = QtWidgets.QScrollArea(ShopWidget) + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setObjectName("scrollArea") + self.scrollAreaWidgetContents = QtWidgets.QWidget() + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 677, 342)) + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.search = QtWidgets.QLineEdit(self.scrollAreaWidgetContents) self.search.setObjectName("search") - self.verticalLayout.addWidget(self.search) - self.free_game_group_box = QtWidgets.QGroupBox(ShopWidget) + self.verticalLayout_3.addWidget(self.search) + self.free_game_group_box = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) self.free_game_group_box.setObjectName("free_game_group_box") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.free_game_group_box) self.verticalLayout_2.setObjectName("verticalLayout_2") self.free_games_stack = QtWidgets.QStackedWidget(self.free_game_group_box) self.free_games_stack.setObjectName("free_games_stack") self.verticalLayout_2.addWidget(self.free_games_stack) - self.verticalLayout.addWidget(self.free_game_group_box) - self.games_groupbox = QtWidgets.QGroupBox(ShopWidget) + self.verticalLayout_3.addWidget(self.free_game_group_box) + self.games_groupbox = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) self.games_groupbox.setObjectName("games_groupbox") - self.horizontalLayout_7 = QtWidgets.QHBoxLayout(self.games_groupbox) - self.horizontalLayout_7.setObjectName("horizontalLayout_7") - self.verticalLayout.addWidget(self.games_groupbox) + self.verticalLayout_3.addWidget(self.games_groupbox) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) + self.verticalLayout_3.addItem(spacerItem) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.verticalLayout.addWidget(self.scrollArea) self.retranslateUi(ShopWidget) self.free_games_stack.setCurrentIndex(-1) diff --git a/rare/ui/components/tabs/store/store.ui b/rare/ui/components/tabs/store/store.ui index 587130de..2c0beb2b 100644 --- a/rare/ui/components/tabs/store/store.ui +++ b/rare/ui/components/tabs/store/store.ui @@ -15,49 +15,67 @@ - - - Search Games + + + true + + + + 0 + 0 + 677 + 342 + + + + + + + Search Games + + + + + + + Free Games + + + + + + -1 + + + + + + + + + + Other nice games + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - - - - Free Games - - - - - - -1 - - - - - - - - - - Other nice games - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index c47b7fb3..fa2abde5 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -259,7 +259,7 @@ class ImageLabel(QLabel): super(ImageLabel, self).__init__() path = os.path.expanduser("~/.cache/rare/cache") if p := os.environ.get("XDG_CACHE_HOME"): - path = p + path = os.path.join(p, "rare", "cache") self.path = path self.manager = QNetworkAccessManager() From 254a8a48e7f9d5aee6304073678f5b03e29e4bd2 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 13 Jun 2021 00:05:14 +0200 Subject: [PATCH 13/30] Fix developer and title of some games --- rare/components/tabs/shop/shop_info.py | 1 - rare/components/tabs/shop/shop_models.py | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index c3bb6f11..140571b3 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -72,7 +72,6 @@ class ShopGameInfo(QWidget, Ui_shop_info): else: return self.game = ShopGame.from_json(game, self.data) - # print(game) self.title.setText(self.game.title) self.price.setText(self.game.price) diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index 23e367fb..7e313b57 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -44,6 +44,8 @@ class ShopGame: @classmethod def from_json(cls, api_data: dict, search_data: dict): + print(api_data) + print(search_data) if isinstance(api_data, list): for product in api_data: if product["_title"] == "home": @@ -53,7 +55,7 @@ class ShopGame: tmp = cls() if "pages" in api_data.keys(): api_data = api_data["pages"][0] - tmp.title = api_data.get("productName", api_data.get("_title", "fail")) + tmp.title = search_data.get("title", "Fail") tmp.image_urls = _ImageUrlModel.from_json(search_data["keyImages"]) links = api_data["data"]["socialLinks"] tmp.links = [] @@ -69,8 +71,12 @@ class ShopGame: tmp.reqs[system["systemType"]][req["title"]] = (req["minimum"], req["recommended"]) except KeyError: pass - tmp.publisher = api_data["data"]["meta"].get("publisher", "undefined") - tmp.developer = api_data["data"]["meta"].get("developer", "undefined") + 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'] From 0d0b858a8ff046eb56393681078067fc3af91ec5 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sun, 13 Jun 2021 14:33:17 +0200 Subject: [PATCH 14/30] Optimize search requests and option to cache images --- rare/components/tabs/settings/rare.py | 6 +- rare/components/tabs/shop/shop_models.py | 2 - rare/components/tabs/shop/shop_widget.py | 122 +++++++++++++---------- rare/ui/components/tabs/settings/rare.py | 21 ++-- rare/ui/components/tabs/settings/rare.ui | 43 +++++--- 5 files changed, 117 insertions(+), 77 deletions(-) diff --git a/rare/components/tabs/settings/rare.py b/rare/components/tabs/settings/rare.py index 2fafc3d9..bf9c511b 100644 --- a/rare/components/tabs/settings/rare.py +++ b/rare/components/tabs/settings/rare.py @@ -34,7 +34,8 @@ class RareSettings(QWidget, Ui_RareSettings): (self.confirm_start, "confirm_start", False), (self.auto_sync_cloud, "auto_sync_cloud", True), (self.notification, "notification", True), - (self.save_size, "save_size", False) + (self.save_size, "save_size", False), + (self.image_cache, "cache_images", True) ] self.settings = QSettings() @@ -99,6 +100,9 @@ class RareSettings(QWidget, Ui_RareSettings): self.save_size.stateChanged.connect( lambda: self.settings.setValue("save_size", self.save_size.isChecked()) ) + self.image_cache.stateChanged.connect( + lambda: self.settings.setValue("cache_images", self.image_cache.isChecked()) + ) if os.name == "posix": self.desktop_file = os.path.expanduser("~/Desktop/Rare.desktop") diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index 7e313b57..d029c27d 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -44,8 +44,6 @@ class ShopGame: @classmethod def from_json(cls, api_data: dict, search_data: dict): - print(api_data) - print(search_data) if isinstance(api_data, list): for product in api_data: if product["_title"] == "home": diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 8bd37eb6..ed85b219 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -4,10 +4,9 @@ import logging import os from json import JSONDecodeError -import requests from PyQt5 import QtGui from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QJsonDocument, QJsonParseError, \ - QStringListModel + QStringListModel, QSettings from PyQt5.QtGui import QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QCompleter, QGroupBox, QHBoxLayout, QScrollArea @@ -16,12 +15,16 @@ from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.utils.extra_widgets import WaitingSpinner, ImageLabel, FlowLayout from rare.utils.utils import get_lang +logger = logging.getLogger("Shop") + # noinspection PyAttributeOutsideInit,PyBroadException class ShopWidget(QScrollArea, Ui_ShopWidget): show_info = pyqtSignal(list) show_game = pyqtSignal(dict) free_game_widgets = [] + active_search_request = False + next_search = "" def __init__(self): super(ShopWidget, self).__init__() @@ -137,19 +140,24 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.free_game_request.deleteLater() def search_games(self, text, show_direct=False): - if text != "": - locale = get_lang() - payload = json.dumps({ - "query": query, - "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 20, - "country": "DE", "keywords": text, "locale": locale, "sortDir": "DESC", - "allowCountries": locale.upper(), - "start": 0, "tag": "", "withMapping": False, "withPrice": True} - }).encode() - request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self.search_request = self.manager.post(request, payload) - self.search_request.finished.connect(lambda: self.show_search_results(show_direct)) + if not self.active_search_request: + if text != "": + locale = get_lang() + payload = json.dumps({ + "query": query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 20, + "country": locale.upper(), "keywords": text, "locale": locale, "sortDir": "DESC", + "allowCountries": locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + }).encode() + request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self.search_request = self.manager.post(request, payload) + self.search_request.finished.connect(lambda: self.show_search_results(show_direct)) + + else: + self.next_search = text def show_search_results(self, show_direct=False): if self.search_request: @@ -158,27 +166,24 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) if QJsonParseError.NoError == error.error: data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"]["elements"] + self.data = data + if show_direct: + self.show_search_result(True) + return + titles = [i.get("title") for i in data] + model = QStringListModel() + model.setStringList(titles) + self.completer.setModel(model) + # self.completer.popup() + if self.search_request: + self.search_request.deleteLater() else: logging.error(error.errorString()) - return # response = .decode(encoding="utf-8") # print(response) # results = json.loads(response) - else: - return - else: - return - self.data = data - if show_direct: - self.show_search_result(True) - return - titles = [i.get("title") for i in data] - model = QStringListModel() - model.setStringList(titles) - self.completer.setModel(model) - # self.completer.popup() - if self.search_request: - self.search_request.deleteLater() + + self.search_games(self.next_search) def show_search_result(self, show_direct=False): if not show_direct: @@ -198,6 +203,7 @@ class GameWidget(QWidget): def __init__(self, path, json_info=None): super(GameWidget, self).__init__() + self.manager = QNetworkAccessManager() if json_info: self.init_ui(json_info, path) self.path = path @@ -207,35 +213,34 @@ class GameWidget(QWidget): self.layout = QVBoxLayout() self.image = ImageLabel() self.json_info = json_info + self.slug = json_info["productSlug"] + self.width = 300 + self.title = json_info["title"] for img in json_info["keyImages"]: if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: - width = 300 - self.image.update_image(img["url"], json_info["title"], (width, int(width * 9 / 16))) + 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: print("No image found") - self.slug = json_info["productSlug"] - self.title = json_info["title"] - if not os.path.exists(p := os.path.join(self.path, f"{json_info['title']}.png")): + save = QSettings().value("cache_images", True, bool) + if os.path.exists(p := os.path.join(self.path, f"{json_info['title']}_wide.png")) and save: + self.image.setPixmap(QPixmap(p) + .scaled(self.width, int(self.width * 9 / 16), transformMode=Qt.SmoothTransformation)) + else: for img in json_info["keyImages"]: - if json_info["title"] != "Mystery Game": - if img["type"] == "DieselStoreFrontWide": - with open(p, "wb") as img_file: - content = requests.get(img["url"]).content - img_file.write(content) - break - else: - if img["type"] == "VaultClosed": - with open(p, "wb") as img_file: - content = requests.get(img["url"]).content - img_file.write(content) - break + if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: + if img["type"] == "VaultClosed" and self.title != "Mystery Game": + continue + self.image_request = self.manager.get(QNetworkRequest(QUrl(img["url"]))) + self.image_request.finished.connect(lambda: self.image_ready(save)) + break else: - print("No image found") - width = 300 - self.image.setPixmap(QPixmap(os.path.join(self.path, f"{json_info['title']}.png")) - .scaled(width, int(width * 9 / 16), transformMode=Qt.SmoothTransformation)) + # No image found + logger.error(f"No image found for {self.title}") + self.layout.addWidget(self.image) self.title_label = QLabel(json_info["title"]) @@ -243,6 +248,19 @@ class GameWidget(QWidget): self.layout.addWidget(self.title_label) self.setLayout(self.layout) + def image_ready(self, save: bool): + if self.image_request: + if self.image_request.error() == QNetworkReply.NoError: + data = self.image_request.readAll().data() + if save: + with open(os.path.join(self.path, f"{self.title}_wide.png"), "wb") as file: + file.write(data) + file.close() + pixmap = QPixmap() + pixmap.loadFromData(data) + self.image.setPixmap(pixmap.scaled(self.width, int(self.width * 9 / 16), + transformMode=Qt.SmoothTransformation)) + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: self.show_info.emit(self.json_info) diff --git a/rare/ui/components/tabs/settings/rare.py b/rare/ui/components/tabs/settings/rare.py index 6e948018..599ccbdb 100644 --- a/rare/ui/components/tabs/settings/rare.py +++ b/rare/ui/components/tabs/settings/rare.py @@ -14,6 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_RareSettings(object): def setupUi(self, RareSettings): RareSettings.setObjectName("RareSettings") + RareSettings.resize(544, 532) self.rare_layout = QtWidgets.QGridLayout(RareSettings) self.rare_layout.setObjectName("rare_layout") self.rpc_layout = QtWidgets.QVBoxLayout() @@ -34,26 +35,29 @@ class Ui_RareSettings(object): self.settings_group.setObjectName("settings_group") self.behavior_layout = QtWidgets.QGridLayout(self.settings_group) self.behavior_layout.setObjectName("behavior_layout") + self.notification = QtWidgets.QCheckBox(self.settings_group) + self.notification.setObjectName("notification") + self.behavior_layout.addWidget(self.notification, 4, 0, 1, 1) + self.auto_update = QtWidgets.QCheckBox(self.settings_group) + self.auto_update.setObjectName("auto_update") + self.behavior_layout.addWidget(self.auto_update, 1, 0, 1, 1) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.behavior_layout.addItem(spacerItem, 2, 1, 2, 1) self.save_size = QtWidgets.QCheckBox(self.settings_group) self.save_size.setObjectName("save_size") self.behavior_layout.addWidget(self.save_size, 5, 0, 1, 1) - self.notification = QtWidgets.QCheckBox(self.settings_group) - self.notification.setObjectName("notification") - self.behavior_layout.addWidget(self.notification, 4, 0, 1, 1) self.auto_sync_cloud = QtWidgets.QCheckBox(self.settings_group) self.auto_sync_cloud.setObjectName("auto_sync_cloud") self.behavior_layout.addWidget(self.auto_sync_cloud, 3, 0, 1, 1) self.confirm_start = QtWidgets.QCheckBox(self.settings_group) self.confirm_start.setObjectName("confirm_start") self.behavior_layout.addWidget(self.confirm_start, 2, 0, 1, 1) - self.auto_update = QtWidgets.QCheckBox(self.settings_group) - self.auto_update.setObjectName("auto_update") - self.behavior_layout.addWidget(self.auto_update, 1, 0, 1, 1) self.sys_tray = QtWidgets.QCheckBox(self.settings_group) self.sys_tray.setObjectName("sys_tray") self.behavior_layout.addWidget(self.sys_tray, 0, 0, 1, 1) + self.image_cache = QtWidgets.QCheckBox(self.settings_group) + self.image_cache.setObjectName("image_cache") + self.behavior_layout.addWidget(self.image_cache, 6, 0, 1, 1) self.rare_layout.addWidget(self.settings_group, 2, 0, 1, 1, QtCore.Qt.AlignTop) self.log_dir_group = QtWidgets.QGroupBox(RareSettings) self.log_dir_group.setObjectName("log_dir_group") @@ -128,12 +132,13 @@ class Ui_RareSettings(object): self.desktop_link.setText(_translate("RareSettings", "Create Desktop link")) self.startmenu_link.setText(_translate("RareSettings", "Create start menu link")) self.settings_group.setTitle(_translate("RareSettings", "Behavior")) - self.save_size.setText(_translate("RareSettings", "Restore window size on application startup")) self.notification.setText(_translate("RareSettings", "Show notification on download completion")) + self.auto_update.setText(_translate("RareSettings", "Update games on application startup")) + self.save_size.setText(_translate("RareSettings", "Restore window size on application startup")) self.auto_sync_cloud.setText(_translate("RareSettings", "Automatically sync with cloud")) self.confirm_start.setText(_translate("RareSettings", "Confirm game launch")) - self.auto_update.setText(_translate("RareSettings", "Update games on application startup")) self.sys_tray.setText(_translate("RareSettings", "Exit to System tray")) + self.image_cache.setText(_translate("RareSettings", "Cache images in store")) self.log_dir_group.setTitle(_translate("RareSettings", "Logs")) self.log_dir_open_button.setText(_translate("RareSettings", "Open Log directory")) self.log_dir_clean_button.setText(_translate("RareSettings", "Clean Log directory")) diff --git a/rare/ui/components/tabs/settings/rare.ui b/rare/ui/components/tabs/settings/rare.ui index 377f874c..6403c5bb 100644 --- a/rare/ui/components/tabs/settings/rare.ui +++ b/rare/ui/components/tabs/settings/rare.ui @@ -2,6 +2,14 @@ RareSettings + + + 0 + 0 + 544 + 532 + + RareSettings @@ -38,6 +46,20 @@ Behavior + + + + Show notification on download completion + + + + + + + Update games on application startup + + + @@ -58,13 +80,6 @@ - - - - Show notification on download completion - - - @@ -79,13 +94,6 @@ - - - - Update games on application startup - - - @@ -93,6 +101,13 @@ + + + + Cache images in store + + + From c077e57a26b27befb80f36284b8694e3412e8b27 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sun, 13 Jun 2021 15:06:29 +0200 Subject: [PATCH 15/30] Read games from api --- rare/components/tabs/shop/shop_widget.py | 65 +++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index ed85b219..e0628b2d 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -63,8 +63,37 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): game_list = ["Satisfactory", "Among Us", "Star Wars Jedi Fallen Order", "Watch Dogs", "Subnautica Below Zero"] # TODO read from api - for game in game_list: - w = GameWidget.from_request(game, self.path) + locale = get_lang() + payload = json.dumps( + {"variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 15, "country": locale.upper(), "keywords": "", "locale": locale, + "sortDir": "DESC", "allowCountries": locale.upper(), "start": 0, "tag": "", + "withMapping": True, "withPrice": True + }, + "query": game_query}).encode() + + request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self.game_request = self.manager.post(request, payload) + self.game_request.finished.connect(self.show_recommended_games) + + def show_recommended_games(self): + print("lol") + if self.game_request: + if self.game_request.error() == QNetworkReply.NoError: + try: + games = json.loads(self.game_request.readAll().data().decode())["data"]["Catalog"]["searchStore"][ + "elements"] + print(games) + except JSONDecodeError: + return + else: + return + else: + return + + for game in games: + w = GameWidget(self.path, game) self.games_groupbox.layout().addWidget(w) w.show_info.connect(self.show_game.emit) @@ -333,3 +362,35 @@ query = "query searchStoreQuery($allowCountries: String, $category: String, $cou "endDate\n discountSetting {\n discountType\n discountPercentage\n " \ " }\n }\n }\n }\n }\n paging {\n count\n " \ "total\n }\n }\n }\n}\n " + +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 " From 0f6a4c5ae6a406add27dfd445f5f707fd82ab3e6 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Mon, 14 Jun 2021 20:22:39 +0200 Subject: [PATCH 16/30] Fix Cload save bug, if save_path is None --- rare/components/tabs/cloud_saves/sync_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rare/components/tabs/cloud_saves/sync_widget.py b/rare/components/tabs/cloud_saves/sync_widget.py index d7fb71e3..17edf12f 100644 --- a/rare/components/tabs/cloud_saves/sync_widget.py +++ b/rare/components/tabs/cloud_saves/sync_widget.py @@ -70,7 +70,7 @@ class SyncWidget(QGroupBox): self.game = self.core.get_game(igame.app_name) self.igame = igame self.has_save_path = True - if not igame.save_path: + if not igame.save_path or igame.save_path is None: try: save_path = self.core.get_save_path(igame.app_name) except Exception as e: From c063e9a92a00b198dc70a6e491dd220c37b99a8e Mon Sep 17 00:00:00 2001 From: Dummerle Date: Mon, 14 Jun 2021 22:30:57 +0200 Subject: [PATCH 17/30] Add browse games function --- rare/components/tab_widget.py | 2 +- rare/components/tabs/shop/__init__.py | 29 ++- rare/components/tabs/shop/browse_games.py | 147 +++++++++++ rare/components/tabs/shop/game_widgets.py | 150 +++++++++++ rare/components/tabs/shop/search_results.py | 2 +- rare/components/tabs/shop/shop_info.py | 17 +- rare/components/tabs/shop/shop_models.py | 5 +- rare/components/tabs/shop/shop_widget.py | 239 +++--------------- rare/ui/components/tabs/store/browse_games.py | 145 +++++++++++ rare/ui/components/tabs/store/browse_games.ui | 225 +++++++++++++++++ .../components/tabs/store/shop_game_info.py | 4 + .../components/tabs/store/shop_game_info.ui | 7 + 12 files changed, 753 insertions(+), 219 deletions(-) create mode 100644 rare/components/tabs/shop/browse_games.py create mode 100644 rare/components/tabs/shop/game_widgets.py create mode 100644 rare/ui/components/tabs/store/browse_games.py create mode 100644 rare/ui/components/tabs/store/browse_games.ui diff --git a/rare/components/tab_widget.py b/rare/components/tab_widget.py index 238e39f1..995868cb 100644 --- a/rare/components/tab_widget.py +++ b/rare/components/tab_widget.py @@ -33,7 +33,7 @@ class TabWidget(QTabWidget): self.addTab(self.downloadTab, "Downloads" + (" (" + str(len(updates)) + ")" if len(updates) != 0 else "")) self.cloud_saves = SyncSaves(core, self) self.addTab(self.cloud_saves, "Cloud Saves") - self.store = Shop() + self.store = Shop(self.core) self.addTab(self.store, self.tr("Store (Beta)")) self.settings = SettingsTab(core, self) diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index 64269786..64067c9d 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -1,35 +1,54 @@ -from PyQt5.QtWidgets import QStackedWidget +import os + +from PyQt5.QtWidgets import QStackedWidget, QTabWidget +from custom_legendary.core import LegendaryCore from rare.components.tabs.shop.search_results import SearchResults from rare.components.tabs.shop.shop_info import ShopGameInfo from rare.components.tabs.shop.shop_widget import ShopWidget +from rare.components.tabs.shop.browse_games import BrowseGames class Shop(QStackedWidget): init = False - def __init__(self): + def __init__(self, core: LegendaryCore): super(Shop, self).__init__() + self.core = core + if p := os.getenv("XDG_CACHE_HOME"): + self.path = p + else: + self.path = os.path.expanduser("~/.cache/rare/cache/") + if not os.path.exists(self.path): + os.makedirs(self.path) - self.shop = ShopWidget() - self.addWidget(self.shop) + self.shop = ShopWidget(self.path) + self.browse_games = BrowseGames(self.path) + + self.store_tabs = QTabWidget() + self.store_tabs.addTab(self.shop, self.tr("Games")) + self.store_tabs.addTab(self.browse_games, self.tr("Browse")) + + self.addWidget(self.store_tabs) self.search_results = SearchResults() self.addWidget(self.search_results) self.search_results.show_info.connect(self.show_game_info) - self.info = ShopGameInfo() + self.info = ShopGameInfo([i.asset_info.namespace for i in self.core.get_game_list(True)]) 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_info) self.shop.show_game.connect(self.show_game_info) + self.browse_games.show_game.connect(self.show_game_info) def load(self): if not self.init: self.init = True self.shop.load() + self.browse_games.prepare_request() def show_game_info(self, data): self.info.update_game(data) diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py new file mode 100644 index 00000000..e862f1bf --- /dev/null +++ b/rare/components/tabs/shop/browse_games.py @@ -0,0 +1,147 @@ +import datetime +import json +import logging +import random + +from PyQt5.QtCore import QUrl, pyqtSignal, QJsonParseError, QJsonDocument +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager +from PyQt5.QtWidgets import QWidget + +from rare.components.tabs.shop.game_widgets import GameWidget +from rare.ui.components.tabs.store.browse_games import Ui_browse_games +from rare.utils.extra_widgets import FlowLayout, WaitingSpinner +from rare.utils.utils import get_lang + +logger = logging.getLogger("BrowseGames") + + +class BrowseGames(QWidget, Ui_browse_games): + show_game = pyqtSignal(dict) + price = "" + platform = (False, False) + + def __init__(self, path): + super(BrowseGames, self).__init__() + self.setupUi(self) + self.path = path + self.games_widget = QWidget() + self.games_widget.setLayout(FlowLayout()) + self.games.setWidget(self.games_widget) + self.manager = QNetworkAccessManager() + + self.stack.addWidget(WaitingSpinner()) + + self.clear_price.toggled.connect(lambda: self.prepare_request("") if self.clear_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.win_cb.toggled.connect( + lambda: self.prepare_request(platform=(self.win_cb.isChecked(), self.mac_cb.isChecked()))) + self.mac_cb.toggled.connect( + lambda: self.prepare_request(platform=(self.win_cb.isChecked(), self.mac_cb.isChecked()))) + + def prepare_request(self, price: str = None, platform: tuple = None): + if price is not None: + self.price = price + if platform is not None: + self.platform = platform + + locale = get_lang() + self.stack.setCurrentIndex(2) + date = f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%X')}.{str(random.randint(0, 999)).zfill(3)}Z]" + payload = {"variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 30, "country": locale.upper(), "keywords": "", "locale": locale, + "sortDir": "DESC", "allowCountries": locale.upper(), "start": 0, "tag": "", + "withMapping": True, "withPrice": True, + "releaseDate": date, "effectiveDate": date + }, + "query": game_query} + + if self.price == "free": + payload["variables"]["freeGame"] = True + elif self.price.startswith(""): + payload["variables"]["priceRange"] = price.replace("", "") + elif self.price == "sale": + payload["variables"]["onSale"] = True + + if self.platform[0]: + payload["variables"]["tag"] = "9547" + if self.platform[1]: + payload["variables"]["tag"] += "|10719" + elif self.platform[1]: + payload["variables"]["tag"] = "10719" + + request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self.game_request = self.manager.post(request, json.dumps(payload).encode("utf-8")) + self.game_request.finished.connect(self.show_games) + + def show_games(self): + if self.game_request: + if self.game_request.error() == QNetworkReply.NoError: + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.game_request.readAll().data(), error) + + if error.error == error.NoError: + try: + games = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ + "elements"] + except TypeError as e: + logger.error("Type Error: " + str(e)) + self.stack.setCurrentIndex(1) + else: + QWidget().setLayout(self.games_widget.layout()) + self.games_widget.setLayout(FlowLayout()) + + for game in games: + w = GameWidget(self.path, game, 275) + self.games_widget.layout().addWidget(w) + w.show_info.connect(self.show_game.emit) + self.stack.setCurrentIndex(0) + return + + else: + logger.info(self.slug, error.errorString()) + else: + print(self.game_request.errorString()) + self.stack.setCurrentIndex(1) + + +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 " diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py new file mode 100644 index 00000000..865d0b4d --- /dev/null +++ b/rare/components/tabs/shop/game_widgets.py @@ -0,0 +1,150 @@ +import json +import logging +import os + +from PyQt5 import QtGui +from PyQt5.QtCore import pyqtSignal, QSettings, Qt, QUrl, QJsonParseError, QJsonDocument +from PyQt5.QtGui import QPixmap +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel + +from rare.utils.extra_widgets import ImageLabel +from rare.utils.utils import get_lang + + +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 + if json_info: + self.init_ui(json_info, path) + self.path = path + + def init_ui(self, json_info, path): + self.path = path + self.layout = QVBoxLayout() + self.image = ImageLabel() + 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"]: + 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: + print(json_info["keyImages"]) + + save = QSettings().value("cache_images", True, bool) + if os.path.exists(p := os.path.join(self.path, f"{json_info['title']}_wide.png")) and save: + self.image.setPixmap(QPixmap(p) + .scaled(self.width, int(self.width * 9 / 16), transformMode=Qt.SmoothTransformation)) + else: + for img in json_info["keyImages"]: + if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: + if img["type"] == "VaultClosed" and self.title != "Mystery Game": + continue + self.image_request = self.manager.get(QNetworkRequest(QUrl(img["url"]))) + self.image_request.finished.connect(lambda: self.image_ready(save)) + break + else: + # No image found + logging.error(f"No image found for {self.title}") + + self.layout.addWidget(self.image) + + self.title_label = QLabel(json_info["title"]) + self.title_label.setWordWrap(True) + self.layout.addWidget(self.title_label) + self.setLayout(self.layout) + + def image_ready(self, save: bool): + if self.image_request: + if self.image_request.error() == QNetworkReply.NoError: + data = self.image_request.readAll().data() + if save: + with open(os.path.join(self.path, f"{self.title}_wide.png"), "wb") as file: + file.write(data) + file.close() + pixmap = QPixmap() + pixmap.loadFromData(data) + self.image.setPixmap(pixmap.scaled(self.width, int(self.width * 9 / 16), + transformMode=Qt.SmoothTransformation)) + + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + self.show_info.emit(self.json_info) + + @classmethod + def from_request(cls, name, path): + c = cls(path) + c.manager = QNetworkAccessManager() + c.request = c.manager.get(QNetworkRequest()) + + locale = get_lang() + payload = json.dumps({ + "query": query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 1, + "country": "DE", "keywords": name, "locale": locale, "sortDir": "DESC", + "allowCountries": locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + }).encode() + request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + c.search_request = c.manager.post(request, payload) + c.search_request.finished.connect(lambda: c.handle_response(path)) + return c + + def handle_response(self, path): + if self.search_request: + if self.search_request.error() == QNetworkReply.NoError: + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) + if QJsonParseError.NoError == error.error: + data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ + "elements"][0] + self.init_ui(data, path) + else: + logging.error(error.errorString()) + return + + else: + return + else: + return + + +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 " diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index 87ea7f85..613de082 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -67,7 +67,7 @@ class _SearchResultItem(QGroupBox): price = result['price']['totalPrice']['fmtPrice']['originalPrice'] discount_price = result['price']['totalPrice']['fmtPrice']['discountPrice'] price_layout = QHBoxLayout() - price_label = QLabel(price) + price_label = QLabel(price if price != "0" else self.tr("Free")) price_layout.addWidget(price_label) if price != discount_price: diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index 140571b3..9c142d9f 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -20,9 +20,10 @@ class ShopGameInfo(QWidget, Ui_shop_info): data: dict # TODO Design - def __init__(self): + def __init__(self, installed_titles: list): super(ShopGameInfo, self).__init__() self.setupUi(self) + self.installed = installed_titles self.open_store_button.clicked.connect(self.button_clicked) self.image = ImageLabel() self.image_stack.addWidget(self.image) @@ -38,6 +39,12 @@ class ShopGameInfo(QWidget, Ui_shop_info): slug = slug.replace("/home", "") self.slug = slug self.title.setText(data["title"]) + 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) self.dev.setText(self.tr("Loading")) self.image.setPixmap(QPixmap()) @@ -73,8 +80,10 @@ class ShopGameInfo(QWidget, Ui_shop_info): return self.game = ShopGame.from_json(game, self.data) self.title.setText(self.game.title) - - self.price.setText(self.game.price) + if self.game.price != "0": + self.price.setText(self.game.price) + else: + self.price.setText(self.tr("Free")) if self.game.price != self.game.discount_price: self.discount_price.setText(self.game.discount_price) self.discount_price.setVisible(True) @@ -90,7 +99,7 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.req_group_box.layout().addWidget(min_label, 0, 1) self.req_group_box.layout().addWidget(rec_label, 0, 2) - for i, (key, value) in enumerate(self.game.reqs["Windows"].items()): + for i, (key, value) in enumerate(self.game.reqs.get("Windows", {}).items()): self.req_group_box.layout().addWidget(QLabel(key), i + 1, 0) min_label = QLabel(value[0]) min_label.setWordWrap(True) diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index d029c27d..d5eaf664 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -63,7 +63,10 @@ class ShopGame: tmp.available_voice_langs = api_data["data"]["requirements"].get("languages", "Failed") tmp.reqs = {} for i, system in enumerate(api_data["data"]["requirements"]["systems"]): - tmp.reqs[system["systemType"]] = {} + try: + tmp.reqs[system["systemType"]] = {} + except KeyError: + continue for req in system["details"]: try: tmp.reqs[system["systemType"]][req["title"]] = (req["minimum"], req["recommended"]) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index e0628b2d..80e84ce6 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -1,18 +1,16 @@ import datetime import json import logging -import os from json import JSONDecodeError -from PyQt5 import QtGui from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QJsonDocument, QJsonParseError, \ - QStringListModel, QSettings -from PyQt5.QtGui import QPixmap + QStringListModel from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QCompleter, QGroupBox, QHBoxLayout, QScrollArea +from PyQt5.QtWidgets import QWidget, QCompleter, QGroupBox, QHBoxLayout, QScrollArea +from rare.components.tabs.shop.game_widgets import GameWidget from rare.ui.components.tabs.store.store import Ui_ShopWidget -from rare.utils.extra_widgets import WaitingSpinner, ImageLabel, FlowLayout +from rare.utils.extra_widgets import WaitingSpinner, FlowLayout from rare.utils.utils import get_lang logger = logging.getLogger("Shop") @@ -26,10 +24,11 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): active_search_request = False next_search = "" - def __init__(self): + def __init__(self, path): super(ShopWidget, self).__init__() self.setWidgetResizable(True) self.setupUi(self) + self.path = path self.manager = QNetworkAccessManager() self.free_games_widget = QWidget() self.free_games_widget.setLayout(FlowLayout()) @@ -49,54 +48,13 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.data = [] self.games_groupbox.setLayout(FlowLayout()) + self.games_groupbox.setVisible(False) def load(self): - if p := os.getenv("XDG_CACHE_HOME"): - self.path = p - else: - self.path = os.path.expanduser("~/.cache/rare/cache/") - if not os.path.exists(self.path): - os.makedirs(self.path) url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" self.free_game_request = self.manager.get(QNetworkRequest(QUrl(url))) self.free_game_request.finished.connect(self.add_free_games) - game_list = ["Satisfactory", "Among Us", "Star Wars Jedi Fallen Order", "Watch Dogs", "Subnautica Below Zero"] - # TODO read from api - locale = get_lang() - payload = json.dumps( - {"variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 15, "country": locale.upper(), "keywords": "", "locale": locale, - "sortDir": "DESC", "allowCountries": locale.upper(), "start": 0, "tag": "", - "withMapping": True, "withPrice": True - }, - "query": game_query}).encode() - - request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self.game_request = self.manager.post(request, payload) - self.game_request.finished.connect(self.show_recommended_games) - - def show_recommended_games(self): - print("lol") - if self.game_request: - if self.game_request.error() == QNetworkReply.NoError: - try: - games = json.loads(self.game_request.readAll().data().decode())["data"]["Catalog"]["searchStore"][ - "elements"] - print(games) - except JSONDecodeError: - return - else: - return - else: - return - - for game in games: - w = GameWidget(self.path, game) - self.games_groupbox.layout().addWidget(w) - w.show_info.connect(self.show_game.emit) - def add_free_games(self): if self.free_game_request: if self.free_game_request.error() == QNetworkReply.NoError: @@ -190,27 +148,31 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): def show_search_results(self, show_direct=False): if self.search_request: - if self.search_request.error() == QNetworkReply.NoError: - error = QJsonParseError() - json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) - if QJsonParseError.NoError == error.error: - data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"]["elements"] - self.data = data - if show_direct: - self.show_search_result(True) - return - titles = [i.get("title") for i in data] - model = QStringListModel() - model.setStringList(titles) - self.completer.setModel(model) - # self.completer.popup() - if self.search_request: - self.search_request.deleteLater() - else: - logging.error(error.errorString()) - # response = .decode(encoding="utf-8") - # print(response) - # results = json.loads(response) + try: + if self.search_request.error() == QNetworkReply.NoError: + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) + if QJsonParseError.NoError == error.error: + data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ + "elements"] + self.data = data + if show_direct: + self.show_search_result(True) + return + titles = [i.get("title") for i in data] + model = QStringListModel() + model.setStringList(titles) + self.completer.setModel(model) + # self.completer.popup() + if self.search_request: + self.search_request.deleteLater() + else: + logging.error(error.errorString()) + # response = .decode(encoding="utf-8") + # print(response) + # results = json.loads(response) + except RuntimeError: + return self.search_games(self.next_search) @@ -227,111 +189,6 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.show_game.emit(result) -class GameWidget(QWidget): - show_info = pyqtSignal(dict) - - def __init__(self, path, json_info=None): - super(GameWidget, self).__init__() - self.manager = QNetworkAccessManager() - if json_info: - self.init_ui(json_info, path) - self.path = path - - def init_ui(self, json_info, path): - self.path = path - self.layout = QVBoxLayout() - self.image = ImageLabel() - self.json_info = json_info - self.slug = json_info["productSlug"] - self.width = 300 - self.title = json_info["title"] - for img in json_info["keyImages"]: - if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: - 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: - print("No image found") - - save = QSettings().value("cache_images", True, bool) - if os.path.exists(p := os.path.join(self.path, f"{json_info['title']}_wide.png")) and save: - self.image.setPixmap(QPixmap(p) - .scaled(self.width, int(self.width * 9 / 16), transformMode=Qt.SmoothTransformation)) - else: - for img in json_info["keyImages"]: - if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: - if img["type"] == "VaultClosed" and self.title != "Mystery Game": - continue - self.image_request = self.manager.get(QNetworkRequest(QUrl(img["url"]))) - self.image_request.finished.connect(lambda: self.image_ready(save)) - break - else: - # No image found - logger.error(f"No image found for {self.title}") - - self.layout.addWidget(self.image) - - self.title_label = QLabel(json_info["title"]) - self.title_label.setWordWrap(True) - self.layout.addWidget(self.title_label) - self.setLayout(self.layout) - - def image_ready(self, save: bool): - if self.image_request: - if self.image_request.error() == QNetworkReply.NoError: - data = self.image_request.readAll().data() - if save: - with open(os.path.join(self.path, f"{self.title}_wide.png"), "wb") as file: - file.write(data) - file.close() - pixmap = QPixmap() - pixmap.loadFromData(data) - self.image.setPixmap(pixmap.scaled(self.width, int(self.width * 9 / 16), - transformMode=Qt.SmoothTransformation)) - - def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - self.show_info.emit(self.json_info) - - @classmethod - def from_request(cls, name, path): - c = cls(path) - c.manager = QNetworkAccessManager() - c.request = c.manager.get(QNetworkRequest()) - - locale = get_lang() - payload = json.dumps({ - "query": query, - "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 1, - "country": "DE", "keywords": name, "locale": locale, "sortDir": "DESC", - "allowCountries": locale.upper(), - "start": 0, "tag": "", "withMapping": False, "withPrice": True} - }).encode() - request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - c.search_request = c.manager.post(request, payload) - c.search_request.finished.connect(lambda: c.handle_response(path)) - return c - - def handle_response(self, path): - if self.search_request: - if self.search_request.error() == QNetworkReply.NoError: - error = QJsonParseError() - json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) - if QJsonParseError.NoError == error.error: - data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ - "elements"][0] - self.init_ui(data, path) - else: - logging.error(error.errorString()) - return - - else: - return - else: - return - - 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 = " \ @@ -362,35 +219,3 @@ query = "query searchStoreQuery($allowCountries: String, $category: String, $cou "endDate\n discountSetting {\n discountType\n discountPercentage\n " \ " }\n }\n }\n }\n }\n paging {\n count\n " \ "total\n }\n }\n }\n}\n " - -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 " diff --git a/rare/ui/components/tabs/store/browse_games.py b/rare/ui/components/tabs/store/browse_games.py new file mode 100644 index 00000000..fba96450 --- /dev/null +++ b/rare/ui/components/tabs/store/browse_games.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'browse_games.ui' +# +# Created by: PyQt5 UI code generator 5.15.4 +# +# 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_browse_games(object): + def setupUi(self, browse_games): + browse_games.setObjectName("browse_games") + browse_games.resize(706, 541) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(browse_games) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.games_gb = QtWidgets.QGroupBox(browse_games) + self.games_gb.setObjectName("games_gb") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.games_gb) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.stack = QtWidgets.QStackedWidget(self.games_gb) + self.stack.setObjectName("stack") + self.games_page = QtWidgets.QWidget() + self.games_page.setObjectName("games_page") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.games_page) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.games = QtWidgets.QScrollArea(self.games_page) + self.games.setWidgetResizable(True) + self.games.setObjectName("games") + self.scrollAreaWidgetContents = QtWidgets.QWidget() + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 394, 306)) + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") + self.games.setWidget(self.scrollAreaWidgetContents) + self.verticalLayout_5.addWidget(self.games) + self.stack.addWidget(self.games_page) + self.error = QtWidgets.QWidget() + self.error.setObjectName("error") + self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.error) + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.error_label = QtWidgets.QLabel(self.error) + self.error_label.setObjectName("error_label") + self.verticalLayout_6.addWidget(self.error_label) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_6.addItem(spacerItem) + self.stack.addWidget(self.error) + self.verticalLayout_4.addWidget(self.stack) + self.horizontalLayout_2.addWidget(self.games_gb) + self.filter_gb = QtWidgets.QGroupBox(browse_games) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.filter_gb.sizePolicy().hasHeightForWidth()) + self.filter_gb.setSizePolicy(sizePolicy) + self.filter_gb.setMaximumSize(QtCore.QSize(200, 16777215)) + self.filter_gb.setFlat(False) + self.filter_gb.setCheckable(False) + self.filter_gb.setObjectName("filter_gb") + self.verticalLayout = QtWidgets.QVBoxLayout(self.filter_gb) + self.verticalLayout.setObjectName("verticalLayout") + self.filter_scroll = QtWidgets.QScrollArea(self.filter_gb) + self.filter_scroll.setWidgetResizable(True) + self.filter_scroll.setObjectName("filter_scroll") + self.scroll_widget = QtWidgets.QWidget() + self.scroll_widget.setGeometry(QtCore.QRect(0, 0, 174, 479)) + self.scroll_widget.setObjectName("scroll_widget") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scroll_widget) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.price_gb = QtWidgets.QGroupBox(self.scroll_widget) + self.price_gb.setObjectName("price_gb") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.price_gb) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.clear_price = QtWidgets.QRadioButton(self.price_gb) + self.clear_price.setChecked(True) + self.clear_price.setObjectName("clear_price") + self.verticalLayout_2.addWidget(self.clear_price) + self.free_button = QtWidgets.QRadioButton(self.price_gb) + self.free_button.setObjectName("free_button") + self.verticalLayout_2.addWidget(self.free_button) + self.under10 = QtWidgets.QRadioButton(self.price_gb) + self.under10.setObjectName("under10") + self.verticalLayout_2.addWidget(self.under10) + self.under20 = QtWidgets.QRadioButton(self.price_gb) + self.under20.setObjectName("under20") + self.verticalLayout_2.addWidget(self.under20) + self.under30 = QtWidgets.QRadioButton(self.price_gb) + self.under30.setObjectName("under30") + self.verticalLayout_2.addWidget(self.under30) + self.above = QtWidgets.QRadioButton(self.price_gb) + self.above.setObjectName("above") + self.verticalLayout_2.addWidget(self.above) + self.on_discount = QtWidgets.QRadioButton(self.price_gb) + self.on_discount.setObjectName("on_discount") + self.verticalLayout_2.addWidget(self.on_discount) + self.verticalLayout_3.addWidget(self.price_gb) + self.platform_gb = QtWidgets.QGroupBox(self.scroll_widget) + self.platform_gb.setObjectName("platform_gb") + self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.platform_gb) + self.verticalLayout_7.setObjectName("verticalLayout_7") + self.win_cb = QtWidgets.QCheckBox(self.platform_gb) + self.win_cb.setObjectName("win_cb") + self.verticalLayout_7.addWidget(self.win_cb) + self.mac_cb = QtWidgets.QCheckBox(self.platform_gb) + self.mac_cb.setObjectName("mac_cb") + self.verticalLayout_7.addWidget(self.mac_cb) + self.verticalLayout_3.addWidget(self.platform_gb) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_3.addItem(spacerItem1) + self.filter_scroll.setWidget(self.scroll_widget) + self.verticalLayout.addWidget(self.filter_scroll) + self.horizontalLayout_2.addWidget(self.filter_gb) + + self.retranslateUi(browse_games) + self.stack.setCurrentIndex(1) + QtCore.QMetaObject.connectSlotsByName(browse_games) + + def retranslateUi(self, browse_games): + _translate = QtCore.QCoreApplication.translate + browse_games.setWindowTitle(_translate("browse_games", "Form")) + self.games_gb.setTitle(_translate("browse_games", "Games")) + self.error_label.setText(_translate("browse_games", "An error occured")) + self.filter_gb.setTitle(_translate("browse_games", "Filter")) + self.price_gb.setTitle(_translate("browse_games", "Price")) + self.clear_price.setText(_translate("browse_games", "Clear price filter")) + self.free_button.setText(_translate("browse_games", "Free")) + self.under10.setText(_translate("browse_games", "Under 10")) + self.under20.setText(_translate("browse_games", "Under 20")) + self.under30.setText(_translate("browse_games", "Under 30")) + self.above.setText(_translate("browse_games", "14.99 and above")) + self.on_discount.setText(_translate("browse_games", "Discount")) + self.platform_gb.setTitle(_translate("browse_games", "Platform")) + self.win_cb.setText(_translate("browse_games", "Windows")) + self.mac_cb.setText(_translate("browse_games", "Mac")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + browse_games = QtWidgets.QWidget() + ui = Ui_browse_games() + ui.setupUi(browse_games) + browse_games.show() + sys.exit(app.exec_()) diff --git a/rare/ui/components/tabs/store/browse_games.ui b/rare/ui/components/tabs/store/browse_games.ui new file mode 100644 index 00000000..8845f141 --- /dev/null +++ b/rare/ui/components/tabs/store/browse_games.ui @@ -0,0 +1,225 @@ + + + browse_games + + + + 0 + 0 + 706 + 541 + + + + Form + + + + + + Games + + + + + + 1 + + + + + + + true + + + + + 0 + 0 + 394 + 306 + + + + + + + + + + + + + An error occured + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + 0 + 0 + + + + + 200 + 16777215 + + + + Filter + + + false + + + false + + + + + + true + + + + + 0 + 0 + 174 + 479 + + + + + + + Price + + + + + + Clear price filter + + + true + + + + + + + Free + + + + + + + Under 10 + + + + + + + Under 20 + + + + + + + Under 30 + + + + + + + 14.99 and above + + + + + + + Discount + + + + + + + + + + Platform + + + + + + Windows + + + + + + + Mac + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index f259a955..ba095bc8 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -35,6 +35,9 @@ class Ui_shop_info(object): 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.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) self.price.setObjectName("price") @@ -69,6 +72,7 @@ class Ui_shop_info(object): self.back_button.setText(_translate("shop_info", "Back")) self.title.setText(_translate("shop_info", "Error")) self.dev.setText(_translate("shop_info", "TextLabel")) + self.owned_label.setText(_translate("shop_info", "You already own this game")) self.price.setText(_translate("shop_info", "TextLabel")) self.discount_price.setText(_translate("shop_info", "TextLabel")) self.open_store_button.setText(_translate("shop_info", "Buy Game in Epic Games Store")) diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index 17ebd5a8..25e500c1 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -52,6 +52,13 @@ + + + + You already own this game + + + From 7cf4f15122ac59aa5868ed60ac9cd748ea04bbeb Mon Sep 17 00:00:00 2001 From: Dummerle Date: Mon, 14 Jun 2021 23:17:21 +0200 Subject: [PATCH 18/30] Fix bug if change filter too fast --- rare/components/tabs/shop/browse_games.py | 25 +- rare/ui/components/tabs/store/browse_games.py | 37 +- rare/ui/components/tabs/store/browse_games.ui | 316 ++++++++---------- 3 files changed, 174 insertions(+), 204 deletions(-) diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py index e862f1bf..ec727d7a 100644 --- a/rare/components/tabs/shop/browse_games.py +++ b/rare/components/tabs/shop/browse_games.py @@ -19,6 +19,8 @@ class BrowseGames(QWidget, Ui_browse_games): show_game = pyqtSignal(dict) price = "" platform = (False, False) + request_active = False + next_request = () def __init__(self, path): super(BrowseGames, self).__init__() @@ -48,11 +50,16 @@ class BrowseGames(QWidget, Ui_browse_games): lambda: self.prepare_request(platform=(self.win_cb.isChecked(), self.mac_cb.isChecked()))) def prepare_request(self, price: str = None, platform: tuple = None): + if price is not None: self.price = price if platform is not None: self.platform = platform + if self.request_active: + self.next_request = (self.price, self.platform) + return + locale = get_lang() self.stack.setCurrentIndex(2) date = f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%X')}.{str(random.randint(0, 999)).zfill(3)}Z]" @@ -80,10 +87,12 @@ class BrowseGames(QWidget, Ui_browse_games): request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self.request_active = True self.game_request = self.manager.post(request, json.dumps(payload).encode("utf-8")) self.game_request.finished.connect(self.show_games) def show_games(self): + if self.game_request: if self.game_request.error() == QNetworkReply.NoError: error = QJsonParseError() @@ -105,13 +114,23 @@ class BrowseGames(QWidget, Ui_browse_games): self.games_widget.layout().addWidget(w) w.show_info.connect(self.show_game.emit) self.stack.setCurrentIndex(0) + + self.request_active = False + if self.next_request: + self.prepare_request(*self.next_request) + self.next_request = () + return else: - logger.info(self.slug, error.errorString()) + logger.error(error.errorString()) else: - print(self.game_request.errorString()) - self.stack.setCurrentIndex(1) + logger.error(self.game_request.errorString()) + if self.next_request: + self.prepare_request(*self.next_request) + self.next_request = () + else: + self.stack.setCurrentIndex(1) game_query = "query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, " \ diff --git a/rare/ui/components/tabs/store/browse_games.py b/rare/ui/components/tabs/store/browse_games.py index fba96450..e5729c0f 100644 --- a/rare/ui/components/tabs/store/browse_games.py +++ b/rare/ui/components/tabs/store/browse_games.py @@ -8,7 +8,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtWidgets class Ui_browse_games(object): @@ -17,11 +17,7 @@ class Ui_browse_games(object): browse_games.resize(706, 541) self.horizontalLayout_2 = QtWidgets.QHBoxLayout(browse_games) self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.games_gb = QtWidgets.QGroupBox(browse_games) - self.games_gb.setObjectName("games_gb") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.games_gb) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.stack = QtWidgets.QStackedWidget(self.games_gb) + self.stack = QtWidgets.QStackedWidget(browse_games) self.stack.setObjectName("stack") self.games_page = QtWidgets.QWidget() self.games_page.setObjectName("games_page") @@ -31,7 +27,7 @@ class Ui_browse_games(object): self.games.setWidgetResizable(True) self.games.setObjectName("games") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 394, 306)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 462, 503)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.games.setWidget(self.scrollAreaWidgetContents) self.verticalLayout_5.addWidget(self.games) @@ -46,25 +42,13 @@ class Ui_browse_games(object): spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_6.addItem(spacerItem) self.stack.addWidget(self.error) - self.verticalLayout_4.addWidget(self.stack) - self.horizontalLayout_2.addWidget(self.games_gb) - self.filter_gb = QtWidgets.QGroupBox(browse_games) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.filter_gb.sizePolicy().hasHeightForWidth()) - self.filter_gb.setSizePolicy(sizePolicy) - self.filter_gb.setMaximumSize(QtCore.QSize(200, 16777215)) - self.filter_gb.setFlat(False) - self.filter_gb.setCheckable(False) - self.filter_gb.setObjectName("filter_gb") - self.verticalLayout = QtWidgets.QVBoxLayout(self.filter_gb) - self.verticalLayout.setObjectName("verticalLayout") - self.filter_scroll = QtWidgets.QScrollArea(self.filter_gb) + self.horizontalLayout_2.addWidget(self.stack) + self.filter_scroll = QtWidgets.QScrollArea(browse_games) + self.filter_scroll.setMaximumSize(QtCore.QSize(200, 16777215)) self.filter_scroll.setWidgetResizable(True) self.filter_scroll.setObjectName("filter_scroll") self.scroll_widget = QtWidgets.QWidget() - self.scroll_widget.setGeometry(QtCore.QRect(0, 0, 174, 479)) + self.scroll_widget.setGeometry(QtCore.QRect(0, 0, 198, 521)) self.scroll_widget.setObjectName("scroll_widget") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scroll_widget) self.verticalLayout_3.setObjectName("verticalLayout_3") @@ -109,19 +93,16 @@ class Ui_browse_games(object): spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_3.addItem(spacerItem1) self.filter_scroll.setWidget(self.scroll_widget) - self.verticalLayout.addWidget(self.filter_scroll) - self.horizontalLayout_2.addWidget(self.filter_gb) + self.horizontalLayout_2.addWidget(self.filter_scroll) self.retranslateUi(browse_games) - self.stack.setCurrentIndex(1) + self.stack.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(browse_games) def retranslateUi(self, browse_games): _translate = QtCore.QCoreApplication.translate browse_games.setWindowTitle(_translate("browse_games", "Form")) - self.games_gb.setTitle(_translate("browse_games", "Games")) self.error_label.setText(_translate("browse_games", "An error occured")) - self.filter_gb.setTitle(_translate("browse_games", "Filter")) self.price_gb.setTitle(_translate("browse_games", "Price")) self.clear_price.setText(_translate("browse_games", "Clear price filter")) self.free_button.setText(_translate("browse_games", "Free")) diff --git a/rare/ui/components/tabs/store/browse_games.ui b/rare/ui/components/tabs/store/browse_games.ui index 8845f141..9f0ce58b 100644 --- a/rare/ui/components/tabs/store/browse_games.ui +++ b/rare/ui/components/tabs/store/browse_games.ui @@ -15,207 +15,177 @@ - - - Games + + + 0 - - - - - 1 - - - - - - - true - - - - - 0 - 0 - 394 - 306 - - - - - - + + + + + + true + + + + + 0 + 0 + 462 + 503 + + + - - - - - - An error occured - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + + + + + + + An error occured + - - - + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - - - - 0 - 0 - - + 200 16777215 - - Filter + + true - - false - - - false - - - - - - true - - - - - 0 - 0 - 174 - 479 - + + + + 0 + 0 + 198 + 521 + + + + + + + Price - + - - - Price + + + Clear price filter + + + true - - - - - Clear price filter - - - true - - - - - - - Free - - - - - - - Under 10 - - - - - - - Under 20 - - - - - - - Under 30 - - - - - - - 14.99 and above - - - - - - - Discount - - - - - - - Platform + + + Free - - - - - Windows - - - - - - - Mac - - - - - - - Qt::Vertical + + + Under 10 - - - 20 - 40 - + + + + + + Under 20 - + + + + + + Under 30 + + + + + + + 14.99 and above + + + + + + + Discount + + - - - + + + + + Platform + + + + + + Windows + + + + + + + Mac + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + From c0f45778700fac5b65a0b5c350179fb3c9c0d808 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 16 Jun 2021 19:03:18 +0200 Subject: [PATCH 19/30] Add tags in shop_info.py --- rare/components/tabs/shop/game_widgets.py | 7 +++++-- rare/components/tabs/shop/shop_info.py | 2 +- rare/components/tabs/shop/shop_models.py | 4 +++- rare/ui/components/tabs/store/shop_game_info.py | 8 ++++++-- rare/ui/components/tabs/store/shop_game_info.ui | 7 +++++++ 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py index 865d0b4d..27151d0d 100644 --- a/rare/components/tabs/shop/game_widgets.py +++ b/rare/components/tabs/shop/game_widgets.py @@ -11,6 +11,8 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel from rare.utils.extra_widgets import ImageLabel from rare.utils.utils import get_lang +logger = logging.getLogger("GameWidgets") + class GameWidget(QWidget): show_info = pyqtSignal(dict) @@ -38,7 +40,8 @@ class GameWidget(QWidget): self.image.update_image(img["url"], json_info["title"], (self.width, int(self.width * 9 / 16))) break else: - print(json_info["keyImages"]) + logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) + # print(json_info["keyImages"]) save = QSettings().value("cache_images", True, bool) if os.path.exists(p := os.path.join(self.path, f"{json_info['title']}_wide.png")) and save: @@ -54,7 +57,7 @@ class GameWidget(QWidget): break else: # No image found - logging.error(f"No image found for {self.title}") + logger.error(f"No image found for {self.title}") self.layout.addWidget(self.image) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index 9c142d9f..79c51e49 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -121,7 +121,7 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.dev.setText(self.game.developer) except KeyError: pass - + self.tags.setText(", ".join(self.game.tags)) # self.price.setText(self.game.price) self.request.deleteLater() diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index d5eaf664..0152d242 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -25,7 +25,7 @@ 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 = ""): + original_price: str = "", discount_price: str = "", tags: list = None): self.title = title self.image_urls = image_urls self.links = [] @@ -41,6 +41,7 @@ class ShopGame: self.developer = developer self.price = original_price self.discount_price = discount_price + self.tags = tags @classmethod def from_json(cls, api_data: dict, search_data: dict): @@ -80,5 +81,6 @@ class ShopGame: 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", [])] return tmp diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index ba095bc8..61c99695 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -8,7 +8,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtWidgets class Ui_shop_info(object): @@ -39,12 +39,15 @@ class Ui_shop_info(object): self.owned_label.setObjectName("owned_label") self.verticalLayout_2.addWidget(self.owned_label) self.price = QtWidgets.QLabel(shop_info) - self.price.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + 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.setObjectName("discount_price") self.verticalLayout_2.addWidget(self.discount_price) + self.tags = QtWidgets.QLabel(shop_info) + self.tags.setObjectName("tags") + self.verticalLayout_2.addWidget(self.tags) self.open_store_button = QtWidgets.QPushButton(shop_info) self.open_store_button.setObjectName("open_store_button") self.verticalLayout_2.addWidget(self.open_store_button) @@ -75,6 +78,7 @@ class Ui_shop_info(object): self.owned_label.setText(_translate("shop_info", "You already own this game")) self.price.setText(_translate("shop_info", "TextLabel")) self.discount_price.setText(_translate("shop_info", "TextLabel")) + self.tags.setText(_translate("shop_info", "TextLabel")) self.open_store_button.setText(_translate("shop_info", "Buy Game in Epic Games Store")) self.req_group_box.setTitle(_translate("shop_info", "Requirements")) diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index 25e500c1..a928b083 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -76,6 +76,13 @@ + + + + TextLabel + + + From e7038be33a7546eac9e9fcd7200f8db645af939f Mon Sep 17 00:00:00 2001 From: Dummerle Date: Thu, 17 Jun 2021 00:34:23 +0200 Subject: [PATCH 20/30] Better Code to browse games, use PIL images to save smaller --- rare/components/tabs/shop/browse_games.py | 126 ++++++++++-------- rare/components/tabs/shop/constants.py | 74 ++++++++++ rare/components/tabs/shop/game_widgets.py | 29 ++-- rare/components/tabs/shop/shop_info.py | 23 ++-- rare/components/tabs/shop/shop_models.py | 3 +- rare/components/tabs/shop/thread.py | 57 ++++++++ rare/ui/components/tabs/store/browse_games.py | 26 ++-- rare/ui/components/tabs/store/browse_games.ui | 63 +++++---- rare/utils/extra_widgets.py | 18 ++- 9 files changed, 299 insertions(+), 120 deletions(-) create mode 100644 rare/components/tabs/shop/constants.py create mode 100644 rare/components/tabs/shop/thread.py diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py index ec727d7a..a6e8f4bb 100644 --- a/rare/components/tabs/shop/browse_games.py +++ b/rare/components/tabs/shop/browse_games.py @@ -5,8 +5,9 @@ import random from PyQt5.QtCore import QUrl, pyqtSignal, QJsonParseError, QJsonDocument from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager -from PyQt5.QtWidgets import QWidget +from PyQt5.QtWidgets import QWidget, QCheckBox, QVBoxLayout, QLabel +from rare.components.tabs.shop.constants import game_query, Constants from rare.components.tabs.shop.game_widgets import GameWidget from rare.ui.components.tabs.store.browse_games import Ui_browse_games from rare.utils.extra_widgets import FlowLayout, WaitingSpinner @@ -18,9 +19,10 @@ logger = logging.getLogger("BrowseGames") class BrowseGames(QWidget, Ui_browse_games): show_game = pyqtSignal(dict) price = "" - platform = (False, False) + tags = [] + types = [] request_active = False - next_request = () + next_request = False def __init__(self, path): super(BrowseGames, self).__init__() @@ -44,20 +46,41 @@ class BrowseGames(QWidget, Ui_browse_games): 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.win_cb.toggled.connect( - lambda: self.prepare_request(platform=(self.win_cb.isChecked(), self.mac_cb.isChecked()))) - self.mac_cb.toggled.connect( - lambda: self.prepare_request(platform=(self.win_cb.isChecked(), self.mac_cb.isChecked()))) + constants = Constants() - def prepare_request(self, price: str = None, platform: tuple = None): + for groupbox, variables in [(self.genre_gb, constants.categories), + (self.platform_gb, constants.platforms), + (self.others_gb, constants.others)]: + + 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) + + for text, tag in constants.types: + checkbox = CheckBox(text, tag) + checkbox.activated.connect(lambda x: self.prepare_request(added_type=x)) + checkbox.deactivated.connect(lambda x: self.prepare_request(removed_type=x)) + self.type_gb.layout().addWidget(checkbox) + + def prepare_request(self, price: str = None, added_tag: int = 0, removed_tag: int = 0, + added_type: str = "", removed_type: str = ""): if price is not None: self.price = price - if platform is not None: - self.platform = platform + 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.request_active: - self.next_request = (self.price, self.platform) + self.next_request = True return locale = get_lang() @@ -78,12 +101,10 @@ class BrowseGames(QWidget, Ui_browse_games): elif self.price == "sale": payload["variables"]["onSale"] = True - if self.platform[0]: - payload["variables"]["tag"] = "9547" - if self.platform[1]: - payload["variables"]["tag"] += "|10719" - elif self.platform[1]: - payload["variables"]["tag"] = "10719" + payload["variables"]["tag"] = "|".join(self.tags) + + if self.types: + payload["variables"]["category"] = "|".join(self.types) request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") @@ -92,7 +113,6 @@ class BrowseGames(QWidget, Ui_browse_games): self.game_request.finished.connect(self.show_games) def show_games(self): - if self.game_request: if self.game_request.error() == QNetworkReply.NoError: error = QJsonParseError() @@ -107,17 +127,23 @@ class BrowseGames(QWidget, Ui_browse_games): self.stack.setCurrentIndex(1) else: QWidget().setLayout(self.games_widget.layout()) - self.games_widget.setLayout(FlowLayout()) + if games: + self.games_widget.setLayout(FlowLayout()) - for game in games: - w = GameWidget(self.path, game, 275) - self.games_widget.layout().addWidget(w) - w.show_info.connect(self.show_game.emit) + for game in games: + w = GameWidget(self.path, game, 275) + self.games_widget.layout().addWidget(w) + w.show_info.connect(self.show_game.emit) + + else: + self.games_widget.setLayout(QVBoxLayout()) + self.games_widget.layout().addWidget( + QLabel(self.tr("Could not get games matching the filter"))) + self.games_widget.layout().addStretch(1) self.stack.setCurrentIndex(0) - self.request_active = False if self.next_request: - self.prepare_request(*self.next_request) + self.prepare_request() self.next_request = () return @@ -127,40 +153,24 @@ class BrowseGames(QWidget, Ui_browse_games): else: logger.error(self.game_request.errorString()) if self.next_request: - self.prepare_request(*self.next_request) - self.next_request = () + self.prepare_request() + self.next_request = False else: self.stack.setCurrentIndex(1) -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 " +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/shop/constants.py b/rare/components/tabs/shop/constants.py new file mode 100644 index 00000000..d604ef33 --- /dev/null +++ b/rare/components/tabs/shop/constants.py @@ -0,0 +1,74 @@ +from PyQt5.QtCore import QObject + + +# Class to use QObject.tr() +class Constants(QObject): + def __init__(self): + super(Constants, self).__init__() + self.categories = sorted([ + (self.tr("Action"), "1216"), + (self.tr("Adventure"), "1117"), + (self.tr("Puzzle"), "1298"), + (self.tr("Open world"), "1307"), + (self.tr("Racing"), "1212"), + (self.tr("RPG"), "1367"), + (self.tr("Shooter"), "1210"), + (self.tr("Strategy"), "1115"), + (self.tr("Survival"), "1080"), + (self.tr("First Person"), "1294"), + (self.tr("Indie"), "1263"), + (self.tr("Simulation"), "1393"), + (self.tr("Sport"), "1283") + ], key=lambda x: x[0]) + + self.platforms = [ + ("MacOS", "9548"), + ("Windows", "9547"), + ] + self.others = [ + (self.tr("Single player"), "1370"), + (self.tr("Multiplayer"), "1203"), + (self.tr("Controller"), "9549"), + (self.tr("Co-op"), "1264"), + ] + + self.types = [ + (self.tr("Editor"), "editors"), + (self.tr("Game"), "games/edition/base"), + (self.tr("Bundle"), "bundles/games"), + (self.tr("Add-on"), "addons"), + (self.tr("Apps"), "software/edition/base") + ] + + +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 " diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py index 27151d0d..cea02ab7 100644 --- a/rare/components/tabs/shop/game_widgets.py +++ b/rare/components/tabs/shop/game_widgets.py @@ -1,7 +1,10 @@ +import io import json import logging import os +from PIL import Image +from PIL.ImageQt import ImageQt from PyQt5 import QtGui from PyQt5.QtCore import pyqtSignal, QSettings, Qt, QUrl, QJsonParseError, QJsonDocument from PyQt5.QtGui import QPixmap @@ -29,6 +32,15 @@ class GameWidget(QWidget): self.path = path self.layout = QVBoxLayout() self.image = ImageLabel() + self.layout.addWidget(self.image) + + self.title_label = QLabel(json_info["title"]) + self.title_label.setWordWrap(True) + self.layout.addWidget(self.title_label) + + for c in r'<>?":|\/*': + json_info["title"] = json_info["title"].replace(c, "") + self.json_info = json_info self.slug = json_info["productSlug"] @@ -59,25 +71,20 @@ class GameWidget(QWidget): # No image found logger.error(f"No image found for {self.title}") - self.layout.addWidget(self.image) - - self.title_label = QLabel(json_info["title"]) - self.title_label.setWordWrap(True) - self.layout.addWidget(self.title_label) self.setLayout(self.layout) def image_ready(self, save: bool): if self.image_request: if self.image_request.error() == QNetworkReply.NoError: data = self.image_request.readAll().data() + image: Image.Image = Image.open(io.BytesIO(data)) + image = image.resize((self.width, int(self.width * 9 / 16))) + if save: - with open(os.path.join(self.path, f"{self.title}_wide.png"), "wb") as file: - file.write(data) - file.close() + image.save(os.path.join(self.path, self.json_info["title"] + "_wide.png"), format="png") pixmap = QPixmap() - pixmap.loadFromData(data) - self.image.setPixmap(pixmap.scaled(self.width, int(self.width * 9 / 16), - transformMode=Qt.SmoothTransformation)) + pixmap.fromImage(ImageQt(image)) + self.image.setPixmap(pixmap) def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: self.show_info.emit(self.json_info) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index 79c51e49..84b4b57e 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -96,17 +96,20 @@ class ShopGameInfo(QWidget, Ui_shop_info): min_label.setFont(bold_font) rec_label = QLabel(self.tr("Recommend")) rec_label.setFont(bold_font) - self.req_group_box.layout().addWidget(min_label, 0, 1) - self.req_group_box.layout().addWidget(rec_label, 0, 2) - for i, (key, value) in enumerate(self.game.reqs.get("Windows", {}).items()): - self.req_group_box.layout().addWidget(QLabel(key), i + 1, 0) - min_label = QLabel(value[0]) - min_label.setWordWrap(True) - self.req_group_box.layout().addWidget(min_label, i + 1, 1) - rec_label = QLabel(value[1]) - rec_label.setWordWrap(True) - self.req_group_box.layout().addWidget(rec_label, i + 1, 2) + if self.game.reqs: + self.req_group_box.layout().addWidget(min_label, 0, 1) + self.req_group_box.layout().addWidget(rec_label, 0, 2) + for i, (key, value) in enumerate(self.game.reqs.get("Windows", {}).items()): + self.req_group_box.layout().addWidget(QLabel(key), i + 1, 0) + min_label = QLabel(value[0]) + min_label.setWordWrap(True) + self.req_group_box.layout().addWidget(min_label, i + 1, 1) + rec_label = QLabel(value[1]) + rec_label.setWordWrap(True) + self.req_group_box.layout().addWidget(rec_label, i + 1, 2) + else: + self.req_group_box.layout().addWidget(QLabel(self.tr("Could not get requirements"))) self.image.update_image(self.game.image_urls.front_tall, self.game.title, (240, 320)) diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index 0152d242..3a4ca515 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -63,7 +63,8 @@ class ShopGame: 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"]["systems"]): + print(api_data["data"]["requirements"]) + for i, system in enumerate(api_data["data"]["requirements"].get("systems", [])): try: tmp.reqs[system["systemType"]] = {} except KeyError: diff --git a/rare/components/tabs/shop/thread.py b/rare/components/tabs/shop/thread.py new file mode 100644 index 00000000..3139c666 --- /dev/null +++ b/rare/components/tabs/shop/thread.py @@ -0,0 +1,57 @@ +import requests +from PyQt5.QtCore import QThread, pyqtSignal + +from custom_legendary.core import LegendaryCore +from rare.components.tabs.shop.browse_games import game_query +from rare.components.tabs.shop.shop_models import ShopGame +from rare.utils.utils import get_lang + + +class AnalyzeThread(QThread): + get_tags = pyqtSignal(dict) + + def __init__(self, core: LegendaryCore): + super(AnalyzeThread, self).__init__() + self.tags = {} + self.core = core + + def run(self) -> None: + locale = get_lang() + for igame in self.core.get_installed_list(): + data = { + "query": game_query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 20, + "country": locale.upper(), "keywords": igame.title, "locale": locale, "sortDir": "DESC", + "allowCountries": locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + } + + try: + search_game = \ + requests.post("https://www.epicgames.com/graphql", json=data).json()["data"]["Catalog"]["searchStore"][ + "elements"][0] + except: + continue + slug = search_game["productSlug"] + is_bundle = False + for i in search_game["categories"]: + if "bundles" in i.get("path", ""): + is_bundle = True + + api_game = requests.get( + f"https://store-content.ak.epicgames.com/api/{locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}").json() + + if api_game.get("error"): + print(igame.title) + continue + + game = ShopGame.from_json(api_game, search_game) + + for i in game.tags: + if i not in self.tags.keys(): + self.tags[i] = 1 + else: + self.tags[i] += 1 + + print(self.tags) diff --git a/rare/ui/components/tabs/store/browse_games.py b/rare/ui/components/tabs/store/browse_games.py index e5729c0f..af8dddbe 100644 --- a/rare/ui/components/tabs/store/browse_games.py +++ b/rare/ui/components/tabs/store/browse_games.py @@ -79,17 +79,26 @@ class Ui_browse_games(object): self.on_discount.setObjectName("on_discount") self.verticalLayout_2.addWidget(self.on_discount) self.verticalLayout_3.addWidget(self.price_gb) + self.genre_gb = QtWidgets.QGroupBox(self.scroll_widget) + self.genre_gb.setObjectName("genre_gb") + self.verticalLayout = QtWidgets.QVBoxLayout(self.genre_gb) + self.verticalLayout.setObjectName("verticalLayout") + self.verticalLayout_3.addWidget(self.genre_gb) + self.type_gb = QtWidgets.QGroupBox(self.scroll_widget) + self.type_gb.setObjectName("type_gb") + self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.type_gb) + self.verticalLayout_8.setObjectName("verticalLayout_8") + self.verticalLayout_3.addWidget(self.type_gb) self.platform_gb = QtWidgets.QGroupBox(self.scroll_widget) self.platform_gb.setObjectName("platform_gb") self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.platform_gb) self.verticalLayout_7.setObjectName("verticalLayout_7") - self.win_cb = QtWidgets.QCheckBox(self.platform_gb) - self.win_cb.setObjectName("win_cb") - self.verticalLayout_7.addWidget(self.win_cb) - self.mac_cb = QtWidgets.QCheckBox(self.platform_gb) - self.mac_cb.setObjectName("mac_cb") - self.verticalLayout_7.addWidget(self.mac_cb) self.verticalLayout_3.addWidget(self.platform_gb) + self.others_gb = QtWidgets.QGroupBox(self.scroll_widget) + self.others_gb.setObjectName("others_gb") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.others_gb) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.verticalLayout_3.addWidget(self.others_gb) spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_3.addItem(spacerItem1) self.filter_scroll.setWidget(self.scroll_widget) @@ -111,9 +120,10 @@ class Ui_browse_games(object): self.under30.setText(_translate("browse_games", "Under 30")) self.above.setText(_translate("browse_games", "14.99 and above")) self.on_discount.setText(_translate("browse_games", "Discount")) + self.genre_gb.setTitle(_translate("browse_games", "Genre")) + self.type_gb.setTitle(_translate("browse_games", "Type")) self.platform_gb.setTitle(_translate("browse_games", "Platform")) - self.win_cb.setText(_translate("browse_games", "Windows")) - self.mac_cb.setText(_translate("browse_games", "Mac")) + self.others_gb.setTitle(_translate("browse_games", "Other Tags")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/store/browse_games.ui b/rare/ui/components/tabs/store/browse_games.ui index 9f0ce58b..b4516ccc 100644 --- a/rare/ui/components/tabs/store/browse_games.ui +++ b/rare/ui/components/tabs/store/browse_games.ui @@ -148,37 +148,46 @@ - - - - Platform - - - - - Windows - - + + + Genre + + + - - - Mac - - + + + Type + + + - - - - - - - Qt::Vertical - - - - 20 + + + + Platform + + + + + + + + Other Tags + + + + + + + + Qt::Vertical + + + + 20 40 diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index fa2abde5..9eaf1ae2 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -1,6 +1,9 @@ +import io import os -from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QUrl +from PIL import Image +from PIL.ImageQt import ImageQt +from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QUrl, QSettings from PyQt5.QtGui import QMovie, QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, QHBoxLayout, QWidget, QPushButton, \ @@ -284,10 +287,15 @@ class ImageLabel(QLabel): if self.request: if self.request.error() == QNetworkReply.NoError: - with open(os.path.join(self.path, self.name), "wb") as file: - file.write(self.request.readAll().data()) - file.close() - self.show_image() + data = self.request.readAll().data() + image: Image.Image = Image.open(io.BytesIO(data)) + image = image.resize((self.img_size[0], self.img_size[1])) + + if QSettings().value("cache_images", True, bool): + image.save(os.path.join(self.path, self.name), format="png") + pixmap = QPixmap() + pixmap.fromImage(ImageQt(image)) + self.setPixmap(pixmap) else: return From 374e8193ac779666e2f868a06ed8aa7a7992cbe2 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Fri, 18 Jun 2021 00:17:01 +0200 Subject: [PATCH 21/30] Fix weird bug with images in ImageLabel --- rare/components/tabs/shop/game_widgets.py | 37 ++--------------------- rare/components/tabs/shop/shop_models.py | 1 - rare/utils/extra_widgets.py | 13 ++++++-- 3 files changed, 12 insertions(+), 39 deletions(-) diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py index cea02ab7..e9919cbd 100644 --- a/rare/components/tabs/shop/game_widgets.py +++ b/rare/components/tabs/shop/game_widgets.py @@ -1,13 +1,9 @@ -import io +import json import json import logging -import os -from PIL import Image -from PIL.ImageQt import ImageQt from PyQt5 import QtGui -from PyQt5.QtCore import pyqtSignal, QSettings, Qt, QUrl, QJsonParseError, QJsonDocument -from PyQt5.QtGui import QPixmap +from PyQt5.QtCore import pyqtSignal, QUrl, QJsonParseError, QJsonDocument from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel @@ -55,37 +51,8 @@ class GameWidget(QWidget): logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) # print(json_info["keyImages"]) - save = QSettings().value("cache_images", True, bool) - if os.path.exists(p := os.path.join(self.path, f"{json_info['title']}_wide.png")) and save: - self.image.setPixmap(QPixmap(p) - .scaled(self.width, int(self.width * 9 / 16), transformMode=Qt.SmoothTransformation)) - else: - for img in json_info["keyImages"]: - if img["type"] in ["DieselStoreFrontWide", "VaultClosed"]: - if img["type"] == "VaultClosed" and self.title != "Mystery Game": - continue - self.image_request = self.manager.get(QNetworkRequest(QUrl(img["url"]))) - self.image_request.finished.connect(lambda: self.image_ready(save)) - break - else: - # No image found - logger.error(f"No image found for {self.title}") - self.setLayout(self.layout) - def image_ready(self, save: bool): - if self.image_request: - if self.image_request.error() == QNetworkReply.NoError: - data = self.image_request.readAll().data() - image: Image.Image = Image.open(io.BytesIO(data)) - image = image.resize((self.width, int(self.width * 9 / 16))) - - if save: - image.save(os.path.join(self.path, self.json_info["title"] + "_wide.png"), format="png") - pixmap = QPixmap() - pixmap.fromImage(ImageQt(image)) - self.image.setPixmap(pixmap) - def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: self.show_info.emit(self.json_info) diff --git a/rare/components/tabs/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index 3a4ca515..8ad6da61 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -63,7 +63,6 @@ class ShopGame: tmp.links.append(tuple((item.replace("link", ""), links[item]))) tmp.available_voice_langs = api_data["data"]["requirements"].get("languages", "Failed") tmp.reqs = {} - print(api_data["data"]["requirements"]) for i, system in enumerate(api_data["data"]["requirements"].get("systems", [])): try: tmp.reqs[system["systemType"]] = {} diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 9eaf1ae2..8a4a9c32 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -1,8 +1,8 @@ import io import os +from logging import getLogger from PIL import Image -from PIL.ImageQt import ImageQt from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QUrl, QSettings from PyQt5.QtGui import QMovie, QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply @@ -13,6 +13,7 @@ from qtawesome import icon from rare import style_path from rare.ui.utils.pathedit import Ui_PathEdit +logger = getLogger("ExtraWidgets") class FlowLayout(QLayout): def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1): @@ -284,18 +285,24 @@ class ImageLabel(QLabel): self.show_image() def image_ready(self): + self.setPixmap(QPixmap()) if self.request: if self.request.error() == QNetworkReply.NoError: - data = self.request.readAll().data() image: Image.Image = Image.open(io.BytesIO(data)) image = image.resize((self.img_size[0], self.img_size[1])) if QSettings().value("cache_images", True, bool): image.save(os.path.join(self.path, self.name), format="png") + byte_array = io.BytesIO() + image.save(byte_array, format="PNG") + # pixmap = QPixmap.fromImage(ImageQt(image)) pixmap = QPixmap() - pixmap.fromImage(ImageQt(image)) + pixmap.loadFromData(byte_array.getvalue()) + # pixmap = QPixmap.fromImage(ImageQt.ImageQt(image)) self.setPixmap(pixmap) + else: + logger.error(self.request.errorString()) else: return From 72457c8b275556a04240d6eacf0076222b5676cb Mon Sep 17 00:00:00 2001 From: Dummerle Date: Fri, 18 Jun 2021 00:29:36 +0200 Subject: [PATCH 22/30] Load browse_games.py when tab clicked --- rare/components/tabs/shop/__init__.py | 7 +-- rare/components/tabs/shop/browse_games.py | 3 ++ rare/components/tabs/shop/thread.py | 57 ----------------------- 3 files changed, 7 insertions(+), 60 deletions(-) delete mode 100644 rare/components/tabs/shop/thread.py diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index 64067c9d..f6af5c59 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -1,12 +1,12 @@ import os from PyQt5.QtWidgets import QStackedWidget, QTabWidget -from custom_legendary.core import LegendaryCore +from custom_legendary.core import LegendaryCore +from rare.components.tabs.shop.browse_games import BrowseGames from rare.components.tabs.shop.search_results import SearchResults from rare.components.tabs.shop.shop_info import ShopGameInfo from rare.components.tabs.shop.shop_widget import ShopWidget -from rare.components.tabs.shop.browse_games import BrowseGames class Shop(QStackedWidget): @@ -28,6 +28,7 @@ class Shop(QStackedWidget): self.store_tabs = QTabWidget() self.store_tabs.addTab(self.shop, self.tr("Games")) self.store_tabs.addTab(self.browse_games, self.tr("Browse")) + self.store_tabs.tabBarClicked.connect(lambda x: self.browse_games.load() if x == 1 else None) self.addWidget(self.store_tabs) @@ -48,7 +49,7 @@ class Shop(QStackedWidget): if not self.init: self.init = True self.shop.load() - self.browse_games.prepare_request() + # self.browse_games.prepare_request() def show_game_info(self, data): self.info.update_game(data) diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py index a6e8f4bb..0b027f4e 100644 --- a/rare/components/tabs/shop/browse_games.py +++ b/rare/components/tabs/shop/browse_games.py @@ -64,6 +64,9 @@ class BrowseGames(QWidget, Ui_browse_games): checkbox.deactivated.connect(lambda x: self.prepare_request(removed_type=x)) self.type_gb.layout().addWidget(checkbox) + def load(self): + self.prepare_request() + def prepare_request(self, price: str = None, added_tag: int = 0, removed_tag: int = 0, added_type: str = "", removed_type: str = ""): diff --git a/rare/components/tabs/shop/thread.py b/rare/components/tabs/shop/thread.py deleted file mode 100644 index 3139c666..00000000 --- a/rare/components/tabs/shop/thread.py +++ /dev/null @@ -1,57 +0,0 @@ -import requests -from PyQt5.QtCore import QThread, pyqtSignal - -from custom_legendary.core import LegendaryCore -from rare.components.tabs.shop.browse_games import game_query -from rare.components.tabs.shop.shop_models import ShopGame -from rare.utils.utils import get_lang - - -class AnalyzeThread(QThread): - get_tags = pyqtSignal(dict) - - def __init__(self, core: LegendaryCore): - super(AnalyzeThread, self).__init__() - self.tags = {} - self.core = core - - def run(self) -> None: - locale = get_lang() - for igame in self.core.get_installed_list(): - data = { - "query": game_query, - "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 20, - "country": locale.upper(), "keywords": igame.title, "locale": locale, "sortDir": "DESC", - "allowCountries": locale.upper(), - "start": 0, "tag": "", "withMapping": False, "withPrice": True} - } - - try: - search_game = \ - requests.post("https://www.epicgames.com/graphql", json=data).json()["data"]["Catalog"]["searchStore"][ - "elements"][0] - except: - continue - slug = search_game["productSlug"] - is_bundle = False - for i in search_game["categories"]: - if "bundles" in i.get("path", ""): - is_bundle = True - - api_game = requests.get( - f"https://store-content.ak.epicgames.com/api/{locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}").json() - - if api_game.get("error"): - print(igame.title) - continue - - game = ShopGame.from_json(api_game, search_game) - - for i in game.tags: - if i not in self.tags.keys(): - self.tags[i] = 1 - else: - self.tags[i] += 1 - - print(self.tags) From beb181c175e78045152adf9dad71ef30c2f44a3a Mon Sep 17 00:00:00 2001 From: Dummerle Date: Fri, 18 Jun 2021 12:24:56 +0200 Subject: [PATCH 23/30] Some fixes --- rare/components/tabs/shop/game_widgets.py | 1 - rare/components/tabs/shop/shop_info.py | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py index e9919cbd..7bc3ae32 100644 --- a/rare/components/tabs/shop/game_widgets.py +++ b/rare/components/tabs/shop/game_widgets.py @@ -1,5 +1,4 @@ import json -import json import logging from PyQt5 import QtGui diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index 84b4b57e..f1aa22dd 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -62,7 +62,6 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.request.finished.connect(self.data_received) def data_received(self): - logging.info(f"Data of game {self.data['title']} received") if self.request: if self.request.error() == QNetworkReply.NoError: error = QJsonParseError() @@ -80,12 +79,18 @@ class ShopGameInfo(QWidget, Ui_shop_info): return self.game = ShopGame.from_json(game, self.data) self.title.setText(self.game.title) + self.price.setFont(QFont()) if self.game.price != "0": self.price.setText(self.game.price) else: self.price.setText(self.tr("Free")) + if self.game.price != self.game.discount_price: - self.discount_price.setText(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) @@ -126,7 +131,6 @@ class ShopGameInfo(QWidget, Ui_shop_info): pass self.tags.setText(", ".join(self.game.tags)) # self.price.setText(self.game.price) - self.request.deleteLater() def button_clicked(self): From 69675ee63990a4222fe10f7b6aa795ecb4ba3ba6 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sun, 20 Jun 2021 21:07:56 +0200 Subject: [PATCH 24/30] Add --debug option --- rare/__main__.py | 1 + rare/app.py | 14 ++++++++------ rare/utils/utils.py | 1 - 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/rare/__main__.py b/rare/__main__.py index 271ddaff..60ae531a 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -12,6 +12,7 @@ def main(): parser.add_argument("-V", "--version", action="store_true", help="Shows version and exits") parser.add_argument("-S", "--silent", action="store_true", help="Launch Rare in background. Open it from System Tray Icon") + parser.add_argument("--debug", action="store_true", help="Launch in debug mode") parser.add_argument("--offline", action="store_true", help="Launch Rare in offline mode") if os.name != "nt": parser.add_argument("--disable-protondb", action="store_true", dest="disable_protondb", diff --git a/rare/app.py b/rare/app.py index 0982d258..befebbe2 100644 --- a/rare/app.py +++ b/rare/app.py @@ -19,12 +19,14 @@ start_time = time.strftime('%y-%m-%d--%H-%M') # year-month-day-hour-minute file_name = os.path.expanduser(f"~/.cache/rare/logs/Rare_{start_time}.log") if not os.path.exists(os.path.dirname(file_name)): os.makedirs(os.path.dirname(file_name)) - -logging.basicConfig( - format='[%(name)s] %(levelname)s: %(message)s', - level=logging.INFO, - filename=file_name, -) +if "--debug" in sys.argv: + logging.basicConfig(format='[%(name)s] %(levelname)s: %(message)s', level=logging.INFO) +else: + logging.basicConfig( + format='[%(name)s] %(levelname)s: %(message)s', + level=logging.INFO, + filename=file_name, + ) logger = logging.getLogger("Rare") diff --git a/rare/utils/utils.py b/rare/utils/utils.py index f295a28b..c9ca9837 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -312,7 +312,6 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): target = os.path.abspath(sys.argv[0]) # Name of link file - linkName = igame.title for c in r'<>?":|\/*': linkName.replace(c, "") From 98386f0bbb3828a47dfe8fa426835e0aef9382c6 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sun, 20 Jun 2021 22:22:36 +0200 Subject: [PATCH 25/30] Add button to search bar in shop main widget --- rare/components/tabs/shop/shop_widget.py | 16 +++++++++--- rare/ui/components/tabs/store/store.py | 6 +---- rare/ui/components/tabs/store/store.ui | 7 ------ rare/utils/extra_widgets.py | 32 ++++++++++++++++++++++-- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 80e84ce6..9e1537a4 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -10,7 +10,7 @@ from PyQt5.QtWidgets import QWidget, QCompleter, QGroupBox, QHBoxLayout, QScroll from rare.components.tabs.shop.game_widgets import GameWidget from rare.ui.components.tabs.store.store import Ui_ShopWidget -from rare.utils.extra_widgets import WaitingSpinner, FlowLayout +from rare.utils.extra_widgets import WaitingSpinner, FlowLayout, ButtonLineEdit from rare.utils.utils import get_lang logger = logging.getLogger("Shop") @@ -40,13 +40,21 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.free_games_widget.layout().addWidget(self.coming_free_games) self.free_games_stack.addWidget(WaitingSpinner()) self.free_games_stack.addWidget(self.free_games_widget) - self.search.textChanged.connect(self.search_games) + self.completer = QCompleter() self.completer.setCaseSensitivity(Qt.CaseInsensitive) - self.search.setCompleter(self.completer) - self.search.returnPressed.connect(self.show_search_result) + self.data = [] + self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search Games")) + self.scrollAreaWidgetContents.layout().insertWidget(0, self.search_bar) + + self.search_bar.textChanged.connect(self.search_games) + + self.search_bar.setCompleter(self.completer) + self.search_bar.returnPressed.connect(self.show_search_result) + self.search_bar.buttonClicked.connect(self.show_search_result) + self.games_groupbox.setLayout(FlowLayout()) self.games_groupbox.setVisible(False) diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py index 6193e3a9..00b6563a 100644 --- a/rare/ui/components/tabs/store/store.py +++ b/rare/ui/components/tabs/store/store.py @@ -8,7 +8,7 @@ # run again. Do not edit this file unless you know what you are doing. -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtWidgets class Ui_ShopWidget(object): @@ -25,9 +25,6 @@ class Ui_ShopWidget(object): self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) self.verticalLayout_3.setObjectName("verticalLayout_3") - self.search = QtWidgets.QLineEdit(self.scrollAreaWidgetContents) - self.search.setObjectName("search") - self.verticalLayout_3.addWidget(self.search) self.free_game_group_box = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) self.free_game_group_box.setObjectName("free_game_group_box") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.free_game_group_box) @@ -51,7 +48,6 @@ class Ui_ShopWidget(object): def retranslateUi(self, ShopWidget): _translate = QtCore.QCoreApplication.translate ShopWidget.setWindowTitle(_translate("ShopWidget", "Form")) - self.search.setPlaceholderText(_translate("ShopWidget", "Search Games")) self.free_game_group_box.setTitle(_translate("ShopWidget", "Free Games")) self.games_groupbox.setTitle(_translate("ShopWidget", "Other nice games")) diff --git a/rare/ui/components/tabs/store/store.ui b/rare/ui/components/tabs/store/store.ui index 2c0beb2b..e103aa4d 100644 --- a/rare/ui/components/tabs/store/store.ui +++ b/rare/ui/components/tabs/store/store.ui @@ -29,13 +29,6 @@ - - - - Search Games - - - diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 8a4a9c32..a30bbea1 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -7,7 +7,7 @@ from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QUrl, QSettings from PyQt5.QtGui import QMovie, QPixmap from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, QHBoxLayout, QWidget, QPushButton, \ - QStyleOptionTab, QStylePainter, QTabBar + QStyleOptionTab, QStylePainter, QTabBar, QLineEdit, QToolButton from qtawesome import icon from rare import style_path @@ -15,6 +15,7 @@ from rare.ui.utils.pathedit import Ui_PathEdit logger = getLogger("ExtraWidgets") + class FlowLayout(QLayout): def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1): super(FlowLayout, self).__init__(parent) @@ -308,5 +309,32 @@ class ImageLabel(QLabel): def show_image(self): self.image = QPixmap(os.path.join(self.path, self.name)).scaled(*self.img_size, - transformMode=Qt.SmoothTransformation) + transformMode=Qt.SmoothTransformation) self.setPixmap(self.image) + + +class ButtonLineEdit(QLineEdit): + buttonClicked = pyqtSignal() + + def __init__(self, icon_name, placeholder_text: str, parent=None): + super(ButtonLineEdit, self).__init__(parent) + + self.button = QToolButton(self) + self.button.setIcon(icon(icon_name, color="white")) + self.button.setStyleSheet('border: 0px; padding: 0px;') + self.button.setCursor(Qt.ArrowCursor) + self.button.clicked.connect(self.buttonClicked.emit) + + frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) + buttonSize = self.button.sizeHint() + + self.setStyleSheet('QLineEdit {padding-right: %dpx; }' % (buttonSize.width() + frameWidth + 1)) + self.setMinimumSize(max(self.minimumSizeHint().width(), buttonSize.width() + frameWidth * 2 + 2), + max(self.minimumSizeHint().height(), buttonSize.height() + frameWidth * 2 + 2)) + + def resizeEvent(self, event): + buttonSize = self.button.sizeHint() + frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) + self.button.move(self.rect().right() - frameWidth - buttonSize.width(), + (self.rect().bottom() - buttonSize.height() + 1) / 2) + super(ButtonLineEdit, self).resizeEvent(event) From 2233c7f3385006e34680206306881d12c6d2b1b6 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 30 Jun 2021 00:38:42 +0200 Subject: [PATCH 26/30] Some optimizations --- rare/components/tabs/shop/browse_games.py | 5 +++- rare/components/tabs/shop/search_results.py | 12 ++++++---- rare/components/tabs/shop/shop_info.py | 2 ++ rare/components/tabs/shop/shop_widget.py | 26 +++++++++++---------- rare/utils/extra_widgets.py | 2 +- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py index 0b027f4e..3777d382 100644 --- a/rare/components/tabs/shop/browse_games.py +++ b/rare/components/tabs/shop/browse_games.py @@ -18,6 +18,7 @@ logger = logging.getLogger("BrowseGames") class BrowseGames(QWidget, Ui_browse_games): show_game = pyqtSignal(dict) + init = False price = "" tags = [] types = [] @@ -65,7 +66,9 @@ class BrowseGames(QWidget, Ui_browse_games): self.type_gb.layout().addWidget(checkbox) def load(self): - self.prepare_request() + if not self.init: + self.prepare_request() + self.init = True def prepare_request(self, price: str = None, added_tag: int = 0, removed_tag: int = 0, added_type: str = "", removed_type: str = ""): diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index 613de082..73dcdaec 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -9,7 +9,6 @@ from rare.utils.extra_widgets import ImageLabel, FlowLayout class SearchResults(QWidget): show_info = pyqtSignal(dict) - # TODO nice look def __init__(self): super(SearchResults, self).__init__() self.main_layout = QVBoxLayout() @@ -32,10 +31,13 @@ class SearchResults(QWidget): QVBoxLayout().addWidget(self.widget) self.widget = QWidget() self.layout = FlowLayout() - for res in results: - w = _SearchResultItem(res) - w.show_info.connect(self.show_info.emit) - self.layout.addWidget(w) + 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) diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index f1aa22dd..87d19bb5 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -47,6 +47,8 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.owned_label.setVisible(False) self.dev.setText(self.tr("Loading")) + self.price.setText(self.tr("Loading")) + self.title.setText(self.tr("Loading")) self.image.setPixmap(QPixmap()) self.data = data is_bundle = False diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 9e1537a4..93a4dec0 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -150,11 +150,13 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") self.search_request = self.manager.post(request, payload) self.search_request.finished.connect(lambda: self.show_search_results(show_direct)) + self.active_search_request = True else: self.next_search = text def show_search_results(self, show_direct=False): + self.active_search_request = False if self.search_request: try: if self.search_request.error() == QNetworkReply.NoError: @@ -166,14 +168,14 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.data = data if show_direct: self.show_search_result(True) - return - titles = [i.get("title") for i in data] - model = QStringListModel() - model.setStringList(titles) - self.completer.setModel(model) - # self.completer.popup() - if self.search_request: - self.search_request.deleteLater() + else: + titles = [i.get("title") for i in data] + model = QStringListModel() + model.setStringList(titles) + self.completer.setModel(model) + # self.completer.popup() + if self.search_request: + self.search_request.deleteLater() else: logging.error(error.errorString()) # response = .decode(encoding="utf-8") @@ -181,13 +183,13 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): # results = json.loads(response) except RuntimeError: return - - self.search_games(self.next_search) + if self.next_search: + self.search_games(self.next_search) + self.next_search = "" def show_search_result(self, show_direct=False): if not show_direct: - if self.data: - self.show_info.emit(self.data) + self.show_info.emit(self.data) else: try: result = self.data[0] diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index a30bbea1..fa65cbeb 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -324,7 +324,7 @@ class ButtonLineEdit(QLineEdit): self.button.setStyleSheet('border: 0px; padding: 0px;') self.button.setCursor(Qt.ArrowCursor) self.button.clicked.connect(self.buttonClicked.emit) - + self.setPlaceholderText(placeholder_text) frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) buttonSize = self.button.sizeHint() From 983e80a4c64955e78deb1d683bce293cf9dec7f3 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sat, 7 Aug 2021 23:42:40 +0200 Subject: [PATCH 27/30] Some optimizations --- rare/components/tabs/shop/__init__.py | 6 +- rare/components/tabs/shop/browse_games.py | 76 +++------- rare/components/tabs/shop/constants.py | 31 ++++ rare/components/tabs/shop/game_widgets.py | 83 +++-------- rare/components/tabs/shop/search_results.py | 37 ++++- rare/components/tabs/shop/shop_info.py | 29 +--- rare/components/tabs/shop/shop_widget.py | 156 +++++--------------- rare/utils/extra_widgets.py | 43 +++--- rare/utils/utils.py | 85 ++++++++++- 9 files changed, 248 insertions(+), 298 deletions(-) diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index f6af5c59..7a8e8f8d 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -41,7 +41,7 @@ class Shop(QStackedWidget): 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_info) + self.shop.show_info.connect(self.show_search_results) self.shop.show_game.connect(self.show_game_info) self.browse_games.show_game.connect(self.show_game_info) @@ -55,6 +55,6 @@ class Shop(QStackedWidget): self.info.update_game(data) self.setCurrentIndex(2) - def show_info(self, data): - self.search_results.show_results(data) + def show_search_results(self, text: str): + self.search_results.load_results(text) self.setCurrentIndex(1) diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py index 3777d382..ff8df1ee 100644 --- a/rare/components/tabs/shop/browse_games.py +++ b/rare/components/tabs/shop/browse_games.py @@ -1,17 +1,15 @@ import datetime -import json import logging import random -from PyQt5.QtCore import QUrl, pyqtSignal, QJsonParseError, QJsonDocument -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager +from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QWidget, QCheckBox, QVBoxLayout, QLabel from rare.components.tabs.shop.constants import game_query, Constants from rare.components.tabs.shop.game_widgets import GameWidget from rare.ui.components.tabs.store.browse_games import Ui_browse_games from rare.utils.extra_widgets import FlowLayout, WaitingSpinner -from rare.utils.utils import get_lang +from rare.utils.utils import get_lang, QtRequestManager logger = logging.getLogger("BrowseGames") @@ -22,8 +20,6 @@ class BrowseGames(QWidget, Ui_browse_games): price = "" tags = [] types = [] - request_active = False - next_request = False def __init__(self, path): super(BrowseGames, self).__init__() @@ -32,7 +28,8 @@ class BrowseGames(QWidget, Ui_browse_games): self.games_widget = QWidget() self.games_widget.setLayout(FlowLayout()) self.games.setWidget(self.games_widget) - self.manager = QNetworkAccessManager() + self.manager = QtRequestManager("json") + self.manager.data_ready.connect(self.show_games) self.stack.addWidget(WaitingSpinner()) @@ -85,10 +82,6 @@ class BrowseGames(QWidget, Ui_browse_games): if removed_type and removed_type in self.types: self.types.remove(removed_type) - if self.request_active: - self.next_request = True - return - locale = get_lang() self.stack.setCurrentIndex(2) date = f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%X')}.{str(random.randint(0, 999)).zfill(3)}Z]" @@ -112,57 +105,26 @@ class BrowseGames(QWidget, Ui_browse_games): if self.types: payload["variables"]["category"] = "|".join(self.types) - request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self.request_active = True - self.game_request = self.manager.post(request, json.dumps(payload).encode("utf-8")) - self.game_request.finished.connect(self.show_games) + self.manager.post("https://www.epicgames.com/graphql", payload) - def show_games(self): - if self.game_request: - if self.game_request.error() == QNetworkReply.NoError: - error = QJsonParseError() - json_data = QJsonDocument.fromJson(self.game_request.readAll().data(), error) + def show_games(self, data): + data = data["data"]["Catalog"]["searchStore"]["elements"] + QWidget().setLayout(self.games_widget.layout()) - if error.error == error.NoError: - try: - games = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ - "elements"] - except TypeError as e: - logger.error("Type Error: " + str(e)) - self.stack.setCurrentIndex(1) - else: - QWidget().setLayout(self.games_widget.layout()) - if games: - self.games_widget.setLayout(FlowLayout()) + if data: + self.games_widget.setLayout(FlowLayout()) - for game in games: - w = GameWidget(self.path, game, 275) - self.games_widget.layout().addWidget(w) - w.show_info.connect(self.show_game.emit) + for game in data: + w = GameWidget(self.path, game, 275) + self.games_widget.layout().addWidget(w) + w.show_info.connect(self.show_game.emit) - else: - self.games_widget.setLayout(QVBoxLayout()) - self.games_widget.layout().addWidget( - QLabel(self.tr("Could not get games matching the filter"))) - self.games_widget.layout().addStretch(1) - self.stack.setCurrentIndex(0) - self.request_active = False - if self.next_request: - self.prepare_request() - self.next_request = () - - return - - else: - logger.error(error.errorString()) - else: - logger.error(self.game_request.errorString()) - if self.next_request: - self.prepare_request() - self.next_request = False else: - self.stack.setCurrentIndex(1) + self.games_widget.setLayout(QVBoxLayout()) + self.games_widget.layout().addWidget( + QLabel(self.tr("Could not get games matching the filter"))) + self.games_widget.layout().addStretch(1) + self.stack.setCurrentIndex(0) class CheckBox(QCheckBox): diff --git a/rare/components/tabs/shop/constants.py b/rare/components/tabs/shop/constants.py index d604ef33..53cdbd9a 100644 --- a/rare/components/tabs/shop/constants.py +++ b/rare/components/tabs/shop/constants.py @@ -72,3 +72,34 @@ game_query = "query searchStoreQuery($allowCountries: String, $category: String, "endDate\n discountSetting {\n discountType\n " \ "discountPercentage\n }\n }\n }\n }\n }\n paging {\n " \ " count\n total\n }\n }\n }\n}\n " + +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 " diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py index 7bc3ae32..04120fbc 100644 --- a/rare/components/tabs/shop/game_widgets.py +++ b/rare/components/tabs/shop/game_widgets.py @@ -1,13 +1,13 @@ -import json import logging from PyQt5 import QtGui -from PyQt5.QtCore import pyqtSignal, QUrl, QJsonParseError, QJsonDocument -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtNetwork import QNetworkAccessManager from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel +from rare.components.tabs.shop.constants import search_query from rare.utils.extra_widgets import ImageLabel -from rare.utils.utils import get_lang +from rare.utils.utils import get_lang, QtRequestManager logger = logging.getLogger("GameWidgets") @@ -19,12 +19,11 @@ class GameWidget(QWidget): super(GameWidget, self).__init__() self.manager = QNetworkAccessManager() self.width = width + self.path = path if json_info: - self.init_ui(json_info, path) - self.path = path + self.init_ui(json_info) - def init_ui(self, json_info, path): - self.path = path + def init_ui(self, json_info): self.layout = QVBoxLayout() self.image = ImageLabel() self.layout.addWidget(self.image) @@ -58,69 +57,21 @@ class GameWidget(QWidget): @classmethod def from_request(cls, name, path): c = cls(path) - c.manager = QNetworkAccessManager() - c.request = c.manager.get(QNetworkRequest()) - + c.manager = QtRequestManager("json") + c.manager.data_ready.connect(c.handle_response) locale = get_lang() - payload = json.dumps({ - "query": query, + payload = { + "query": search_query, "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 1, "country": "DE", "keywords": name, "locale": locale, "sortDir": "DESC", "allowCountries": locale.upper(), "start": 0, "tag": "", "withMapping": False, "withPrice": True} - }).encode() - request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - c.search_request = c.manager.post(request, payload) - c.search_request.finished.connect(lambda: c.handle_response(path)) + } + c.manager.post("https://www.epicgames.com/graphql", payload) + c.search_request.da.connect(lambda: c.handle_response(path)) return c - def handle_response(self, path): - if self.search_request: - if self.search_request.error() == QNetworkReply.NoError: - error = QJsonParseError() - json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) - if QJsonParseError.NoError == error.error: - data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ - "elements"][0] - self.init_ui(data, path) - else: - logging.error(error.errorString()) - return + def handle_response(self, data): - else: - return - else: - return - - -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 " + data = data["data"]["Catalog"]["searchStore"]["elements"][0] + self.init_ui(data) diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index 73dcdaec..a8026a22 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -1,16 +1,21 @@ 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 +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QGroupBox, QPushButton, \ + QStackedWidget -from rare.utils.extra_widgets import ImageLabel, FlowLayout +from rare.components.tabs.shop.constants import search_query +from rare.utils.extra_widgets import ImageLabel, FlowLayout, WaitingSpinner +from rare.utils.utils import QtRequestManager, get_lang -class SearchResults(QWidget): +class SearchResults(QStackedWidget): show_info = pyqtSignal(dict) def __init__(self): super(SearchResults, self).__init__() + self.search_result_widget = QWidget() + self.addWidget(self.search_result_widget) self.main_layout = QVBoxLayout() self.back_button = QPushButton(self.tr("Back")) self.main_layout.addWidget(self.back_button) @@ -25,9 +30,30 @@ class SearchResults(QWidget): self.layout = FlowLayout() self.widget.setLayout(self.layout) - self.setLayout(self.main_layout) + self.search_manager = QtRequestManager("json") + self.search_manager.data_ready.connect(self.show_results) - def show_results(self, results: list): + 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 != "": + locale = get_lang() + payload = { + "query": search_query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 20, + "country": locale.upper(), "keywords": text, "locale": locale, "sortDir": "DESC", + "allowCountries": locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + } + self.search_manager.post("https://www.epicgames.com/graphql", payload) + + def show_results(self, results: dict): + results = results["data"]["Catalog"]["searchStore"]["elements"] QVBoxLayout().addWidget(self.widget) self.widget = QWidget() self.layout = FlowLayout() @@ -40,6 +66,7 @@ class SearchResults(QWidget): self.layout.addWidget(w) self.widget.setLayout(self.layout) self.result_area.setWidget(self.widget) + self.setCurrentIndex(0) class _SearchResultItem(QGroupBox): diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/shop_info.py index 87d19bb5..c0e170f2 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/shop_info.py @@ -1,16 +1,13 @@ -import json import logging import webbrowser -from PyQt5.QtCore import QUrl, QJsonDocument, QJsonParseError from PyQt5.QtGui import QPixmap, QFont -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QWidget, QLabel from rare.components.tabs.shop.shop_models import ShopGame from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info from rare.utils.extra_widgets import WaitingSpinner, ImageLabel -from rare.utils.utils import get_lang +from rare.utils.utils import get_lang, QtRequestManager logger = logging.getLogger("ShopInfo") @@ -28,7 +25,8 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.image = ImageLabel() self.image_stack.addWidget(self.image) self.image_stack.addWidget(WaitingSpinner()) - self.manager = QNetworkAccessManager() + self.manager = QtRequestManager("json") + self.manager.data_ready.connect(self.data_received) def update_game(self, data: dict): self.image_stack.setCurrentIndex(1) @@ -60,25 +58,9 @@ class ShopGameInfo(QWidget, Ui_shop_info): locale = get_lang() url = f"https://store-content.ak.epicgames.com/api/{locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" # game = api_utils.get_product(slug, locale) - self.request = self.manager.get(QNetworkRequest(QUrl(url))) - self.request.finished.connect(self.data_received) + self.manager.get(url) - def data_received(self): - if self.request: - if self.request.error() == QNetworkReply.NoError: - error = QJsonParseError() - json_data = QJsonDocument.fromJson(self.request.readAll().data(), error) - - if error.error == error.NoError: - game = json.loads(json_data.toJson().data().decode()) - else: - logging.info(self.slug, error.errorString()) - return - else: - logger.error("Data failed") - return - else: - return + def data_received(self, game): self.game = ShopGame.from_json(game, self.data) self.title.setText(self.game.title) self.price.setFont(QFont()) @@ -133,7 +115,6 @@ class ShopGameInfo(QWidget, Ui_shop_info): pass self.tags.setText(", ".join(self.game.tags)) # self.price.setText(self.game.price) - self.request.deleteLater() def button_clicked(self): webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 93a4dec0..2c3e0e52 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -1,24 +1,22 @@ import datetime -import json import logging -from json import JSONDecodeError -from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QJsonDocument, QJsonParseError, \ - QStringListModel -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply +from PyQt5.QtCore import Qt, pyqtSignal, QStringListModel +from PyQt5.QtNetwork import QNetworkAccessManager from PyQt5.QtWidgets import QWidget, QCompleter, QGroupBox, QHBoxLayout, QScrollArea +from rare.components.tabs.shop.constants import search_query from rare.components.tabs.shop.game_widgets import GameWidget from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.utils.extra_widgets import WaitingSpinner, FlowLayout, ButtonLineEdit -from rare.utils.utils import get_lang +from rare.utils.utils import QtRequestManager, get_lang logger = logging.getLogger("Shop") # noinspection PyAttributeOutsideInit,PyBroadException class ShopWidget(QScrollArea, Ui_ShopWidget): - show_info = pyqtSignal(list) + show_info = pyqtSignal(str) show_game = pyqtSignal(dict) free_game_widgets = [] active_search_request = False @@ -49,31 +47,40 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search Games")) self.scrollAreaWidgetContents.layout().insertWidget(0, self.search_bar) - self.search_bar.textChanged.connect(self.search_games) + # self.search_bar.textChanged.connect(self.search_games) self.search_bar.setCompleter(self.completer) - self.search_bar.returnPressed.connect(self.show_search_result) - self.search_bar.buttonClicked.connect(self.show_search_result) + self.search_bar.returnPressed.connect(self.show_search_results) + self.search_bar.buttonClicked.connect(self.show_search_results) self.games_groupbox.setLayout(FlowLayout()) self.games_groupbox.setVisible(False) + self.search_request_manager = QtRequestManager("json") + self.search_request_manager.data_ready.connect(self.set_completer) + + self.search_bar.textChanged.connect(self.load_completer) + + def load_completer(self, text): + if text != "": + locale = get_lang() + payload = { + "query": search_query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 20, + "country": locale.upper(), "keywords": text, "locale": locale, "sortDir": "DESC", + "allowCountries": locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + } + self.search_request_manager.post("https://www.epicgames.com/graphql", payload) + def load(self): url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" - self.free_game_request = self.manager.get(QNetworkRequest(QUrl(url))) - self.free_game_request.finished.connect(self.add_free_games) + self.free_game_request_manager = QtRequestManager("json") + self.free_game_request_manager.get(url) + self.free_game_request_manager.data_ready.connect(self.add_free_games) - def add_free_games(self): - if self.free_game_request: - if self.free_game_request.error() == QNetworkReply.NoError: - try: - free_games = json.loads(self.free_game_request.readAll().data().decode()) - except JSONDecodeError: - return - else: - return - else: - return + def add_free_games(self, free_games): free_games = free_games["data"]["Catalog"]["searchStore"]["elements"] date = datetime.datetime.now() free_games_now = [] @@ -132,100 +139,13 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.coming_free_games.layout().addStretch(1) # self.coming_free_games.setFixedWidth(int(40 + len(coming_free_games) * 300)) self.free_games_stack.setCurrentIndex(1) - self.free_game_request.deleteLater() - def search_games(self, text, show_direct=False): - if not self.active_search_request: - if text != "": - locale = get_lang() - payload = json.dumps({ - "query": query, - "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 20, - "country": locale.upper(), "keywords": text, "locale": locale, "sortDir": "DESC", - "allowCountries": locale.upper(), - "start": 0, "tag": "", "withMapping": False, "withPrice": True} - }).encode() - request = QNetworkRequest(QUrl("https://www.epicgames.com/graphql")) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - self.search_request = self.manager.post(request, payload) - self.search_request.finished.connect(lambda: self.show_search_results(show_direct)) - self.active_search_request = True + def set_completer(self, search_data): + search_data = search_data["data"]["Catalog"]["searchStore"]["elements"] + titles = [i.get("title") for i in search_data] + model = QStringListModel() + model.setStringList(titles) + self.completer.setModel(model) - else: - self.next_search = text - - def show_search_results(self, show_direct=False): - self.active_search_request = False - if self.search_request: - try: - if self.search_request.error() == QNetworkReply.NoError: - error = QJsonParseError() - json_data = QJsonDocument.fromJson(self.search_request.readAll().data(), error) - if QJsonParseError.NoError == error.error: - data = json.loads(json_data.toJson().data().decode())["data"]["Catalog"]["searchStore"][ - "elements"] - self.data = data - if show_direct: - self.show_search_result(True) - else: - titles = [i.get("title") for i in data] - model = QStringListModel() - model.setStringList(titles) - self.completer.setModel(model) - # self.completer.popup() - if self.search_request: - self.search_request.deleteLater() - else: - logging.error(error.errorString()) - # response = .decode(encoding="utf-8") - # print(response) - # results = json.loads(response) - except RuntimeError: - return - if self.next_search: - self.search_games(self.next_search) - self.next_search = "" - - def show_search_result(self, show_direct=False): - if not show_direct: - self.show_info.emit(self.data) - else: - try: - result = self.data[0] - except IndexError: - print("error") - return - self.show_game.emit(result) - - -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 " + def show_search_results(self): + self.show_info.emit(self.search_bar.text()) diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index fa65cbeb..1ebcb757 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -3,15 +3,15 @@ import os from logging import getLogger from PIL import Image -from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QUrl, QSettings +from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QSettings from PyQt5.QtGui import QMovie, QPixmap -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, QHBoxLayout, QWidget, QPushButton, \ QStyleOptionTab, QStylePainter, QTabBar, QLineEdit, QToolButton from qtawesome import icon from rare import style_path from rare.ui.utils.pathedit import Ui_PathEdit +from rare.utils.utils import QtRequestManager logger = getLogger("ExtraWidgets") @@ -266,7 +266,8 @@ class ImageLabel(QLabel): if p := os.environ.get("XDG_CACHE_HOME"): path = os.path.join(p, "rare", "cache") self.path = path - self.manager = QNetworkAccessManager() + self.manager = QtRequestManager("bytes") + self.manager.data_ready.connect(self.image_ready) def update_image(self, url, name, size: tuple = (240, 320)): self.setFixedSize(*size) @@ -280,32 +281,26 @@ class ImageLabel(QLabel): name_extension = "tall" self.name = f"{self.name}_{name_extension}.png" if not os.path.exists(os.path.join(self.path, self.name)): - self.request = self.manager.get(QNetworkRequest(QUrl(url))) - self.request.finished.connect(self.image_ready) + self.manager.get(url) + # self.request.finished.connect(self.image_ready) else: self.show_image() - def image_ready(self): + def image_ready(self, data): self.setPixmap(QPixmap()) - if self.request: - if self.request.error() == QNetworkReply.NoError: - data = self.request.readAll().data() - image: Image.Image = Image.open(io.BytesIO(data)) - image = image.resize((self.img_size[0], self.img_size[1])) - if QSettings().value("cache_images", True, bool): - image.save(os.path.join(self.path, self.name), format="png") - byte_array = io.BytesIO() - image.save(byte_array, format="PNG") - # pixmap = QPixmap.fromImage(ImageQt(image)) - pixmap = QPixmap() - pixmap.loadFromData(byte_array.getvalue()) - # pixmap = QPixmap.fromImage(ImageQt.ImageQt(image)) - self.setPixmap(pixmap) - else: - logger.error(self.request.errorString()) - else: - return + image: Image.Image = Image.open(io.BytesIO(data)) + image = image.resize((self.img_size[0], self.img_size[1])) + + if QSettings().value("cache_images", True, bool): + image.save(os.path.join(self.path, self.name), format="png") + byte_array = io.BytesIO() + image.save(byte_array, format="PNG") + # pixmap = QPixmap.fromImage(ImageQt(image)) + pixmap = QPixmap() + pixmap.loadFromData(byte_array.getvalue()) + # pixmap = QPixmap.fromImage(ImageQt.ImageQt(image)) + self.setPixmap(pixmap) def show_image(self): self.image = QPixmap(os.path.join(self.path, self.name)).scaled(*self.img_size, diff --git a/rare/utils/utils.py b/rare/utils/utils.py index c9ca9837..b8485435 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -6,8 +6,9 @@ from logging import getLogger import requests from PIL import Image, UnidentifiedImageError -from PyQt5.QtCore import pyqtSignal, QLocale, QSettings +from PyQt5.QtCore import pyqtSignal, QLocale, QSettings, QObject, QUrl, QJsonParseError, QJsonDocument from PyQt5.QtGui import QPalette, QColor +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply # Windows if os.name == "nt": @@ -336,3 +337,85 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): shortcut.IconLocation = os.path.join(icon + ".ico") shortcut.save() + + +class QtRequestManager(QObject): + data_ready = pyqtSignal(object) + request = None + request_active = False + + def __init__(self, type: str = "json", queue=False): + super(QtRequestManager, self).__init__() + self.manager = QNetworkAccessManager() + self.type = type + self.queue = queue + if self.queue: + self.next_request = [] + else: + self.next_request = ["", tuple(())] + + def post(self, url: str, payload: dict): + if not self.request_active: + request = QNetworkRequest(QUrl(url)) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + + payload = json.dumps(payload).encode("utf-8") + + self.request = self.manager.post(request, payload) + self.request_active = True + self.request.finished.connect(self.prepare_data) + + else: + if self.queue: + self.next_request.append(["post", (url, payload)]) + else: + self.next_request = ["post", (url, payload)] + + def get(self, url: str): + if not self.request_active: + self.request_active = True + self.request = self.manager.get(QNetworkRequest(QUrl(url))) + self.request.finished.connect(self.prepare_data) + else: + if self.queue: + self.next_request.append(["get", (url,)]) + else: + self.next_request = ["get", (url,)] + + def prepare_data(self): + self.request_active = False + data = {} if self.type == "json" else b"" + if self.request: + try: + if self.request.error() == QNetworkReply.NoError: + if self.type == "json": + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.request.readAll().data(), error) + if QJsonParseError.NoError == error.error: + data = json.loads(json_data.toJson().data().decode()) + + else: + logger.error(error.errorString()) + else: + data = self.request.readAll().data() + + except RuntimeError as e: + logger.error(str(e)) + self.data_ready.emit(data) + self.request.deleteLater() + + if self.queue: + if self.next_request: + if self.next_request[0][0] == "post": + self.post(*self.next_request[0][1]) + else: + self.get(self.next_request[0][1][0]) + self.next_request.pop(0) + else: + if method := self.next_request[0]: + if method == "post": + self.post(*self.next_request[1]) + self.next_request = ["", ()] + else: + self.get(self.next_request[1][0]) + self.next_request = ["", ()] From 59a7359e19ec227ceee54efe82edb77ae4762bcb Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sun, 8 Aug 2021 00:49:27 +0200 Subject: [PATCH 28/30] Set cache_dir and data_dir in one place (xdg) --- rare/__init__.py | 21 +++++++++++++++++++ rare/__main__.py | 4 ++-- rare/app.py | 4 ++-- rare/components/dialogs/launch_dialog.py | 11 +++++----- rare/components/main_window.py | 3 ++- .../tabs/games/game_info/__init__.py | 3 ++- rare/components/tabs/games/game_info/dlcs.py | 3 ++- .../tabs/games/game_info/uninstalled_info.py | 3 ++- rare/components/tabs/games/game_list.py | 3 ++- .../game_widgets/base_installed_widget.py | 3 ++- rare/components/tabs/settings/rare.py | 18 ++++++++-------- rare/components/tabs/shop/__init__.py | 13 +++--------- rare/ui/components/tabs/settings/rare.py | 6 ++++++ rare/ui/components/tabs/settings/rare.ui | 21 +++++++++++++++---- rare/utils/extra_widgets.py | 7 ++----- rare/utils/steam_grades.py | 14 +++++++------ rare/utils/utils.py | 10 ++++----- 17 files changed, 93 insertions(+), 54 deletions(-) diff --git a/rare/__init__.py b/rare/__init__.py index e826caf3..b95517e7 100644 --- a/rare/__init__.py +++ b/rare/__init__.py @@ -1,5 +1,26 @@ import os __version__ = "1.4.1" + style_path = os.path.join(os.path.dirname(__file__), "styles/") lang_path = os.path.join(os.path.dirname(__file__), "languages/") + +# Cache Directory: Store images +if p := os.getenv("XDG_CACHE_HOME"): + cache_dir = os.path.join(p, "rare/cache") +elif os.name == "nt": + cache_dir = os.path.expandvars("%APPDATA%/rare/cache") +else: + cache_dir = os.path.expanduser("~/.cache/rare/cache") +if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + +# Data Directory: Images +if p := os.getenv("XDG_DATA_HOME"): + data_dir = os.path.join(p, "rare") +if os.name == "nt": + data_dir = os.path.expandvars("%APPDATA%/rare/") +else: + data_dir = os.path.expanduser("~/.local/share/rare/") +if not os.path.exists(data_dir): + os.makedirs(data_dir) diff --git a/rare/__main__.py b/rare/__main__.py index 60ae531a..bf9c6fba 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -3,7 +3,7 @@ import os from argparse import ArgumentParser -from rare import __version__ +from rare import __version__, data_dir from rare.utils import singleton, utils @@ -47,7 +47,7 @@ def main(): except singleton.SingleInstanceException: print("Rare is already running") - with open(os.path.expanduser("~/.cache/rare/lockfile"), "w") as file: + with open(os.path.join(data_dir, "lockfile"), "w") as file: if args.subparser == "launch": file.write("launch " + args.app_name) else: diff --git a/rare/app.py b/rare/app.py index befebbe2..7f12df59 100644 --- a/rare/app.py +++ b/rare/app.py @@ -9,14 +9,14 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QStyleFactory from custom_legendary.core import LegendaryCore -from rare import lang_path, style_path +from rare import lang_path, style_path, cache_dir from rare.components.dialogs.launch_dialog import LaunchDialog from rare.components.main_window import MainWindow from rare.components.tray_icon import TrayIcon from rare.utils.utils import get_lang, load_color_scheme start_time = time.strftime('%y-%m-%d--%H-%M') # year-month-day-hour-minute -file_name = os.path.expanduser(f"~/.cache/rare/logs/Rare_{start_time}.log") +file_name = os.path.join(cache_dir, f"logs/Rare_{start_time}.log") if not os.path.exists(os.path.dirname(file_name)): os.makedirs(os.path.dirname(file_name)) if "--debug" in sys.argv: diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index 6842c851..694dfe2d 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -7,6 +7,7 @@ from PyQt5.QtWidgets import QDialog from requests.exceptions import ConnectionError from custom_legendary.core import LegendaryCore +from rare import data_dir, cache_dir from rare.components.dialogs.login import LoginDialog from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog from rare.utils import steam_grades @@ -37,14 +38,14 @@ class SteamThread(QThread): def run(self) -> None: gamelist = self.core.get_game_list(True) - if not os.path.exists(os.path.expanduser("~/.cache/rare/game_list.json")): + if not os.path.exists(os.path.join(data_dir, "game_list.json")): self.action.emit(self.tr("Getting data from ProtonDB")) steam_grades.upgrade_all([(i.app_title, i.app_name) for i in gamelist], self.progress) self.progress.emit(99) self.action.emit(self.tr("Checking Games for data")) - grades = json.load(open(os.path.expanduser("~/.cache/rare/game_list.json"))) - ids = json.load(open(os.path.expanduser("~/.cache/rare/steam_ids.json"))) + grades = json.load(open(os.path.join(data_dir, "game_list.json"))) + ids = json.load(open(os.path.join(cache_dir, "steam_ids.json"))) for game in gamelist: if not grades.get(game.app_name): steam_id = steam_grades.get_steam_id(game.app_title, ids) @@ -58,7 +59,7 @@ class SteamThread(QThread): if not grades[game.app_name].get("grade"): grades[game.app_name]["grade"] = steam_grades.get_grade(game.app_title) - with open(os.path.expanduser("~/.cache/rare/game_list.json"), "w") as f: + with open(os.path.join(data_dir, "game_list.json"), "w") as f: f.write(json.dumps(grades)) f.close() @@ -121,7 +122,7 @@ class LaunchDialog(QDialog, Ui_LaunchDialog): def launch(self, offline=False): # self.core = core - if not os.path.exists(p := os.path.expanduser("~/.cache/rare/images")): + if not os.path.exists(p := QSettings().value("img_dir", os.path.join(data_dir, "images"))): os.makedirs(p) self.offline = offline diff --git a/rare/components/main_window.py b/rare/components/main_window.py index e525ab0d..ac44c53a 100644 --- a/rare/components/main_window.py +++ b/rare/components/main_window.py @@ -6,6 +6,7 @@ from PyQt5.QtGui import QCloseEvent from PyQt5.QtWidgets import QMainWindow, QMessageBox, QApplication from custom_legendary.core import LegendaryCore +from rare import data_dir from rare.components.tab_widget import TabWidget from rare.utils.rpc import DiscordRPC @@ -56,7 +57,7 @@ class MainWindow(QMainWindow): self.timer.start(1000) def timer_finished(self): - file_path = os.path.expanduser("~/.cache/rare/lockfile") + file_path = os.path.join(data_dir, "lockfile") if os.path.exists(file_path): file = open(file_path, "r") action = file.read() diff --git a/rare/components/tabs/games/game_info/__init__.py b/rare/components/tabs/games/game_info/__init__.py index b9729dfa..b661a6d3 100644 --- a/rare/components/tabs/games/game_info/__init__.py +++ b/rare/components/tabs/games/game_info/__init__.py @@ -8,6 +8,7 @@ from qtawesome import icon from custom_legendary.core import LegendaryCore from custom_legendary.models.game import Game, InstalledGame +from rare import data_dir from rare.components.tabs.games.game_info.dlcs import DlcTab from rare.components.tabs.games.game_info.game_settings import GameSettings from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo @@ -73,7 +74,7 @@ class GameInfo(QWidget, Ui_GameInfo): "bronze": self.tr("Bronze"), "fail": self.tr("Could not get grade"), "pending": self.tr("Not enough reports")} - if os.path.exists(p := os.path.expanduser("~/.cache/rare/game_list.json")): + if os.path.exists(p := os.path.join(data_dir, "game_list.json")): self.grade_table = json.load(open(p)) else: self.grade_table = {} diff --git a/rare/components/tabs/games/game_info/dlcs.py b/rare/components/tabs/games/game_info/dlcs.py index a708cca2..8a0bd2db 100644 --- a/rare/components/tabs/games/game_info/dlcs.py +++ b/rare/components/tabs/games/game_info/dlcs.py @@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QGroupBox, QHBoxLayout, QVBoxLayout, QScrollArea, QL from custom_legendary.core import LegendaryCore from custom_legendary.models.game import Game +from rare import data_dir from rare.utils.utils import download_image @@ -82,7 +83,7 @@ class DLCWidget(QGroupBox): super(DLCWidget, self).__init__() self.main_layout = QHBoxLayout() self.dlc = dlc - IMAGE_DIR = QSettings().value("img_dir", os.path.expanduser("~/.cache/rare/images")) + IMAGE_DIR = QSettings().value("img_dir", os.path.join(data_dir, "images")) if installed: if os.path.exists(os.path.join(IMAGE_DIR, dlc.app_name, "FinalArt.png")): diff --git a/rare/components/tabs/games/game_info/uninstalled_info.py b/rare/components/tabs/games/game_info/uninstalled_info.py index 8a230516..e5224ca8 100644 --- a/rare/components/tabs/games/game_info/uninstalled_info.py +++ b/rare/components/tabs/games/game_info/uninstalled_info.py @@ -8,6 +8,7 @@ from qtawesome import icon from custom_legendary.core import LegendaryCore from custom_legendary.models.game import Game +from rare import data_dir from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.utils.extra_widgets import SideTabBar from rare.utils.json_formatter import QJsonModel @@ -65,7 +66,7 @@ class UninstalledInfo(QWidget, Ui_GameInfo): "bronze": self.tr("Bronze"), "fail": self.tr("Could not get grade"), "pending": self.tr("Not enough reports")} - if os.path.exists(p := os.path.expanduser("~/.cache/rare/game_list.json")): + if os.path.exists(p := os.path.join(data_dir, "game_list.json")): self.grade_table = json.load(open(p)) else: self.grade_table = {} diff --git a/rare/components/tabs/games/game_list.py b/rare/components/tabs/games/game_list.py index 5954ba31..c8fe7d9d 100644 --- a/rare/components/tabs/games/game_list.py +++ b/rare/components/tabs/games/game_list.py @@ -7,6 +7,7 @@ from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QScrollArea, QWidget, QLabel, QVBoxLayout, QStackedWidget from custom_legendary.core import LegendaryCore +from rare import data_dir from rare.components.tabs.games.game_widgets.base_installed_widget import BaseInstalledWidget from rare.components.tabs.games.game_widgets.installed_icon_widget import GameWidgetInstalled from rare.components.tabs.games.game_widgets.installed_list_widget import InstalledListWidget @@ -64,7 +65,7 @@ class GameList(QStackedWidget): self.list_layout = QVBoxLayout() self.list_layout.addWidget(QLabel(self.info_text)) - self.IMAGE_DIR = self.settings.value("img_dir", os.path.expanduser("~/.cache/rare/images"), str) + self.IMAGE_DIR = self.settings.value("img_dir", os.path.join(data_dir, "images"), str) self.updates = [] self.widgets = {} diff --git a/rare/components/tabs/games/game_widgets/base_installed_widget.py b/rare/components/tabs/games/game_widgets/base_installed_widget.py index b14acbf6..1aec2a81 100644 --- a/rare/components/tabs/games/game_widgets/base_installed_widget.py +++ b/rare/components/tabs/games/game_widgets/base_installed_widget.py @@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QGroupBox, QMessageBox, QAction from custom_legendary.core import LegendaryCore from custom_legendary.models.game import InstalledGame +from rare import cache_dir from rare.components.dialogs.uninstall_dialog import UninstallDialog from rare.utils import legendary_utils from rare.utils.utils import create_desktop_link @@ -133,7 +134,7 @@ class BaseInstalledWidget(QGroupBox): def stderr(self): stderr = bytes(self.proc.readAllStandardError()).decode("utf-8") self.game_logger.error(stderr) - QMessageBox.warning(self, "Warning", stderr + "\nSee ~/.cache/rare/logs/") + QMessageBox.warning(self, "Warning", stderr + f"\nSee {cache_dir}/logs/") def finished(self, exit_code): logger.info("Game exited with exit code: " + str(exit_code)) diff --git a/rare/components/tabs/settings/rare.py b/rare/components/tabs/settings/rare.py index bf9c511b..eabf0abe 100644 --- a/rare/components/tabs/settings/rare.py +++ b/rare/components/tabs/settings/rare.py @@ -5,12 +5,12 @@ import sys from logging import getLogger from PyQt5.QtCore import QSettings, Qt -from PyQt5.QtWidgets import QFileDialog, QWidget +from PyQt5.QtWidgets import QWidget +from rare import cache_dir, data_dir from rare.components.tabs.settings.rpc_settings import RPCSettings from rare.ui.components.tabs.settings.rare import Ui_RareSettings from rare.utils import utils -from rare.utils.extra_widgets import PathEdit from rare.utils.utils import get_lang, get_possible_langs, get_color_schemes, get_style_sheets logger = getLogger("RareSettings") @@ -39,13 +39,13 @@ class RareSettings(QWidget, Ui_RareSettings): ] self.settings = QSettings() - self.img_dir_path = self.settings.value("img_dir", os.path.expanduser("~/.cache/rare/images/"), type=str) + self.img_dir_path = self.settings.value("img_dir", os.path.join(data_dir, "images"), type=str) language = self.settings.value("language", get_lang(), type=str) - self.logdir = os.path.expanduser("~/.cache/rare/logs") + self.logdir = os.path.join(cache_dir, "logs") # Select Image directory - self.img_dir = PathEdit(self.img_dir_path, file_type=QFileDialog.DirectoryOnly, save_func=self.save_path) - self.img_dir_layout.addWidget(self.img_dir) + # self.img_dir = PathEdit(self.img_dir_path, file_type=QFileDialog.DirectoryOnly, save_func=self.save_path) + # self.img_dir_layout.addWidget(self.img_dir) # Select lang self.lang_select.addItems([i[1] for i in languages]) @@ -125,7 +125,7 @@ class RareSettings(QWidget, Ui_RareSettings): self.log_dir_open_button.clicked.connect(self.open_dir) self.log_dir_clean_button.clicked.connect(self.clean_logdir) - logdir = os.path.expanduser("~/.cache/rare/logs") + logdir = os.path.join(cache_dir, "logs") # get size of logdir size = 0 for i in os.listdir(logdir): @@ -137,8 +137,8 @@ class RareSettings(QWidget, Ui_RareSettings): # self.log_dir_size_label.setVisible(False) def clean_logdir(self): - for i in os.listdir(os.path.expanduser("~/.cache/rare/logs")): - os.remove(os.path.expanduser("~/.cache/rare/logs/") + i) + for i in os.listdir(os.path.join(cache_dir, "logs")): + os.remove(os.path.join(cache_dir, "logs/") + i) self.log_dir_size_label.setText("0KB") def create_start_menu_link(self): diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index 7a8e8f8d..bdd7ea45 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -1,8 +1,7 @@ -import os - from PyQt5.QtWidgets import QStackedWidget, QTabWidget from custom_legendary.core import LegendaryCore +from rare import cache_dir from rare.components.tabs.shop.browse_games import BrowseGames from rare.components.tabs.shop.search_results import SearchResults from rare.components.tabs.shop.shop_info import ShopGameInfo @@ -15,15 +14,9 @@ class Shop(QStackedWidget): def __init__(self, core: LegendaryCore): super(Shop, self).__init__() self.core = core - if p := os.getenv("XDG_CACHE_HOME"): - self.path = p - else: - self.path = os.path.expanduser("~/.cache/rare/cache/") - if not os.path.exists(self.path): - os.makedirs(self.path) - self.shop = ShopWidget(self.path) - self.browse_games = BrowseGames(self.path) + self.shop = ShopWidget(cache_dir) + self.browse_games = BrowseGames(cache_dir) self.store_tabs = QTabWidget() self.store_tabs.addTab(self.shop, self.tr("Games")) diff --git a/rare/ui/components/tabs/settings/rare.py b/rare/ui/components/tabs/settings/rare.py index 599ccbdb..e68100c3 100644 --- a/rare/ui/components/tabs/settings/rare.py +++ b/rare/ui/components/tabs/settings/rare.py @@ -118,6 +118,10 @@ class Ui_RareSettings(object): self.img_dir_group.setObjectName("img_dir_group") self.img_dir_layout = QtWidgets.QVBoxLayout(self.img_dir_group) self.img_dir_layout.setObjectName("img_dir_layout") + self.label = QtWidgets.QLabel(self.img_dir_group) + self.label.setWordWrap(True) + self.label.setObjectName("label") + self.img_dir_layout.addWidget(self.label) self.rare_layout.addWidget(self.img_dir_group, 0, 0, 1, 1, QtCore.Qt.AlignTop) spacerItem2 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.rare_layout.addItem(spacerItem2, 3, 0, 1, 2) @@ -150,6 +154,8 @@ class Ui_RareSettings(object): self.lang_label.setText(_translate("RareSettings", "Language")) self.color_label.setText(_translate("RareSettings", "Color Scheme")) self.img_dir_group.setTitle(_translate("RareSettings", "Image Cache Directory")) + self.label.setText(_translate("RareSettings", + "To change image directory, edit XDG_DATA_HOME variable. To change cache directory edit XDG_CACHE_HOME variable")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/settings/rare.ui b/rare/ui/components/tabs/settings/rare.ui index 6403c5bb..676306fe 100644 --- a/rare/ui/components/tabs/settings/rare.ui +++ b/rare/ui/components/tabs/settings/rare.ui @@ -231,10 +231,23 @@ - - Image Cache Directory - - + + Image Cache Directory + + + + + + To change image directory, edit XDG_DATA_HOME variable. To change cache directory edit + XDG_CACHE_HOME variable + + + + true + + + + diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 1ebcb757..dcbbd243 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -9,7 +9,7 @@ from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, Q QStyleOptionTab, QStylePainter, QTabBar, QLineEdit, QToolButton from qtawesome import icon -from rare import style_path +from rare import style_path, cache_dir from rare.ui.utils.pathedit import Ui_PathEdit from rare.utils.utils import QtRequestManager @@ -262,10 +262,7 @@ class ImageLabel(QLabel): def __init__(self): super(ImageLabel, self).__init__() - path = os.path.expanduser("~/.cache/rare/cache") - if p := os.environ.get("XDG_CACHE_HOME"): - path = os.path.join(p, "rare", "cache") - self.path = path + self.path = cache_dir self.manager = QtRequestManager("bytes") self.manager.data_ready.connect(self.image_ready) diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py index 49feda5a..46c310e8 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -6,9 +6,11 @@ from datetime import date import requests from PyQt5.QtCore import pyqtSignal +from rare import data_dir, cache_dir + replace_chars = ",;.:-_ " -file = os.path.expanduser("~/.cache/rare/game_list.json") +file = os.path.join(data_dir, "game_list.json") url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/" @@ -28,7 +30,7 @@ def get_grade(steam_code): def load_json() -> dict: - if not os.path.exists(p := os.path.expanduser("~/.cache/rare/steam_ids.json")): + if not os.path.exists(p := os.path.join(cache_dir, "steam_ids.json")): response = requests.get(url) steam_ids = json.loads(response.text)["applist"]["apps"] ids = {} @@ -40,7 +42,7 @@ def load_json() -> dict: f.close() return ids else: - return json.loads(open(os.path.expanduser("~/.cache/rare/steam_ids.json"), "r").read()) + return json.loads(open(os.path.join(cache_dir, "steam_ids.json"), "r").read()) def upgrade_all(games, progress: pyqtSignal = None): @@ -59,7 +61,7 @@ def upgrade_all(games, progress: pyqtSignal = None): if progress: progress.emit(int(i / len(games) * 100)) - with open(os.path.expanduser("~/.cache/rare/game_list.json"), "w") as f: + with open(os.path.join(data_dir, "game_list.json"), "w") as f: f.write(json.dumps(data)) f.close() @@ -67,7 +69,7 @@ def upgrade_all(games, progress: pyqtSignal = None): def get_steam_id(title: str, json_data=None): title = title.replace("Early Access", "").replace("Experimental", "").strip() if not json_data: - if not os.path.exists(p := os.path.expanduser("~/.cache/rare/steam_ids.json")): + if not os.path.exists(p := os.path.join(cache_dir, "steam_ids.json")): response = requests.get(url) ids = {} steam_ids = json.loads(response.text)["applist"]["apps"] @@ -78,7 +80,7 @@ def get_steam_id(title: str, json_data=None): f.write(json.dumps(steam_ids)) f.close() else: - ids = json.loads(open(os.path.expanduser("~/.cache/rare/steam_ids.json"), "r").read()) + ids = json.loads(open(os.path.join(cache_dir, "steam_ids.json"), "r").read()) else: ids = json_data steam_name = difflib.get_close_matches(title, ids.keys(), n=1) diff --git a/rare/utils/utils.py b/rare/utils/utils.py index b8485435..f0719849 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -14,14 +14,14 @@ from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkRepl if os.name == "nt": from win32com.client import Dispatch -from rare import lang_path, style_path +from rare import lang_path, style_path, data_dir # Mac not supported from custom_legendary.core import LegendaryCore logger = getLogger("Utils") s = QSettings("Rare", "Rare") -IMAGE_DIR = s.value("img_dir", os.path.expanduser("~/.cache/rare/images"), type=str) +IMAGE_DIR = s.value("img_dir", os.path.join(data_dir, "images"), type=str) def download_images(signal: pyqtSignal, core: LegendaryCore): @@ -272,12 +272,12 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): igame = core.get_installed_game(app_name) if os.path.exists( - os.path.join(QSettings('Rare', 'Rare').value('img_dir', os.path.expanduser('~/.cache/rare/images'), str), + os.path.join(QSettings('Rare', 'Rare').value('img_dir', os.path.join(data_dir, 'images'), str), igame.app_name, 'Thumbnail.png')): - icon = os.path.join(QSettings('Rare', 'Rare').value('img_dir', os.path.expanduser('~/.cache/rare/images'), str), + icon = os.path.join(QSettings('Rare', 'Rare').value('img_dir', os.path.join(data_dir, 'images'), str), igame.app_name, 'Thumbnail') else: - icon = os.path.join(QSettings('Rare', 'Rare').value('img_dir', os.path.expanduser('~/.cache/rare/images'), str), + icon = os.path.join(QSettings('Rare', 'Rare').value('img_dir', os.path.join('images'), str), igame.app_name, 'DieselGameBoxTall') # Linux if os.name == "posix": From ff0ba4c544ece154ad1a341d9cc20b761b7cb378 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 18 Aug 2021 22:17:14 +0200 Subject: [PATCH 29/30] Add wishlist offers in Store --- rare/__main__.py | 5 --- rare/app.py | 5 --- rare/components/tabs/shop/__init__.py | 2 +- rare/components/tabs/shop/constants.py | 2 + rare/components/tabs/shop/game_widgets.py | 24 ++++++++++- rare/components/tabs/shop/shop_widget.py | 51 ++++++++++++++++++----- rare/ui/components/tabs/store/store.py | 8 ++-- rare/ui/components/tabs/store/store.ui | 8 ++-- rare/utils/utils.py | 5 ++- 9 files changed, 78 insertions(+), 32 deletions(-) diff --git a/rare/__main__.py b/rare/__main__.py index bf9c6fba..f582c6ab 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -14,11 +14,6 @@ def main(): help="Launch Rare in background. Open it from System Tray Icon") parser.add_argument("--debug", action="store_true", help="Launch in debug mode") parser.add_argument("--offline", action="store_true", help="Launch Rare in offline mode") - if os.name != "nt": - parser.add_argument("--disable-protondb", action="store_true", dest="disable_protondb", - help="Do not download and check data from ProtonDB. Disable it, if you don't need grades") - parser.add_argument("--enable-protondb", action="store_true", dest="enable_protondb", - help="Enable ProtonDB data, after disabled") parser.add_argument("--desktop-shortcut", action="store_true", dest="desktop_shortcut", help="Use this, if there is no link on desktop to start Rare") diff --git a/rare/app.py b/rare/app.py index 7f12df59..dbffb6af 100644 --- a/rare/app.py +++ b/rare/app.py @@ -60,11 +60,6 @@ class App(QApplication): self.setApplicationName("Rare") self.setOrganizationName("Rare") settings = QSettings() - if os.name != "nt": - if args.disable_protondb: - settings.setValue("disable_protondb", True) - if args.enable_protondb: - settings.remove("disable_protondb") # Translator self.translator = QTranslator() diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index bdd7ea45..6c7ac533 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -15,7 +15,7 @@ class Shop(QStackedWidget): super(Shop, self).__init__() self.core = core - self.shop = ShopWidget(cache_dir) + self.shop = ShopWidget(cache_dir, core) self.browse_games = BrowseGames(cache_dir) self.store_tabs = QTabWidget() diff --git a/rare/components/tabs/shop/constants.py b/rare/components/tabs/shop/constants.py index 53cdbd9a..b3088df4 100644 --- a/rare/components/tabs/shop/constants.py +++ b/rare/components/tabs/shop/constants.py @@ -103,3 +103,5 @@ search_query = "query searchStoreQuery($allowCountries: String, $category: Strin "endDate\n discountSetting {\n discountType\n discountPercentage\n " \ " }\n }\n }\n }\n }\n paging {\n count\n " \ "total\n }\n }\n }\n}\n " + +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" diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py index 04120fbc..ed236631 100644 --- a/rare/components/tabs/shop/game_widgets.py +++ b/rare/components/tabs/shop/game_widgets.py @@ -2,8 +2,9 @@ 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 +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout from rare.components.tabs.shop.constants import search_query from rare.utils.extra_widgets import ImageLabel @@ -28,7 +29,7 @@ class GameWidget(QWidget): self.image = ImageLabel() self.layout.addWidget(self.image) - self.title_label = QLabel(json_info["title"]) + self.title_label = QLabel(json_info.get("title")) self.title_label.setWordWrap(True) self.layout.addWidget(self.title_label) @@ -75,3 +76,22 @@ class GameWidget(QWidget): data = data["data"]["Catalog"]["searchStore"]["elements"][0] self.init_ui(data) + + +class GameWidgetDiscount(GameWidget): + def __init__(self, *args, **kwargs): + super(GameWidgetDiscount, self).__init__(*args, **kwargs) + + h_layout = QHBoxLayout() + self.layout.addLayout(h_layout) + + price = args[1]['price']['totalPrice']['fmtPrice']['originalPrice'] + discount_price = args[1]['price']['totalPrice']['fmtPrice']['discountPrice'] + + price_label = QLabel(price) + + font = QFont() + font.setStrikeOut(True) + price_label.setFont(font) + h_layout.addWidget(QLabel(discount_price if discount_price != "0" else self.tr("Free"))) + h_layout.addWidget(price_label) diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 2c3e0e52..1d63a84a 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -5,8 +5,9 @@ from PyQt5.QtCore import Qt, pyqtSignal, QStringListModel from PyQt5.QtNetwork import QNetworkAccessManager from PyQt5.QtWidgets import QWidget, QCompleter, QGroupBox, QHBoxLayout, QScrollArea -from rare.components.tabs.shop.constants import search_query -from rare.components.tabs.shop.game_widgets import GameWidget +from custom_legendary.core import LegendaryCore +from rare.components.tabs.shop.constants import search_query, wishlist_query +from rare.components.tabs.shop.game_widgets import GameWidget, GameWidgetDiscount from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.utils.extra_widgets import WaitingSpinner, FlowLayout, ButtonLineEdit from rare.utils.utils import QtRequestManager, get_lang @@ -22,11 +23,12 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): active_search_request = False next_search = "" - def __init__(self, path): + def __init__(self, path, core: LegendaryCore): super(ShopWidget, self).__init__() self.setWidgetResizable(True) self.setupUi(self) self.path = path + self.core = core self.manager = QNetworkAccessManager() self.free_games_widget = QWidget() self.free_games_widget.setLayout(FlowLayout()) @@ -53,23 +55,24 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.search_bar.returnPressed.connect(self.show_search_results) self.search_bar.buttonClicked.connect(self.show_search_results) - self.games_groupbox.setLayout(FlowLayout()) - self.games_groupbox.setVisible(False) - self.search_request_manager = QtRequestManager("json") self.search_request_manager.data_ready.connect(self.set_completer) self.search_bar.textChanged.connect(self.load_completer) + self.wishlist_gb.setLayout(FlowLayout()) + self.wishlist_gb.setVisible(False) + self.locale = get_lang() def load_completer(self, text): if text != "": - locale = get_lang() + payload = { "query": search_query, "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 20, - "country": locale.upper(), "keywords": text, "locale": locale, "sortDir": "DESC", - "allowCountries": locale.upper(), + "country": self.locale.upper(), "keywords": text, "locale": self.locale, + "sortDir": "DESC", + "allowCountries": self.locale.upper(), "start": 0, "tag": "", "withMapping": False, "withPrice": True} } self.search_request_manager.post("https://www.epicgames.com/graphql", payload) @@ -78,9 +81,37 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" self.free_game_request_manager = QtRequestManager("json") self.free_game_request_manager.get(url) - self.free_game_request_manager.data_ready.connect(self.add_free_games) + self.free_game_request_manager.data_ready.connect( + self.add_free_games) + + self.wishlist_manager = QtRequestManager("json") + self.wishlist_manager.data_ready.connect(self.add_wishlist) + + wish_list_url = "https://www.epicgames.com/graphql" + self.wishlist_manager.post(wish_list_url, + payload={ + "query": wishlist_query, + "variables": { + "country": self.locale.upper(), + "locale": self.locale + } + }, + headers={"Authorization": self.core.egs.session.headers["Authorization"]}) + + def add_wishlist(self, wishlist): + wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"] + discounts = 0 + for game in wishlist: + if game["offer"]["price"]["totalPrice"]["discount"] > 0: + w = GameWidgetDiscount(self.path, game["offer"]) + w.show_info.connect(self.show_game.emit) + self.wishlist_gb.layout().addWidget(w) + discounts += 1 + if discounts != 0: + self.wishlist_gb.setVisible(True) def add_free_games(self, free_games): + self.free_game_request_manager.deleteLater() free_games = free_games["data"]["Catalog"]["searchStore"]["elements"] date = datetime.datetime.now() free_games_now = [] diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py index 00b6563a..d27baade 100644 --- a/rare/ui/components/tabs/store/store.py +++ b/rare/ui/components/tabs/store/store.py @@ -33,9 +33,9 @@ class Ui_ShopWidget(object): self.free_games_stack.setObjectName("free_games_stack") self.verticalLayout_2.addWidget(self.free_games_stack) self.verticalLayout_3.addWidget(self.free_game_group_box) - self.games_groupbox = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) - self.games_groupbox.setObjectName("games_groupbox") - self.verticalLayout_3.addWidget(self.games_groupbox) + self.wishlist_gb = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + self.wishlist_gb.setObjectName("wishlist_gb") + self.verticalLayout_3.addWidget(self.wishlist_gb) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_3.addItem(spacerItem) self.scrollArea.setWidget(self.scrollAreaWidgetContents) @@ -49,7 +49,7 @@ class Ui_ShopWidget(object): _translate = QtCore.QCoreApplication.translate ShopWidget.setWindowTitle(_translate("ShopWidget", "Form")) self.free_game_group_box.setTitle(_translate("ShopWidget", "Free Games")) - self.games_groupbox.setTitle(_translate("ShopWidget", "Other nice games")) + self.wishlist_gb.setTitle(_translate("ShopWidget", "Discounts from your wishlist")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/store/store.ui b/rare/ui/components/tabs/store/store.ui index e103aa4d..d215e5a2 100644 --- a/rare/ui/components/tabs/store/store.ui +++ b/rare/ui/components/tabs/store/store.ui @@ -46,10 +46,10 @@ - - - Other nice games - + + + Discounts from your wishlist + diff --git a/rare/utils/utils.py b/rare/utils/utils.py index f0719849..34e88a2d 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -354,12 +354,15 @@ class QtRequestManager(QObject): else: self.next_request = ["", tuple(())] - def post(self, url: str, payload: dict): + def post(self, url: str, payload: dict, headers: dict = None): if not self.request_active: request = QNetworkRequest(QUrl(url)) request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") payload = json.dumps(payload).encode("utf-8") + if headers is not None: + for key, value in headers.items(): + request.setRawHeader(key.encode(), value.encode()) self.request = self.manager.post(request, payload) self.request_active = True From df5da90292d2ec4fd245d6a4ba074b8ffed56e20 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sun, 22 Aug 2021 22:22:17 +0200 Subject: [PATCH 30/30] New api core for store requests --- rare/__main__.py | 5 +- rare/components/tabs/shop/__init__.py | 19 ++- rare/components/tabs/shop/browse_games.py | 32 ++-- rare/components/tabs/shop/constants.py | 2 + .../tabs/shop/{shop_info.py => game_info.py} | 55 +++++-- rare/components/tabs/shop/game_widgets.py | 17 +-- rare/components/tabs/shop/search_results.py | 19 +-- rare/components/tabs/shop/shop_api_core.py | 137 ++++++++++++++++++ rare/components/tabs/shop/shop_models.py | 57 +++++++- rare/components/tabs/shop/shop_widget.py | 47 +++--- .../components/tabs/store/shop_game_info.py | 4 + .../components/tabs/store/shop_game_info.ui | 47 +++--- rare/utils/extra_widgets.py | 11 +- rare/utils/qt_requests.py | 92 ++++++++++++ rare/utils/utils.py | 87 +---------- 15 files changed, 412 insertions(+), 219 deletions(-) rename rare/components/tabs/shop/{shop_info.py => game_info.py} (68%) create mode 100644 rare/components/tabs/shop/shop_api_core.py create mode 100644 rare/utils/qt_requests.py diff --git a/rare/__main__.py b/rare/__main__.py index f582c6ab..6bffb67d 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -8,6 +8,7 @@ from rare.utils import singleton, utils def main(): + # CLI Options parser = ArgumentParser() parser.add_argument("-V", "--version", action="store_true", help="Shows version and exits") parser.add_argument("-S", "--silent", action="store_true", @@ -37,7 +38,7 @@ def main(): print(__version__) exit(0) try: - # this object only allows one instance pre machine + # this object only allows one instance per machine me = singleton.SingleInstance() except singleton.SingleInstanceException: print("Rare is already running") @@ -54,6 +55,8 @@ def main(): if args.subparser == "launch": args.silent = True + # start app + # Import start now, to not import everything from rare.app import start start(args) diff --git a/rare/components/tabs/shop/__init__.py b/rare/components/tabs/shop/__init__.py index 6c7ac533..513264df 100644 --- a/rare/components/tabs/shop/__init__.py +++ b/rare/components/tabs/shop/__init__.py @@ -3,8 +3,9 @@ from PyQt5.QtWidgets import QStackedWidget, QTabWidget from custom_legendary.core import LegendaryCore from rare import cache_dir from rare.components.tabs.shop.browse_games import BrowseGames +from rare.components.tabs.shop.game_info import ShopGameInfo from rare.components.tabs.shop.search_results import SearchResults -from rare.components.tabs.shop.shop_info import ShopGameInfo +from rare.components.tabs.shop.shop_api_core import ShopApiCore from rare.components.tabs.shop.shop_widget import ShopWidget @@ -15,8 +16,10 @@ class Shop(QStackedWidget): super(Shop, self).__init__() self.core = core - self.shop = ShopWidget(cache_dir, core) - self.browse_games = BrowseGames(cache_dir) + self.shop_api = ShopApiCore(self.core.egs.session.headers["Authorization"]) + + self.shop = ShopWidget(cache_dir, core, self.shop_api) + self.browse_games = BrowseGames(cache_dir, self.shop_api) self.store_tabs = QTabWidget() self.store_tabs.addTab(self.shop, self.tr("Games")) @@ -25,24 +28,28 @@ class Shop(QStackedWidget): self.addWidget(self.store_tabs) - self.search_results = SearchResults() + self.search_results = SearchResults(self.shop_api) self.addWidget(self.search_results) self.search_results.show_info.connect(self.show_game_info) - self.info = ShopGameInfo([i.asset_info.namespace for i in self.core.get_game_list(True)]) + self.info = ShopGameInfo([i.asset_info.namespace for i in self.core.get_game_list(True)], self.shop_api) 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.shop.show_game.connect(self.show_game_info) self.browse_games.show_game.connect(self.show_game_info) + self.shop_api.update_wishlist.connect(self.update_wishlist) + + def update_wishlist(self): + self.shop.update_wishlist() def load(self): if not self.init: self.init = True self.shop.load() - # self.browse_games.prepare_request() def show_game_info(self, data): self.info.update_game(data) diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py index ff8df1ee..cb125207 100644 --- a/rare/components/tabs/shop/browse_games.py +++ b/rare/components/tabs/shop/browse_games.py @@ -5,11 +5,12 @@ import random from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QWidget, QCheckBox, QVBoxLayout, QLabel -from rare.components.tabs.shop.constants import game_query, Constants +from rare.components.tabs.shop.constants import Constants from rare.components.tabs.shop.game_widgets import GameWidget +from rare.components.tabs.shop.shop_models import BrowseModel from rare.ui.components.tabs.store.browse_games import Ui_browse_games from rare.utils.extra_widgets import FlowLayout, WaitingSpinner -from rare.utils.utils import get_lang, QtRequestManager +from rare.utils.utils import get_lang logger = logging.getLogger("BrowseGames") @@ -21,15 +22,14 @@ class BrowseGames(QWidget, Ui_browse_games): tags = [] types = [] - def __init__(self, path): + def __init__(self, path, api_core): super(BrowseGames, self).__init__() self.setupUi(self) + self.api_core = api_core self.path = path self.games_widget = QWidget() self.games_widget.setLayout(FlowLayout()) self.games.setWidget(self.games_widget) - self.manager = QtRequestManager("json") - self.manager.data_ready.connect(self.show_games) self.stack.addWidget(WaitingSpinner()) @@ -85,30 +85,16 @@ class BrowseGames(QWidget, Ui_browse_games): locale = get_lang() self.stack.setCurrentIndex(2) date = f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%X')}.{str(random.randint(0, 999)).zfill(3)}Z]" - payload = {"variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 30, "country": locale.upper(), "keywords": "", "locale": locale, - "sortDir": "DESC", "allowCountries": locale.upper(), "start": 0, "tag": "", - "withMapping": True, "withPrice": True, - "releaseDate": date, "effectiveDate": date - }, - "query": game_query} - if self.price == "free": - payload["variables"]["freeGame"] = True - elif self.price.startswith(""): - payload["variables"]["priceRange"] = price.replace("", "") - elif self.price == "sale": - payload["variables"]["onSale"] = True - - payload["variables"]["tag"] = "|".join(self.tags) + browse_model = BrowseModel(locale=locale, date=date, count=30, price=self.price) + browse_model.tag = "|".join(self.tags) if self.types: - payload["variables"]["category"] = "|".join(self.types) + browse_model.category = "|".join(self.types) - self.manager.post("https://www.epicgames.com/graphql", payload) + self.api_core.browse_games(browse_model, self.show_games) def show_games(self, data): - data = data["data"]["Catalog"]["searchStore"]["elements"] QWidget().setLayout(self.games_widget.layout()) if data: diff --git a/rare/components/tabs/shop/constants.py b/rare/components/tabs/shop/constants.py index b3088df4..b424fb70 100644 --- a/rare/components/tabs/shop/constants.py +++ b/rare/components/tabs/shop/constants.py @@ -105,3 +105,5 @@ search_query = "query searchStoreQuery($allowCountries: String, $category: Strin "total\n }\n }\n }\n}\n " 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" diff --git a/rare/components/tabs/shop/shop_info.py b/rare/components/tabs/shop/game_info.py similarity index 68% rename from rare/components/tabs/shop/shop_info.py rename to rare/components/tabs/shop/game_info.py index c0e170f2..4e06e502 100644 --- a/rare/components/tabs/shop/shop_info.py +++ b/rare/components/tabs/shop/game_info.py @@ -7,7 +7,7 @@ from PyQt5.QtWidgets import QWidget, QLabel from rare.components.tabs.shop.shop_models import ShopGame from rare.ui.components.tabs.store.shop_game_info import Ui_shop_info from rare.utils.extra_widgets import WaitingSpinner, ImageLabel -from rare.utils.utils import get_lang, QtRequestManager +from rare.utils.utils import get_lang logger = logging.getLogger("ShopInfo") @@ -17,26 +17,43 @@ class ShopGameInfo(QWidget, Ui_shop_info): data: dict # TODO Design - def __init__(self, installed_titles: list): + def __init__(self, installed_titles: list, api_core): super(ShopGameInfo, self).__init__() self.setupUi(self) + 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(WaitingSpinner()) - self.manager = QtRequestManager("json") - self.manager.data_ready.connect(self.data_received) + + self.locale = get_lang() + self.wishlist_button.clicked.connect(self.add_to_wishlist) + self.in_wishlist = False + self.wishlist = [] + + def handle_wishlist_update(self, data): + 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().setParent(None) slug = data["productSlug"] if "/home" in slug: slug = slug.replace("/home", "") self.slug = slug - self.title.setText(data["title"]) + if data["namespace"] in self.installed: self.open_store_button.setText(self.tr("Show Game on Epic Page")) self.owned_label.setVisible(True) @@ -46,7 +63,7 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.dev.setText(self.tr("Loading")) self.price.setText(self.tr("Loading")) - self.title.setText(self.tr("Loading")) + # self.title.setText(self.tr("Loading")) self.image.setPixmap(QPixmap()) self.data = data is_bundle = False @@ -55,10 +72,18 @@ class ShopGameInfo(QWidget, Ui_shop_info): is_bundle = True # init API request - locale = get_lang() - url = f"https://store-content.ak.epicgames.com/api/{locale}/content/{'products' if not is_bundle else 'bundles'}/{slug}" - # game = api_utils.get_product(slug, locale) - self.manager.get(url) + self.api_core.get_game(slug, is_bundle, 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): self.game = ShopGame.from_json(game, self.data) @@ -103,9 +128,6 @@ class ShopGameInfo(QWidget, Ui_shop_info): self.image.update_image(self.game.image_urls.front_tall, self.game.title, (240, 320)) self.image_stack.setCurrentIndex(0) - # self.image_request = self.manager.get(QNetworkRequest(QUrl(self.game.image_urls.offer_image_tall))) - # self.image_request.finished.connect(self.image_loaded) - try: if isinstance(self.game.developer, list): self.dev.setText(", ".join(self.game.developer)) @@ -114,7 +136,12 @@ class ShopGameInfo(QWidget, Ui_shop_info): except KeyError: pass self.tags.setText(", ".join(self.game.tags)) - # self.price.setText(self.game.price) + self.price.setText(self.game.price) + + 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): webbrowser.open("https://www.epicgames.com/store/de/p/" + self.slug) diff --git a/rare/components/tabs/shop/game_widgets.py b/rare/components/tabs/shop/game_widgets.py index ed236631..6d5ef422 100644 --- a/rare/components/tabs/shop/game_widgets.py +++ b/rare/components/tabs/shop/game_widgets.py @@ -6,9 +6,7 @@ from PyQt5.QtGui import QFont from PyQt5.QtNetwork import QNetworkAccessManager from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout -from rare.components.tabs.shop.constants import search_query from rare.utils.extra_widgets import ImageLabel -from rare.utils.utils import get_lang, QtRequestManager logger = logging.getLogger("GameWidgets") @@ -56,20 +54,9 @@ class GameWidget(QWidget): self.show_info.emit(self.json_info) @classmethod - def from_request(cls, name, path): + def from_request(cls, name, path, shop_api): c = cls(path) - c.manager = QtRequestManager("json") - c.manager.data_ready.connect(c.handle_response) - locale = get_lang() - payload = { - "query": search_query, - "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 1, - "country": "DE", "keywords": name, "locale": locale, "sortDir": "DESC", - "allowCountries": locale.upper(), - "start": 0, "tag": "", "withMapping": False, "withPrice": True} - } - c.manager.post("https://www.epicgames.com/graphql", payload) - c.search_request.da.connect(lambda: c.handle_response(path)) + shop_api.search_game(name, c.handle_response) return c def handle_response(self, data): diff --git a/rare/components/tabs/shop/search_results.py b/rare/components/tabs/shop/search_results.py index a8026a22..3585610b 100644 --- a/rare/components/tabs/shop/search_results.py +++ b/rare/components/tabs/shop/search_results.py @@ -4,17 +4,16 @@ from PyQt5.QtGui import QFont from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QGroupBox, QPushButton, \ QStackedWidget -from rare.components.tabs.shop.constants import search_query from rare.utils.extra_widgets import ImageLabel, FlowLayout, WaitingSpinner -from rare.utils.utils import QtRequestManager, get_lang class SearchResults(QStackedWidget): show_info = pyqtSignal(dict) - def __init__(self): + 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")) @@ -30,9 +29,6 @@ class SearchResults(QStackedWidget): self.layout = FlowLayout() self.widget.setLayout(self.layout) - self.search_manager = QtRequestManager("json") - self.search_manager.data_ready.connect(self.show_results) - self.search_result_widget.setLayout(self.main_layout) self.addWidget(WaitingSpinner()) @@ -41,16 +37,7 @@ class SearchResults(QStackedWidget): def load_results(self, text: str): self.setCurrentIndex(1) if text != "": - locale = get_lang() - payload = { - "query": search_query, - "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", - "count": 20, - "country": locale.upper(), "keywords": text, "locale": locale, "sortDir": "DESC", - "allowCountries": locale.upper(), - "start": 0, "tag": "", "withMapping": False, "withPrice": True} - } - self.search_manager.post("https://www.epicgames.com/graphql", payload) + self.api_core.search_game(text, self.show_results) def show_results(self, results: dict): results = results["data"]["Catalog"]["searchStore"]["elements"] diff --git a/rare/components/tabs/shop/shop_api_core.py b/rare/components/tabs/shop/shop_api_core.py new file mode 100644 index 00000000..516dd79f --- /dev/null +++ b/rare/components/tabs/shop/shop_api_core.py @@ -0,0 +1,137 @@ +from logging import getLogger + +from PyQt5.QtCore import pyqtSignal, QObject + +from rare.components.tabs.shop.constants import wishlist_query, search_query, game_query, add_to_wishlist_query, \ + remove_from_wishlist_query +from rare.components.tabs.shop.shop_models import BrowseModel +from rare.utils.qt_requests import QtRequestManager +from rare.utils.utils import get_lang + +logger = getLogger("ShopAPICore") +graphql_url = "https://www.epicgames.com/graphql" + + +class ShopApiCore(QObject): + update_wishlist = pyqtSignal() + + def __init__(self, auth_token): + super(ShopApiCore, self).__init__() + self.token = auth_token + self.manager = QtRequestManager() + self.auth_manager = QtRequestManager(authorization_token=auth_token) + self.locale = get_lang() + + self.browse_active = False + self.next_browse_request = tuple(()) + + def get_free_games(self, handle_func: callable): + url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" + + self.manager.get(url, lambda data: self._handle_free_games(data, handle_func)) + + def _handle_free_games(self, data, handle_func): + try: + handle_func(data["data"]["Catalog"]["searchStore"]["elements"]) + except KeyError as e: + logger.error(str(e)) + + def get_wishlist(self, handle_func): + self.auth_manager.post(graphql_url, { + "query": wishlist_query, + "variables": { + "country": self.locale.upper(), + "locale": self.locale + } + }, lambda data: self._handle_wishlist(data, handle_func)) + + def _handle_wishlist(self, data, handle_func): + handle_func(data["data"]["Wishlist"]["wishlistItems"]["elements"]) + + def search_game(self, name, handle_func): + payload = { + "query": search_query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 1, + "country": "DE", "keywords": name, "locale": self.locale, "sortDir": "DESC", + "allowCountries": self.locale.upper(), + "start": 0, "tag": "", "withMapping": False, "withPrice": True} + } + + self.manager.post(graphql_url, payload, lambda data: self._handle_search(data, handle_func)) + + def _handle_search(self, data, handle_func): + handle_func(data) + + 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 + payload = { + "variables": browse_model.__dict__, + "query": game_query + } + + self.auth_manager.post(graphql_url, payload, lambda data: self._handle_browse_games(data, handle_func)) + + def _handle_browse_games(self, data, handle_func): + self.browse_active = False + if not self.next_browse_request: + handle_func(data["data"]["Catalog"]["searchStore"]["elements"]) + else: + self.browse_games(*self.next_browse_request) + 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): + handle_func(data) + + def add_to_wishlist(self, namespace, offer_id, handle_func: callable): + payload = { + "variables": { + "offerId": offer_id, + "namespace": namespace, + "country": self.locale.upper(), + "locale": self.locale + }, + "query": add_to_wishlist_query + } + self.auth_manager.post(graphql_url, payload, lambda data: self._handle_add_to_wishlist(data, handle_func)) + + 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, payload, lambda data: self._handle_remove_from_wishlist(data, handle_func)) + + 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/shop/shop_models.py b/rare/components/tabs/shop/shop_models.py index 8ad6da61..2d6e2b83 100644 --- a/rare/components/tabs/shop/shop_models.py +++ b/rare/components/tabs/shop/shop_models.py @@ -1,3 +1,10 @@ +import datetime +import random +from dataclasses import dataclass + +from rare.utils.utils import get_lang + + class _ImageUrlModel: def __init__(self, front_tall: str = "", offer_image_tall: str = "", thumbnail: str = "", front_wide: str = ""): @@ -25,7 +32,8 @@ 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): + original_price: str = "", discount_price: str = "", tags: list = None, namespace: str = "", + offer_id: str = ""): self.title = title self.image_urls = image_urls self.links = [] @@ -42,6 +50,8 @@ class ShopGame: 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): @@ -50,10 +60,9 @@ class ShopGame: if product["_title"] == "home": api_data = product break - - tmp = cls() if "pages" in api_data.keys(): api_data = api_data["pages"][0] + tmp = cls() tmp.title = search_data.get("title", "Fail") tmp.image_urls = _ImageUrlModel.from_json(search_data["keyImages"]) links = api_data["data"]["socialLinks"] @@ -82,5 +91,47 @@ class ShopGame: 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 + locale: str = get_lang() + 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%X')}.{str(random.randint(0, 999)).zfill(3)}Z]" + price: str = "" + + @property + def __dict__(self): + payload = {"category": self.category, + "count": self.count, + "country": self.locale.upper(), + "keywords": self.keywords, + "locale": self.locale, + "sortDir": self.sortDir, + "allowCountries": self.locale.upper(), + "start": self.start, + "tag": self.tag, + "withMapping": self.withMapping, + "withPrice": self.withPrice, + "releaseDate": self.date, + "effectiveDate": self.date, + } + + if self.price == "free": + payload["freeGame"] = True + elif self.price.startswith(""): + payload["priceRange"] = self.price.replace("", "") + elif self.price == "sale": + payload["onSale"] = True + return payload diff --git a/rare/components/tabs/shop/shop_widget.py b/rare/components/tabs/shop/shop_widget.py index 1d63a84a..45150592 100644 --- a/rare/components/tabs/shop/shop_widget.py +++ b/rare/components/tabs/shop/shop_widget.py @@ -6,11 +6,12 @@ from PyQt5.QtNetwork import QNetworkAccessManager from PyQt5.QtWidgets import QWidget, QCompleter, QGroupBox, QHBoxLayout, QScrollArea from custom_legendary.core import LegendaryCore -from rare.components.tabs.shop.constants import search_query, wishlist_query +from rare.components.tabs.shop import ShopApiCore +from rare.components.tabs.shop.constants import search_query from rare.components.tabs.shop.game_widgets import GameWidget, GameWidgetDiscount from rare.ui.components.tabs.store.store import Ui_ShopWidget from rare.utils.extra_widgets import WaitingSpinner, FlowLayout, ButtonLineEdit -from rare.utils.utils import QtRequestManager, get_lang +from rare.utils.utils import get_lang logger = logging.getLogger("Shop") @@ -22,13 +23,15 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): free_game_widgets = [] active_search_request = False next_search = "" + wishlist: list = [] - def __init__(self, path, core: LegendaryCore): + 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.shop_api = shop_api self.manager = QNetworkAccessManager() self.free_games_widget = QWidget() self.free_games_widget.setLayout(FlowLayout()) @@ -55,10 +58,7 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.search_bar.returnPressed.connect(self.show_search_results) self.search_bar.buttonClicked.connect(self.show_search_results) - self.search_request_manager = QtRequestManager("json") - self.search_request_manager.data_ready.connect(self.set_completer) - - self.search_bar.textChanged.connect(self.load_completer) + # self.search_bar.textChanged.connect(self.load_completer) self.wishlist_gb.setLayout(FlowLayout()) self.wishlist_gb.setVisible(False) self.locale = get_lang() @@ -78,28 +78,18 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): self.search_request_manager.post("https://www.epicgames.com/graphql", payload) def load(self): - url = "https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions" - self.free_game_request_manager = QtRequestManager("json") - self.free_game_request_manager.get(url) - self.free_game_request_manager.data_ready.connect( - self.add_free_games) + # load free games + self.shop_api.get_free_games(self.add_free_games) + # load wishlist + self.shop_api.get_wishlist(self.add_wishlist_items) - self.wishlist_manager = QtRequestManager("json") - self.wishlist_manager.data_ready.connect(self.add_wishlist) + def update_wishlist(self): + self.shop_api.get_wishlist(self.add_wishlist_items) - wish_list_url = "https://www.epicgames.com/graphql" - self.wishlist_manager.post(wish_list_url, - payload={ - "query": wishlist_query, - "variables": { - "country": self.locale.upper(), - "locale": self.locale - } - }, - headers={"Authorization": self.core.egs.session.headers["Authorization"]}) + def add_wishlist_items(self, wishlist): + QWidget().setLayout(self.wishlist_gb.layout()) - def add_wishlist(self, wishlist): - wishlist = wishlist["data"]["Wishlist"]["wishlistItems"]["elements"] + self.wishlist_gb.setLayout(FlowLayout()) discounts = 0 for game in wishlist: if game["offer"]["price"]["totalPrice"]["discount"] > 0: @@ -107,12 +97,9 @@ class ShopWidget(QScrollArea, Ui_ShopWidget): w.show_info.connect(self.show_game.emit) self.wishlist_gb.layout().addWidget(w) discounts += 1 - if discounts != 0: - self.wishlist_gb.setVisible(True) + self.wishlist_gb.setVisible(discounts > 0) def add_free_games(self, free_games): - self.free_game_request_manager.deleteLater() - free_games = free_games["data"]["Catalog"]["searchStore"]["elements"] date = datetime.datetime.now() free_games_now = [] coming_free_games = [] diff --git a/rare/ui/components/tabs/store/shop_game_info.py b/rare/ui/components/tabs/store/shop_game_info.py index 61c99695..80a089f5 100644 --- a/rare/ui/components/tabs/store/shop_game_info.py +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -51,6 +51,9 @@ class Ui_shop_info(object): self.open_store_button = QtWidgets.QPushButton(shop_info) self.open_store_button.setObjectName("open_store_button") self.verticalLayout_2.addWidget(self.open_store_button) + self.wishlist_button = QtWidgets.QPushButton(shop_info) + 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) @@ -80,6 +83,7 @@ class Ui_shop_info(object): self.discount_price.setText(_translate("shop_info", "TextLabel")) self.tags.setText(_translate("shop_info", "TextLabel")) 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")) diff --git a/rare/ui/components/tabs/store/shop_game_info.ui b/rare/ui/components/tabs/store/shop_game_info.ui index a928b083..82437277 100644 --- a/rare/ui/components/tabs/store/shop_game_info.ui +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -83,26 +83,33 @@ - - - - Buy Game in Epic Games Store - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - + + + + Buy Game in Epic Games Store + + + + + + + Add to wishlist + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index dcbbd243..90fc8bec 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -11,7 +11,7 @@ from qtawesome import icon from rare import style_path, cache_dir from rare.ui.utils.pathedit import Ui_PathEdit -from rare.utils.utils import QtRequestManager +from rare.utils.qt_requests import QtRequestManager logger = getLogger("ExtraWidgets") @@ -264,7 +264,6 @@ class ImageLabel(QLabel): super(ImageLabel, self).__init__() self.path = cache_dir self.manager = QtRequestManager("bytes") - self.manager.data_ready.connect(self.image_ready) def update_image(self, url, name, size: tuple = (240, 320)): self.setFixedSize(*size) @@ -278,14 +277,16 @@ class ImageLabel(QLabel): name_extension = "tall" self.name = f"{self.name}_{name_extension}.png" if not os.path.exists(os.path.join(self.path, self.name)): - self.manager.get(url) + self.manager.get(url, self.image_ready) # self.request.finished.connect(self.image_ready) else: self.show_image() def image_ready(self, data): - self.setPixmap(QPixmap()) - + try: + self.setPixmap(QPixmap()) + except RuntimeError: + return image: Image.Image = Image.open(io.BytesIO(data)) image = image.resize((self.img_size[0], self.img_size[1])) diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py new file mode 100644 index 00000000..10d3560b --- /dev/null +++ b/rare/utils/qt_requests.py @@ -0,0 +1,92 @@ +import json +from dataclasses import dataclass +from logging import getLogger + +from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QJsonParseError, QJsonDocument +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply + +logger = getLogger("QtRequests") + + +class QtRequestManager(QObject): + data_ready = pyqtSignal(object) + request = None + request_active = None + + def __init__(self, type: str = "json", authorization_token: str = None): + super(QtRequestManager, self).__init__() + self.manager = QNetworkAccessManager() + self.type = type + self.authorization_token = authorization_token + self.request_queue = [] + + def post(self, url: str, payload: dict, handle_func): + if not self.request_active: + request = QNetworkRequest(QUrl(url)) + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") + self.request_active = RequestQueueItem(handle_func=handle_func) + payload = json.dumps(payload).encode("utf-8") + + request.setHeader(QNetworkRequest.UserAgentHeader, + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36") + + if self.authorization_token is not None: + request.setRawHeader(b"Authorization", self.authorization_token.encode()) + + self.request = self.manager.post(request, payload) + self.request.finished.connect(self.prepare_data) + + else: + self.request_queue.append( + RequestQueueItem(method="post", url=url, payload=payload, handle_func=handle_func)) + + def get(self, url: str, handle_func: callable): + if not self.request_active: + request = QNetworkRequest(QUrl(url)) + request.setHeader(QNetworkRequest.UserAgentHeader, + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36") + + self.request_active = RequestQueueItem(handle_func=handle_func) + self.request = self.manager.get(request) + + self.request.finished.connect(self.prepare_data) + else: + self.request_queue.append(RequestQueueItem(method="get", url=url, handle_func=handle_func)) + + def prepare_data(self): + # self.request_active = False + data = {} if self.type == "json" else b"" + if self.request: + try: + if self.request.error() == QNetworkReply.NoError: + if self.type == "json": + error = QJsonParseError() + json_data = QJsonDocument.fromJson(self.request.readAll().data(), error) + if QJsonParseError.NoError == error.error: + data = json.loads(json_data.toJson().data().decode()) + + else: + logger.error(error.errorString()) + else: + data = self.request.readAll().data() + + except RuntimeError as e: + logger.error(str(e)) + self.request_active.handle_func(data) + self.request.deleteLater() + self.request_active = None + + if self.request_queue: + if self.request_queue[0].method == "post": + self.post(self.request_queue[0].url, self.request_queue[0].payload, self.request_queue[0].handle_func) + else: + self.get(self.request_queue[0].url, self.request_queue[0].handle_func) + self.request_queue.pop(0) + + +@dataclass +class RequestQueueItem: + method: str = None + url: str = None + handle_func: callable = None + payload: dict = None diff --git a/rare/utils/utils.py b/rare/utils/utils.py index 34e88a2d..86672dd2 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -6,9 +6,8 @@ from logging import getLogger import requests from PIL import Image, UnidentifiedImageError -from PyQt5.QtCore import pyqtSignal, QLocale, QSettings, QObject, QUrl, QJsonParseError, QJsonDocument +from PyQt5.QtCore import pyqtSignal, QLocale, QSettings from PyQt5.QtGui import QPalette, QColor -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply # Windows if os.name == "nt": @@ -338,87 +337,3 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop"): shortcut.save() - -class QtRequestManager(QObject): - data_ready = pyqtSignal(object) - request = None - request_active = False - - def __init__(self, type: str = "json", queue=False): - super(QtRequestManager, self).__init__() - self.manager = QNetworkAccessManager() - self.type = type - self.queue = queue - if self.queue: - self.next_request = [] - else: - self.next_request = ["", tuple(())] - - def post(self, url: str, payload: dict, headers: dict = None): - if not self.request_active: - request = QNetworkRequest(QUrl(url)) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") - - payload = json.dumps(payload).encode("utf-8") - if headers is not None: - for key, value in headers.items(): - request.setRawHeader(key.encode(), value.encode()) - - self.request = self.manager.post(request, payload) - self.request_active = True - self.request.finished.connect(self.prepare_data) - - else: - if self.queue: - self.next_request.append(["post", (url, payload)]) - else: - self.next_request = ["post", (url, payload)] - - def get(self, url: str): - if not self.request_active: - self.request_active = True - self.request = self.manager.get(QNetworkRequest(QUrl(url))) - self.request.finished.connect(self.prepare_data) - else: - if self.queue: - self.next_request.append(["get", (url,)]) - else: - self.next_request = ["get", (url,)] - - def prepare_data(self): - self.request_active = False - data = {} if self.type == "json" else b"" - if self.request: - try: - if self.request.error() == QNetworkReply.NoError: - if self.type == "json": - error = QJsonParseError() - json_data = QJsonDocument.fromJson(self.request.readAll().data(), error) - if QJsonParseError.NoError == error.error: - data = json.loads(json_data.toJson().data().decode()) - - else: - logger.error(error.errorString()) - else: - data = self.request.readAll().data() - - except RuntimeError as e: - logger.error(str(e)) - self.data_ready.emit(data) - self.request.deleteLater() - - if self.queue: - if self.next_request: - if self.next_request[0][0] == "post": - self.post(*self.next_request[0][1]) - else: - self.get(self.next_request[0][1][0]) - self.next_request.pop(0) - else: - if method := self.next_request[0]: - if method == "post": - self.post(*self.next_request[1]) - self.next_request = ["", ()] - else: - self.get(self.next_request[1][0]) - self.next_request = ["", ()]