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/__init__.py b/rare/__init__.py index cc439139..2d9d49f9 100644 --- a/rare/__init__.py +++ b/rare/__init__.py @@ -12,6 +12,7 @@ elif os.name == "nt": cache_dir = os.path.expandvars("%APPDATA%/rare/cache") else: cache_dir = os.path.expanduser("~/.cache/rare/") + if not os.path.exists(cache_dir): os.makedirs(cache_dir) @@ -26,3 +27,4 @@ if not os.path.exists(data_dir): os.makedirs(data_dir) image_dir = os.path.join(data_dir, "images") + diff --git a/rare/__main__.py b/rare/__main__.py index de841916..6bffb67d 100644 --- a/rare/__main__.py +++ b/rare/__main__.py @@ -3,15 +3,17 @@ import os from argparse import ArgumentParser -from rare import __version__ +from rare import __version__, data_dir 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", 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") parser.add_argument("--desktop-shortcut", action="store_true", dest="desktop_shortcut", @@ -36,12 +38,12 @@ 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") - 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: @@ -53,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/app.py b/rare/app.py index f47eef12..ed2457ea 100644 --- a/rare/app.py +++ b/rare/app.py @@ -10,21 +10,24 @@ from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QStyleFactory from custom_legendary.core import LegendaryCore from rare import languages_path, resources_path + 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)) - -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/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index d2a52091..318568e1 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -7,6 +7,7 @@ from requests.exceptions import ConnectionError from custom_legendary.core import LegendaryCore from rare import image_dir + from rare.components.dialogs.login import LoginDialog from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog from rare.utils.utils import download_images @@ -26,6 +27,7 @@ class ImageThread(QThread): self.download_progess.emit(100) + class LaunchDialog(QDialog, Ui_LaunchDialog): quit_app = pyqtSignal(int) start_app = pyqtSignal(bool) @@ -66,6 +68,7 @@ class LaunchDialog(QDialog, Ui_LaunchDialog): if not os.path.exists(image_dir): os.makedirs(image_dir) + if not self.offline: self.image_info.setText(self.tr("Downloading Images")) self.image_thread = ImageThread(self.core, self) diff --git a/rare/components/main_window.py b/rare/components/main_window.py index dbc86e53..ef6d18e0 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 @@ -59,7 +60,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/tab_widget.py b/rare/components/tab_widget.py index 6f14e0e5..af676954 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 @@ -13,6 +11,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 @@ -23,32 +22,26 @@ 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)) - # 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.core) + 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) @@ -105,11 +98,17 @@ 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() + # TODO; maybe pass InstallOptionsModel only, not split arguments def install_game(self, options: InstallOptionsModel, update=False, silent=False): + install_dialog = InstallDialog(self.core, InstallQueueItemModel(options=options), update=update, silent=silent, parent=self) @@ -155,8 +154,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/games/game_info/__init__.py b/rare/components/tabs/games/game_info/__init__.py index 6587e78d..42b9c195 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 @@ -72,6 +73,18 @@ class GameInfo(QWidget, Ui_GameInfo): self.setupUi(self) self.core = core + + self.ratings = {"platinum": self.tr("Platinum"), + "gold": self.tr("Gold"), + "silver": self.tr("Silver"), + "bronze": self.tr("Bronze"), + "fail": self.tr("Could not get grade"), + "pending": self.tr("Not enough reports")} + if os.path.exists(p := os.path.join(data_dir, "game_list.json")): + self.grade_table = json.load(open(p)) + else: + self.grade_table = {} + if platform.system() == "Windows": self.lbl_grade.setVisible(False) self.grade.setVisible(False) 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 0d149eaa..168143b4 100644 --- a/rare/components/tabs/games/game_info/uninstalled_info.py +++ b/rare/components/tabs/games/game_info/uninstalled_info.py @@ -9,6 +9,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 @@ -66,6 +67,7 @@ class UninstalledInfo(QWidget, Ui_GameInfo): self.steam_worker.rating_signal.connect(self.grade.setText) if platform.system() == "Windows": + self.lbl_grade.setVisible(False) self.grade.setVisible(False) diff --git a/rare/components/tabs/games/game_list.py b/rare/components/tabs/games/game_list.py index 36119932..b5d09591 100644 --- a/rare/components/tabs/games/game_list.py +++ b/rare/components/tabs/games/game_list.py @@ -5,6 +5,7 @@ from PyQt5.QtCore import Qt, pyqtSignal, QSettings, QTimer 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 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 6c46941d..46e80148 100644 --- a/rare/components/tabs/games/game_widgets/base_installed_widget.py +++ b/rare/components/tabs/games/game_widgets/base_installed_widget.py @@ -7,6 +7,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.components.extra.Console import ConsoleWindow from rare.utils import legendary_utils @@ -150,8 +151,10 @@ class BaseInstalledWidget(QGroupBox): def stderr(self): stderr = bytes(self.proc.readAllStandardError()).decode("utf-8", errors="ignore") print(stderr) + logger.error(stderr) # QMessageBox.warning(self, "Warning", stderr + "\nSee ~/.cache/rare/logs/") + def finished(self, exit_code): logger.info("Game exited with exit code: " + str(exit_code)) self.finish_signal.emit(self.game.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/settings/rare.py b/rare/components/tabs/settings/rare.py index ee609981..e5ac84df 100644 --- a/rare/components/tabs/settings/rare.py +++ b/rare/components/tabs/settings/rare.py @@ -6,12 +6,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") @@ -36,17 +36,18 @@ class RareSettings(QWidget, Ui_RareSettings): (self.auto_sync_cloud, "auto_sync_cloud", True), (self.notification, "notification", True), (self.save_size, "save_size", False), - (self.log_games, "show_console", False) + (self.log_games, "show_console", False), + (self.image_cache, "cache_images", True) ] 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]) @@ -90,6 +91,7 @@ class RareSettings(QWidget, Ui_RareSettings): ) if platform.system() == "Linux": + self.desktop_file = os.path.expanduser("~/Desktop/Rare.desktop") self.start_menu_link = os.path.expanduser("~/.local/share/applications/Rare.desktop") elif platform.system() == "Windows": @@ -110,7 +112,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): @@ -121,8 +123,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 new file mode 100644 index 00000000..513264df --- /dev/null +++ b/rare/components/tabs/shop/__init__.py @@ -0,0 +1,60 @@ +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_api_core import ShopApiCore +from rare.components.tabs.shop.shop_widget import ShopWidget + + +class Shop(QStackedWidget): + init = False + + def __init__(self, core: LegendaryCore): + super(Shop, self).__init__() + self.core = core + + 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")) + 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) + + 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.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() + + def show_game_info(self, data): + self.info.update_game(data) + self.setCurrentIndex(2) + + def show_search_results(self, text: str): + self.search_results.load_results(text) + self.setCurrentIndex(1) diff --git a/rare/components/tabs/shop/browse_games.py b/rare/components/tabs/shop/browse_games.py new file mode 100644 index 00000000..cb125207 --- /dev/null +++ b/rare/components/tabs/shop/browse_games.py @@ -0,0 +1,130 @@ +import datetime +import logging +import random + +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QWidget, QCheckBox, QVBoxLayout, QLabel + +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 + +logger = logging.getLogger("BrowseGames") + + +class BrowseGames(QWidget, Ui_browse_games): + show_game = pyqtSignal(dict) + init = False + price = "" + tags = [] + types = [] + + 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.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) + + constants = Constants() + + 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 load(self): + 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 = ""): + + if price is not None: + self.price = price + if added_tag != 0: + self.tags.append(added_tag) + if removed_tag != 0 and removed_tag in self.tags: + self.tags.remove(removed_tag) + + if added_type: + self.types.append(added_type) + if removed_type and removed_type in self.types: + self.types.remove(removed_type) + + 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]" + + browse_model = BrowseModel(locale=locale, date=date, count=30, price=self.price) + browse_model.tag = "|".join(self.tags) + + if self.types: + browse_model.category = "|".join(self.types) + + self.api_core.browse_games(browse_model, self.show_games) + + def show_games(self, data): + QWidget().setLayout(self.games_widget.layout()) + + if data: + self.games_widget.setLayout(FlowLayout()) + + 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) + + +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..b424fb70 --- /dev/null +++ b/rare/components/tabs/shop/constants.py @@ -0,0 +1,109 @@ +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 " + +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 " + +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/game_info.py b/rare/components/tabs/shop/game_info.py new file mode 100644 index 00000000..4e06e502 --- /dev/null +++ b/rare/components/tabs/shop/game_info.py @@ -0,0 +1,147 @@ +import logging +import webbrowser + +from PyQt5.QtGui import QPixmap, QFont +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 + +logger = logging.getLogger("ShopInfo") + + +class ShopGameInfo(QWidget, Ui_shop_info): + game: ShopGame + data: dict + + # TODO Design + def __init__(self, installed_titles: list, api_core): + super(ShopGameInfo, self).__init__() + self.setupUi(self) + self.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.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 + + 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.price.setText(self.tr("Loading")) + # self.title.setText(self.tr("Loading")) + self.image.setPixmap(QPixmap()) + self.data = data + is_bundle = False + for i in data["categories"]: + if "bundles" in i.get("path", ""): + is_bundle = True + + # init API request + 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) + 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: + 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) + # 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) + + 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)) + + self.image_stack.setCurrentIndex(0) + try: + if isinstance(self.game.developer, list): + self.dev.setText(", ".join(self.game.developer)) + else: + self.dev.setText(self.game.developer) + except KeyError: + pass + self.tags.setText(", ".join(self.game.tags)) + 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 new file mode 100644 index 00000000..6d5ef422 --- /dev/null +++ b/rare/components/tabs/shop/game_widgets.py @@ -0,0 +1,84 @@ +import logging + +from PyQt5 import QtGui +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtGui import QFont +from PyQt5.QtNetwork import QNetworkAccessManager +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout + +from rare.utils.extra_widgets import ImageLabel + +logger = logging.getLogger("GameWidgets") + + +class GameWidget(QWidget): + show_info = pyqtSignal(dict) + + def __init__(self, path, json_info=None, width=300): + super(GameWidget, self).__init__() + self.manager = QNetworkAccessManager() + self.width = width + self.path = path + if json_info: + self.init_ui(json_info) + + def init_ui(self, json_info): + self.layout = QVBoxLayout() + self.image = ImageLabel() + self.layout.addWidget(self.image) + + self.title_label = QLabel(json_info.get("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"] + + 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: + logger.info(", ".join([img["type"] for img in json_info["keyImages"]])) + # print(json_info["keyImages"]) + + 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, shop_api): + c = cls(path) + shop_api.search_game(name, c.handle_response) + return c + + def handle_response(self, data): + + 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/search_results.py b/rare/components/tabs/shop/search_results.py new file mode 100644 index 00000000..3585610b --- /dev/null +++ b/rare/components/tabs/shop/search_results.py @@ -0,0 +1,103 @@ +from PyQt5 import QtGui +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QGroupBox, QPushButton, \ + QStackedWidget + +from rare.utils.extra_widgets import ImageLabel, FlowLayout, WaitingSpinner + + +class SearchResults(QStackedWidget): + show_info = pyqtSignal(dict) + + def __init__(self, api_core): + super(SearchResults, self).__init__() + self.search_result_widget = QWidget() + self.api_core = api_core + self.addWidget(self.search_result_widget) + self.main_layout = QVBoxLayout() + self.back_button = QPushButton(self.tr("Back")) + self.main_layout.addWidget(self.back_button) + self.main_layout.addWidget(self.back_button) + self.result_area = QScrollArea() + self.widget = QWidget() + self.result_area.setWidgetResizable(True) + self.main_layout.addWidget(self.result_area) + self.result_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + + self.result_area.setWidget(self.widget) + self.layout = FlowLayout() + self.widget.setLayout(self.layout) + + self.search_result_widget.setLayout(self.main_layout) + + self.addWidget(WaitingSpinner()) + self.setCurrentIndex(1) + + def load_results(self, text: str): + self.setCurrentIndex(1) + if text != "": + self.api_core.search_game(text, self.show_results) + + def show_results(self, results: dict): + results = results["data"]["Catalog"]["searchStore"]["elements"] + QVBoxLayout().addWidget(self.widget) + self.widget = QWidget() + self.layout = FlowLayout() + if not results: + self.layout.addWidget(QLabel(self.tr("No results found"))) + else: + for res in results: + w = _SearchResultItem(res) + w.show_info.connect(self.show_info.emit) + self.layout.addWidget(w) + self.widget.setLayout(self.layout) + self.result_area.setWidget(self.widget) + self.setCurrentIndex(0) + + +class _SearchResultItem(QGroupBox): + res: dict + show_info = pyqtSignal(dict) + + def __init__(self, result: dict): + super(_SearchResultItem, self).__init__() + self.layout = QVBoxLayout() + self.image = ImageLabel() + for img in result["keyImages"]: + if img["type"] == "DieselStoreFrontTall": + width = 240 + self.image.update_image(img["url"], result["title"], (width, 360)) + break + else: + print("No image found") + self.layout.addWidget(self.image) + + self.res = result + self.title = QLabel(self.res["title"]) + title_font = QFont() + title_font.setPixelSize(15) + self.title.setFont(title_font) + self.title.setWordWrap(True) + self.layout.addWidget(self.title) + price = result['price']['totalPrice']['fmtPrice']['originalPrice'] + discount_price = result['price']['totalPrice']['fmtPrice']['discountPrice'] + price_layout = QHBoxLayout() + price_label = QLabel(price if price != "0" else self.tr("Free")) + price_layout.addWidget(price_label) + + if price != discount_price: + font = QFont() + font.setStrikeOut(True) + price_label.setFont(font) + price_layout.addWidget(QLabel(discount_price)) + # self.discount_price = QLabel(f"{self.tr('Discount price: ')}{discount_price}") + self.layout.addLayout(price_layout) + + self.setLayout(self.layout) + + self.setFixedWidth(260) + + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + if a0.button() == 1: + self.show_info.emit(self.res) diff --git a/rare/components/tabs/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 new file mode 100644 index 00000000..2d6e2b83 --- /dev/null +++ b/rare/components/tabs/shop/shop_models.py @@ -0,0 +1,137 @@ +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 = ""): + 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: dict = None, publisher: str = "", developer: str = "", + original_price: str = "", discount_price: str = "", tags: list = None, namespace: str = "", + offer_id: str = ""): + self.title = title + self.image_urls = image_urls + self.links = [] + if social_links: + for item in social_links: + if item.startswith("link"): + self.links.append(tuple((item.replace("link", ""), social_links[item]))) + else: + self.links = [] + self.languages = langs + self.reqs = reqs + self.publisher = publisher + self.developer = developer + self.price = original_price + self.discount_price = discount_price + self.tags = tags + self.namespace = namespace + self.offer_id = offer_id + + @classmethod + def from_json(cls, api_data: dict, search_data: dict): + if isinstance(api_data, list): + for product in api_data: + if product["_title"] == "home": + api_data = product + break + if "pages" in api_data.keys(): + 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"] + tmp.links = [] + for item in links: + if item.startswith("link"): + tmp.links.append(tuple((item.replace("link", ""), links[item]))) + tmp.available_voice_langs = api_data["data"]["requirements"].get("languages", "Failed") + tmp.reqs = {} + for i, system in enumerate(api_data["data"]["requirements"].get("systems", [])): + try: + tmp.reqs[system["systemType"]] = {} + except KeyError: + continue + for req in system["details"]: + try: + tmp.reqs[system["systemType"]][req["title"]] = (req["minimum"], req["recommended"]) + except KeyError: + pass + tmp.publisher = api_data["data"]["meta"].get("publisher", "") + tmp.developer = api_data["data"]["meta"].get("developer", "") + if not tmp.developer: + for i in search_data["customAttributes"]: + if i["key"] == "developerName": + tmp.developer = i["value"] + tmp.price = search_data['price']['totalPrice']['fmtPrice']['originalPrice'] + tmp.discount_price = search_data['price']['totalPrice']['fmtPrice']['discountPrice'] + tmp.tags = [i.replace("_", " ").capitalize() for i in api_data["data"]["meta"].get("tags", [])] + tmp.namespace = search_data["namespace"] + tmp.offer_id = search_data["id"] + + return tmp + + +@dataclass +class BrowseModel: + category: str = "games/edition/base|bundles/games|editors|software/edition/base" + count: int = 30 + 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 new file mode 100644 index 00000000..45150592 --- /dev/null +++ b/rare/components/tabs/shop/shop_widget.py @@ -0,0 +1,169 @@ +import datetime +import logging + +from PyQt5.QtCore import Qt, pyqtSignal, QStringListModel +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 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 get_lang + +logger = logging.getLogger("Shop") + + +# noinspection PyAttributeOutsideInit,PyBroadException +class ShopWidget(QScrollArea, Ui_ShopWidget): + show_info = pyqtSignal(str) + show_game = pyqtSignal(dict) + free_game_widgets = [] + active_search_request = False + next_search = "" + wishlist: list = [] + + def __init__(self, path, core: LegendaryCore, shop_api: ShopApiCore): + super(ShopWidget, self).__init__() + self.setWidgetResizable(True) + self.setupUi(self) + self.path = path + self.core = core + self.shop_api = shop_api + 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.addWidget(self.free_games_widget) + + self.completer = QCompleter() + self.completer.setCaseSensitivity(Qt.CaseInsensitive) + + 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_results) + self.search_bar.buttonClicked.connect(self.show_search_results) + + # 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 != "": + + payload = { + "query": search_query, + "variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", + "count": 20, + "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) + + def load(self): + # load free games + self.shop_api.get_free_games(self.add_free_games) + # load wishlist + self.shop_api.get_wishlist(self.add_wishlist_items) + + def update_wishlist(self): + self.shop_api.get_wishlist(self.add_wishlist_items) + + def add_wishlist_items(self, wishlist): + QWidget().setLayout(self.wishlist_gb.layout()) + + self.wishlist_gb.setLayout(FlowLayout()) + 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 + self.wishlist_gb.setVisible(discounts > 0) + + def add_free_games(self, free_games): + 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 + 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 + 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(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(self.path, free_game) + if free_game["title"] != "Mystery Game": + w.show_info.connect(self.show_game.emit) + self.coming_free_games.layout().addWidget(w) + self.free_game_widgets.append(w) + 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) + + 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) + + def show_search_results(self): + self.show_info.emit(self.search_bar.text()) diff --git a/rare/ui/components/tabs/settings/rare.py b/rare/ui/components/tabs/settings/rare.py index 79fc415f..6f97a33c 100644 --- a/rare/ui/components/tabs/settings/rare.py +++ b/rare/ui/components/tabs/settings/rare.py @@ -15,6 +15,7 @@ class Ui_RareSettings(object): def setupUi(self, RareSettings): RareSettings.setObjectName("RareSettings") RareSettings.resize(694, 532) + self.rare_layout = QtWidgets.QGridLayout(RareSettings) self.rare_layout.setObjectName("rare_layout") self.settings_group = QtWidgets.QGroupBox(RareSettings) @@ -24,6 +25,13 @@ class Ui_RareSettings(object): 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.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.auto_update = QtWidgets.QCheckBox(self.settings_group) @@ -44,6 +52,15 @@ class Ui_RareSettings(object): self.log_games = QtWidgets.QCheckBox(self.settings_group) self.log_games.setObjectName("log_games") self.behavior_layout.addWidget(self.log_games, 6, 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.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.interface_group = QtWidgets.QGroupBox(RareSettings) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) @@ -121,6 +138,13 @@ class Ui_RareSettings(object): self.img_dir_layout = QtWidgets.QVBoxLayout(self.img_dir_group) self.img_dir_layout.setObjectName("img_dir_layout") self.rare_layout.addWidget(self.img_dir_group, 0, 0, 1, 1) + 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) self.retranslateUi(RareSettings) QtCore.QMetaObject.connectSlotsByName(RareSettings) @@ -136,6 +160,16 @@ class Ui_RareSettings(object): self.notification.setText(_translate("RareSettings", "Show notification on download completion")) self.sys_tray.setText(_translate("RareSettings", "Exit to System tray")) self.log_games.setText(_translate("RareSettings", "Show console for game debug")) + 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.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")) self.interface_group.setTitle(_translate("RareSettings", "Interface")) self.style_label.setText(_translate("RareSettings", "Style Sheet")) self.color_select.setItemText(0, _translate("RareSettings", "None")) @@ -150,6 +184,8 @@ class Ui_RareSettings(object): self.log_dir_open_button.setText(_translate("RareSettings", "Open Log directory")) self.log_dir_clean_button.setText(_translate("RareSettings", "Clean Log directory")) 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 f99b1b84..e69de29b 100644 --- a/rare/ui/components/tabs/settings/rare.ui +++ b/rare/ui/components/tabs/settings/rare.ui @@ -1,257 +0,0 @@ - - - RareSettings - - - - 0 - 0 - 694 - 532 - - - - RareSettings - - - - - - Behavior - - - - - - Confirm game launch - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Update games on application startup - - - - - - - Restore window size on application startup - - - - - - - Automatically sync with cloud - - - - - - - Show notification on download completion - - - - - - - Exit to System tray - - - - - - - Show console for game debug - - - - - - - - - - - 0 - 0 - - - - Interface - - - - - - Style Sheet - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - None - - - - - - - - - None - - - - - - - - Language - - - - - - - Color Scheme - - - - - - - - true - - - - Restart Rare to apply. - - - true - - - - - - - - - - Shortcuts - - - - - - Create Desktop link - - - - - - - Create start menu link - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Logs - - - - - - Open Log directory - - - - - - - Clean Log directory - - - - - - - - - - true - - - - - - - - - - Image Cache Directory - - - - - - - - - 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 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..af8dddbe --- /dev/null +++ b/rare/ui/components/tabs/store/browse_games.py @@ -0,0 +1,136 @@ +# -*- 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, 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.stack = QtWidgets.QStackedWidget(browse_games) + 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, 462, 503)) + 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.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, 198, 521)) + 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.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.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) + self.horizontalLayout_2.addWidget(self.filter_scroll) + + self.retranslateUi(browse_games) + 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.error_label.setText(_translate("browse_games", "An error occured")) + 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.genre_gb.setTitle(_translate("browse_games", "Genre")) + self.type_gb.setTitle(_translate("browse_games", "Type")) + self.platform_gb.setTitle(_translate("browse_games", "Platform")) + self.others_gb.setTitle(_translate("browse_games", "Other Tags")) + + +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..b4516ccc --- /dev/null +++ b/rare/ui/components/tabs/store/browse_games.ui @@ -0,0 +1,204 @@ + + + browse_games + + + + 0 + 0 + 706 + 541 + + + + Form + + + + + + 0 + + + + + + + true + + + + + 0 + 0 + 462 + 503 + + + + + + + + + + + + + An error occured + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 200 + 16777215 + + + + true + + + + + 0 + 0 + 198 + 521 + + + + + + + Price + + + + + + Clear price filter + + + true + + + + + + + Free + + + + + + + Under 10 + + + + + + + Under 20 + + + + + + + Under 30 + + + + + + + 14.99 and above + + + + + + + Discount + + + + + + + + + + Genre + + + + + + + + Type + + + + + + + + Platform + + + + + + + + Other Tags + + + + + + + + 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 new file mode 100644 index 00000000..80a089f5 --- /dev/null +++ b/rare/ui/components/tabs/store/shop_game_info.py @@ -0,0 +1,97 @@ +# -*- 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, QtWidgets + + +class Ui_shop_info(object): + def setupUi(self, shop_info): + shop_info.setObjectName("shop_info") + shop_info.resize(702, 468) + self.verticalLayout = QtWidgets.QVBoxLayout(shop_info) + self.verticalLayout.setObjectName("verticalLayout") + self.back_button = QtWidgets.QPushButton(shop_info) + self.back_button.setObjectName("back_button") + self.verticalLayout.addWidget(self.back_button) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.image_stack = QtWidgets.QStackedWidget(shop_info) + self.image_stack.setObjectName("image_stack") + self.horizontalLayout.addWidget(self.image_stack) + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.title = QtWidgets.QLabel(shop_info) + self.title.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) + self.title.setObjectName("title") + self.verticalLayout_2.addWidget(self.title) + self.dev = QtWidgets.QLabel(shop_info) + 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") + 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) + 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) + 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(-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.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.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")) + + +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..82437277 --- /dev/null +++ b/rare/ui/components/tabs/store/shop_game_info.ui @@ -0,0 +1,155 @@ + + + shop_info + + + + 0 + 0 + 702 + 468 + + + + Form + + + + + + Back + + + + + + + + + -1 + + + + + + + + + Error + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + You already own this game + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + Buy Game in Epic Games Store + + + + + + + Add to wishlist + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Requirements + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/rare/ui/components/tabs/store/store.py b/rare/ui/components/tabs/store/store.py new file mode 100644 index 00000000..d27baade --- /dev/null +++ b/rare/ui/components/tabs/store/store.py @@ -0,0 +1,62 @@ +# -*- 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, 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.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.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_3.addWidget(self.free_game_group_box) + 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) + self.verticalLayout.addWidget(self.scrollArea) + + self.retranslateUi(ShopWidget) + self.free_games_stack.setCurrentIndex(-1) + QtCore.QMetaObject.connectSlotsByName(ShopWidget) + + def retranslateUi(self, ShopWidget): + _translate = QtCore.QCoreApplication.translate + ShopWidget.setWindowTitle(_translate("ShopWidget", "Form")) + self.free_game_group_box.setTitle(_translate("ShopWidget", "Free Games")) + self.wishlist_gb.setTitle(_translate("ShopWidget", "Discounts from your wishlist")) + + +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..d215e5a2 --- /dev/null +++ b/rare/ui/components/tabs/store/store.ui @@ -0,0 +1,76 @@ + + + ShopWidget + + + + 0 + 0 + 697 + 362 + + + + Form + + + + + + true + + + + + 0 + 0 + 677 + 342 + + + + + + + Free Games + + + + + + -1 + + + + + + + + + + Discounts from your wishlist + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + diff --git a/rare/utils/extra_widgets.py b/rare/utils/extra_widgets.py index 4af13b9b..08b15a70 100644 --- a/rare/utils/extra_widgets.py +++ b/rare/utils/extra_widgets.py @@ -1,13 +1,20 @@ +import io import os +from logging import getLogger -from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal -from PyQt5.QtGui import QMovie +from PIL import Image +from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QSettings +from PyQt5.QtGui import QMovie, QPixmap 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 resources_path + from rare.ui.utils.pathedit import Ui_PathEdit +from rare.utils.qt_requests import QtRequestManager + +logger = getLogger("ExtraWidgets") class FlowLayout(QLayout): @@ -114,13 +121,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 = "", @@ -257,3 +257,78 @@ 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__() + self.path = cache_dir + self.manager = QtRequestManager("bytes") + + 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 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.manager.get(url, self.image_ready) + # self.request.finished.connect(self.image_ready) + else: + self.show_image() + + def image_ready(self, data): + 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])) + + 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, + 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) + self.setPlaceholderText(placeholder_text) + 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) 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/steam_grades.py b/rare/utils/steam_grades.py index 5fbf7ac0..27705482 100644 --- a/rare/utils/steam_grades.py +++ b/rare/utils/steam_grades.py @@ -9,6 +9,8 @@ from PyQt5.QtCore import QThread, pyqtSignal from custom_legendary.core import LegendaryCore from rare import cache_dir, data_dir +from rare import data_dir, cache_dir + replace_chars = ",;.:-_ " file = os.path.join(cache_dir, "game_list.json") @@ -75,6 +77,7 @@ def get_grade(steam_code): def load_json() -> dict: if not os.path.exists(file): + response = requests.get(url) steam_ids = json.loads(response.text)["applist"]["apps"] ids = {} @@ -106,6 +109,7 @@ def get_steam_id(title: str): ids = json.loads(open(file, "r").read()) if title in ids.keys(): steam_name = [title] + else: steam_name = difflib.get_close_matches(title, ids.keys(), n=1) if steam_name: diff --git a/rare/utils/utils.py b/rare/utils/utils.py index e530ab19..ee5f97c2 100644 --- a/rare/utils/utils.py +++ b/rare/utils/utils.py @@ -24,6 +24,7 @@ logger = getLogger("Utils") s = QSettings("Rare", "Rare") + def download_images(signal: pyqtSignal, core: LegendaryCore): if not os.path.isdir(image_dir): os.makedirs(image_dir) @@ -280,12 +281,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 platform.system() == "Linux": @@ -329,7 +330,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, "") @@ -376,3 +376,4 @@ def get_uninstalled_pixmap(app_name: str) -> QPixmap: else: pixmap = QPixmap() return pixmap +