From 4063195b4df11c4ff341747fb9d0707b51d67e17 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 21 Jan 2023 02:15:06 +0200 Subject: [PATCH] DownloadsTab: Refactor downloads tab When updates are queued, they are removed from the update's list. An exceptions is made when the queued item comes from repairing (without updating), in which case the update is disabled for the runtime. A queued item can be either removed (if it is an update it will be added back to the updates groups) or forced to be updated now. If a queued item is forced, the currently running item will be added to the front of the queue. Downloads will be queued if there is no active download but there is a queue already. The download thread is now responsible for emitting the progress to `RareGame` InstallDialog: Pass `RareGame` and `InstallOptionsModel` only as arguments. The `update`, `repair` and `silent` arguments are already part of `InstallOptionsModel` `RareGame` is used to query information about the game. InstallInfoWorker: Pass only `InstallOptionsModel` as argument Emit `InstallQueueItemModel` as result, to re-use the worker when queuing stopped games RareGame: Query and store metadata property about entitlement grant date RareGame: Add `RareEosOverlay` class that imitates `RareGame` to handle the overlay LibraryWidgetController: Remove dead signal routing code, these signals are handled by `RareGame` Directly parent library widgets instead of reparenting them GameWidgets: Remove unused signals EOSGroup: Set install location based on preferences and use EOSOverlayApp from legendary GamesTab: Connect the `progress` signals of dlcs to the base game's signals GamesTab: Remove dead code GlobalSignals: Remove `ProgresSignals` RareCore: Mangle internal signleton's names Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com> --- rare/components/dialogs/install_dialog.py | 175 +++---- rare/components/dialogs/launch_dialog.py | 2 + rare/components/main_window.py | 4 +- rare/components/tabs/__init__.py | 29 +- rare/components/tabs/downloads/__init__.py | 428 ++++++++---------- .../tabs/downloads/dl_queue_widget.py | 151 ------ rare/components/tabs/downloads/groups.py | 242 ++++++++++ .../{download_thread.py => thread.py} | 94 ++-- rare/components/tabs/downloads/widgets.py | 123 +++++ rare/components/tabs/games/__init__.py | 38 +- .../tabs/games/game_info/game_info.py | 2 +- .../tabs/games/game_widgets/__init__.py | 102 ++--- .../tabs/games/game_widgets/game_widget.py | 2 +- .../games/game_widgets/icon_game_widget.py | 12 +- .../tabs/games/game_widgets/icon_widget.py | 2 +- .../games/game_widgets/list_game_widget.py | 2 - .../tabs/games/integrations/eos_group.py | 9 +- rare/components/tray_icon.py | 3 +- rare/models/game.py | 139 ++++-- rare/models/install.py | 2 +- rare/models/signals.py | 23 +- rare/shared/game_process.py | 2 +- rare/shared/game_utils.py | 4 +- rare/shared/rare_core.py | 115 ++--- rare/shared/workers/verify.py | 2 +- .../tabs/downloads/download_widget.py | 10 +- .../tabs/downloads/download_widget.ui | 2 +- rare/utils/misc.py | 6 +- 28 files changed, 954 insertions(+), 771 deletions(-) delete mode 100644 rare/components/tabs/downloads/dl_queue_widget.py create mode 100644 rare/components/tabs/downloads/groups.py rename rare/components/tabs/downloads/{download_thread.py => thread.py} (70%) create mode 100644 rare/components/tabs/downloads/widgets.py diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index 5b3f2386..596b6a1e 100644 --- a/rare/components/dialogs/install_dialog.py +++ b/rare/components/dialogs/install_dialog.py @@ -3,11 +3,12 @@ import platform as pf import sys from typing import Tuple, List, Union, Optional -from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot, QSettings +from PyQt5.QtCore import QObject, QRunnable, pyqtSignal, pyqtSlot +from PyQt5.QtCore import Qt, QThreadPool, QSettings from PyQt5.QtGui import QCloseEvent, QKeyEvent from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox, QLayout, QWidget, QVBoxLayout, QApplication +from legendary.lfs.eos import EOSOverlayApp from legendary.models.downloading import ConditionCheckResult -from legendary.models.game import Game from legendary.utils.selective_dl import get_sdl_appname from rare.lgndr.cli import LegendaryCLI @@ -15,8 +16,9 @@ from rare.lgndr.core import LegendaryCore from rare.lgndr.glue.arguments import LgndrInstallGameArgs from rare.lgndr.glue.exception import LgndrException from rare.lgndr.glue.monkeys import LgndrIndirectStatus -from rare.models.install import InstallDownloadModel, InstallQueueItemModel -from rare.shared import LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton +from rare.models.game import RareGame +from rare.models.install import InstallDownloadModel, InstallQueueItemModel, InstallOptionsModel +from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced from rare.utils import config_helper @@ -37,7 +39,7 @@ class InstallDialogAdvanced(CollapsibleFrame): class InstallDialog(QDialog): result_ready = pyqtSignal(InstallQueueItemModel) - def __init__(self, dl_item: InstallQueueItemModel, update=False, repair=False, silent=False, parent=None): + def __init__(self, rgame: RareGame, options: InstallOptionsModel, parent=None,): super(InstallDialog, self).__init__(parent) self.setAttribute(Qt.WA_DeleteOnClose, True) self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint) @@ -45,14 +47,9 @@ class InstallDialog(QDialog): self.ui.setupUi(self) self.core = LegendaryCoreSingleton() - self.api_results = ApiResultsSingleton() - self.dl_item = dl_item - self.app_name = self.dl_item.options.app_name - self.game = ( - self.core.get_game(self.app_name) - if not self.dl_item.options.overlay - else Game(app_name=self.app_name, app_title="Epic Overlay") - ) + self.rgame = rgame + self.options = options + self.__download: Optional[InstallDownloadModel] = None self.advanced = InstallDialogAdvanced(parent=self) self.ui.advanced_layout.addWidget(self.advanced) @@ -60,12 +57,6 @@ class InstallDialog(QDialog): self.selectable = CollapsibleFrame(widget=None, title=self.tr("Optional downloads"), parent=self) self.ui.selectable_layout.addWidget(self.selectable) - self.game_path = self.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value", "") - - self.update = update - self.repair = repair - self.silent = silent - self.options_changed = False self.worker_running = False self.reject_close = True @@ -73,24 +64,31 @@ class InstallDialog(QDialog): self.threadpool = QThreadPool(self) self.threadpool.setMaxThreadCount(1) - header = self.tr("Update") if update else self.tr("Install") - self.ui.install_dialog_label.setText(f'

{header} "{self.game.app_title}"

') - self.setWindowTitle(f'{QApplication.instance().applicationName()} - {header} "{self.game.app_title}"') + if options.repair_mode: + header = self.tr("Repair") + if options.repair_and_update: + header = self.tr("Repair and update") + elif options.update: + header = self.tr("Update") + else: + header = self.tr("Install") + self.ui.install_dialog_label.setText(f'

{header} "{self.rgame.app_title}"

') + self.setWindowTitle(f'{QApplication.instance().applicationName()} - {header} "{self.rgame.app_title}"') - if not self.dl_item.options.base_path: - self.dl_item.options.base_path = self.core.lgd.config.get( + if not self.options.base_path: + self.options.base_path = self.core.lgd.config.get( "Legendary", "install_dir", fallback=os.path.expanduser("~/legendary") ) self.install_dir_edit = PathEdit( - path=self.dl_item.options.base_path, + path=self.options.base_path, file_type=QFileDialog.DirectoryOnly, edit_func=self.option_changed, parent=self, ) self.ui.install_dir_layout.addWidget(self.install_dir_edit) - if self.update: + if self.options.update: self.ui.install_dir_label.setEnabled(False) self.install_dir_edit.setEnabled(False) self.ui.shortcut_label.setEnabled(False) @@ -101,9 +99,9 @@ class InstallDialog(QDialog): self.error_box() platforms = ["Windows"] - if dl_item.options.app_name in self.api_results.bit32_games: + if self.rgame.is_win32: platforms.append("Win32") - if dl_item.options.app_name in self.api_results.mac_games: + if self.rgame.is_mac: platforms.append("Mac") self.ui.platform_combo.addItems(platforms) self.ui.platform_combo.currentIndexChanged.connect(lambda: self.option_changed(None)) @@ -145,16 +143,16 @@ class InstallDialog(QDialog): self.ui.install_button.setEnabled(False) - if self.dl_item.options.overlay: - self.ui.platform_label.setVisible(False) - self.ui.platform_combo.setVisible(False) - self.advanced.ui.ignore_space_label.setVisible(False) - self.advanced.ui.ignore_space_check.setVisible(False) - self.advanced.ui.download_only_label.setVisible(False) - self.advanced.ui.download_only_check.setVisible(False) - self.ui.shortcut_label.setVisible(False) - self.ui.shortcut_check.setVisible(False) - self.selectable.setVisible(False) + if self.options.overlay: + self.ui.platform_label.setEnabled(False) + self.ui.platform_combo.setEnabled(False) + self.advanced.ui.ignore_space_label.setEnabled(False) + self.advanced.ui.ignore_space_check.setEnabled(False) + self.advanced.ui.download_only_label.setEnabled(False) + self.advanced.ui.download_only_check.setEnabled(False) + self.ui.shortcut_label.setEnabled(False) + self.ui.shortcut_check.setEnabled(False) + self.selectable.setEnabled(False) if pf.system() == "Darwin": self.ui.shortcut_check.setDisabled(True) @@ -173,12 +171,12 @@ class InstallDialog(QDialog): self.ui.verify_button.clicked.connect(self.verify_clicked) self.ui.install_button.clicked.connect(self.install_clicked) - self.advanced.ui.install_prereqs_check.setChecked(self.dl_item.options.install_prereqs) + self.advanced.ui.install_prereqs_check.setChecked(self.options.install_prereqs) self.ui.install_dialog_layout.setSizeConstraint(QLayout.SetFixedSize) def execute(self): - if self.silent: + if self.options.silent: self.reject_close = False self.get_download_info() else: @@ -192,10 +190,10 @@ class InstallDialog(QDialog): cb.deleteLater() self.selectable_checks.clear() - if config_tags := self.core.lgd.config.get(self.game.app_name, 'install_tags', fallback=None): + if config_tags := self.core.lgd.config.get(self.rgame.app_name, 'install_tags', fallback=None): self.config_tags = config_tags.split(",") - config_disable_sdl = self.core.lgd.config.getboolean(self.game.app_name, 'disable_sdl', fallback=False) - sdl_name = get_sdl_appname(self.game.app_name) + config_disable_sdl = self.core.lgd.config.getboolean(self.rgame.app_name, 'disable_sdl', fallback=False) + sdl_name = get_sdl_appname(self.rgame.app_name) if not config_disable_sdl and sdl_name is not None: # FIXME: this should be updated whenever platform changes sdl_data = self.core.get_sdl_data(sdl_name, platform=platform) @@ -220,28 +218,26 @@ class InstallDialog(QDialog): self.selectable.setDisabled(True) def get_options(self): - self.dl_item.options.base_path = self.install_dir_edit.text() if not self.update else None - - self.dl_item.options.max_workers = self.advanced.ui.max_workers_spin.value() - self.dl_item.options.shared_memory = self.advanced.ui.max_memory_spin.value() - self.dl_item.options.order_opt = self.advanced.ui.dl_optimizations_check.isChecked() - self.dl_item.options.force = self.advanced.ui.force_download_check.isChecked() - self.dl_item.options.ignore_space = self.advanced.ui.ignore_space_check.isChecked() - self.dl_item.options.no_install = self.advanced.ui.download_only_check.isChecked() - self.dl_item.options.platform = self.ui.platform_combo.currentText() - self.dl_item.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() - self.dl_item.options.create_shortcut = self.ui.shortcut_check.isChecked() + self.options.base_path = self.install_dir_edit.text() if not self.options.update else None + self.options.max_workers = self.advanced.ui.max_workers_spin.value() + self.options.shared_memory = self.advanced.ui.max_memory_spin.value() + self.options.order_opt = self.advanced.ui.dl_optimizations_check.isChecked() + self.options.force = self.advanced.ui.force_download_check.isChecked() + self.options.ignore_space = self.advanced.ui.ignore_space_check.isChecked() + self.options.no_install = self.advanced.ui.download_only_check.isChecked() + self.options.platform = self.ui.platform_combo.currentText() + self.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() + self.options.create_shortcut = self.ui.shortcut_check.isChecked() if self.selectable_checks: - self.dl_item.options.install_tag = [""] + self.options.install_tag = [""] for cb in self.selectable_checks: if data := cb.isChecked(): # noinspection PyTypeChecker - self.dl_item.options.install_tag.extend(data) + self.options.install_tag.extend(data) def get_download_info(self): - self.dl_item.download = None - info_worker = InstallInfoWorker(self.core, self.dl_item, self.game) - info_worker.setAutoDelete(True) + self.__download = None + info_worker = InstallInfoWorker(self.core, self.options) info_worker.signals.result.connect(self.on_worker_result) info_worker.signals.failed.connect(self.on_worker_failed) info_worker.signals.finished.connect(self.on_worker_finished) @@ -270,21 +266,21 @@ class InstallDialog(QDialog): def non_reload_option_changed(self, option: str): if option == "download_only": - self.dl_item.options.no_install = self.advanced.ui.download_only_check.isChecked() + self.options.no_install = self.advanced.ui.download_only_check.isChecked() elif option == "shortcut": QSettings().setValue("create_shortcut", self.ui.shortcut_check.isChecked()) - self.dl_item.options.create_shortcut = self.ui.shortcut_check.isChecked() + self.options.create_shortcut = self.ui.shortcut_check.isChecked() elif option == "install_prereqs": - self.dl_item.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() + self.options.install_prereqs = self.advanced.ui.install_prereqs_check.isChecked() def cancel_clicked(self): if self.config_tags: - config_helper.add_option(self.game.app_name, 'install_tags', ','.join(self.config_tags)) + config_helper.add_option(self.rgame.app_name, 'install_tags', ','.join(self.config_tags)) else: # lk: this is purely for cleaning any install tags we might have added erroneously to the config - config_helper.remove_option(self.game.app_name, 'install_tags') + config_helper.remove_option(self.rgame.app_name, 'install_tags') - self.dl_item.download = None + self.__download = None self.reject_close = False self.close() @@ -292,33 +288,35 @@ class InstallDialog(QDialog): self.reject_close = False self.close() - def on_worker_result(self, dl_item: InstallDownloadModel): - self.dl_item.download = dl_item - download_size = self.dl_item.download.analysis.dl_size - install_size = self.dl_item.download.analysis.install_size + @pyqtSlot(InstallQueueItemModel) + def on_worker_result(self, dl_item: InstallQueueItemModel): + self.__download = dl_item.download + download_size = dl_item.download.analysis.dl_size + install_size = dl_item.download.analysis.install_size + # install_size = self.dl_item.download.analysis.disk_space_delta if download_size: - self.ui.download_size_text.setText("{}".format(get_size(download_size))) + self.ui.download_size_text.setText(get_size(download_size)) self.ui.download_size_text.setStyleSheet("font-style: normal; font-weight: bold") self.ui.install_button.setEnabled(not self.options_changed) else: self.ui.install_size_text.setText(self.tr("Game already installed")) self.ui.install_size_text.setStyleSheet("font-style: italics; font-weight: normal") - self.ui.install_size_text.setText("{}".format(get_size(install_size))) + self.ui.install_size_text.setText(get_size(install_size)) self.ui.install_size_text.setStyleSheet("font-style: normal; font-weight: bold") self.ui.verify_button.setEnabled(self.options_changed) self.ui.cancel_button.setEnabled(True) if pf.system() == "Windows" or ArgumentsSingleton().debug: - if dl_item.igame.prereq_info and not dl_item.igame.prereq_info.get("installed", False): + if dl_item.download.igame.prereq_info and not dl_item.download.igame.prereq_info.get("installed", False): self.advanced.ui.install_prereqs_check.setEnabled(True) self.advanced.ui.install_prereqs_label.setEnabled(True) self.advanced.ui.install_prereqs_check.setChecked(True) - prereq_name = dl_item.igame.prereq_info.get("name", "") - prereq_path = os.path.split(dl_item.igame.prereq_info.get("path", ""))[-1] + prereq_name = dl_item.download.igame.prereq_info.get("name", "") + prereq_path = os.path.split(dl_item.download.igame.prereq_info.get("path", ""))[-1] prereq_desc = prereq_name if prereq_name else prereq_path self.advanced.ui.install_prereqs_check.setText( self.tr("Also install: {}").format(prereq_desc) ) - if self.silent: + if self.options.silent: self.close() def on_worker_failed(self, message: str): @@ -328,7 +326,7 @@ class InstallDialog(QDialog): self.error_box(error_text, message) self.ui.verify_button.setEnabled(self.options_changed) self.ui.cancel_button.setEnabled(True) - if self.silent: + if self.options.silent: self.show() def error_box(self, label: str = "", message: str = ""): @@ -349,7 +347,7 @@ class InstallDialog(QDialog): else: self.threadpool.clear() self.threadpool.waitForDone() - self.result_ready.emit(self.dl_item) + self.result_ready.emit(InstallQueueItemModel(options=self.options, download=self.__download)) super(InstallDialog, self).closeEvent(a0) def keyPressEvent(self, e: QKeyEvent) -> None: @@ -359,51 +357,51 @@ class InstallDialog(QDialog): class InstallInfoWorker(QRunnable): class Signals(QObject): - result = pyqtSignal(InstallDownloadModel) + result = pyqtSignal(InstallQueueItemModel) failed = pyqtSignal(str) finished = pyqtSignal() - def __init__(self, core: LegendaryCore, dl_item: InstallQueueItemModel, game: Game = None): + def __init__(self, core: LegendaryCore, options: InstallOptionsModel): sys.excepthook = sys.__excepthook__ super(InstallInfoWorker, self).__init__() + self.setAutoDelete(True) self.signals = InstallInfoWorker.Signals() self.core = core - self.dl_item = dl_item - self.game = game + self.options = options @pyqtSlot() def run(self): try: - if not self.dl_item.options.overlay: + if not self.options.overlay: cli = LegendaryCLI(self.core) status = LgndrIndirectStatus() result = cli.install_game( - LgndrInstallGameArgs(**self.dl_item.options.as_install_kwargs(), indirect_status=status) + LgndrInstallGameArgs(**self.options.as_install_kwargs(), indirect_status=status) ) if result: download = InstallDownloadModel(*result) else: raise LgndrException(status.message) else: - if not os.path.exists(path := self.dl_item.options.base_path): + if not os.path.exists(path := self.options.base_path): os.makedirs(path) dlm, analysis, igame = self.core.prepare_overlay_install( - path=self.dl_item.options.base_path + path=self.options.base_path ) download = InstallDownloadModel( dlm=dlm, analysis=analysis, igame=igame, - game=self.game, + game=EOSOverlayApp, repair=False, repair_file="", res=ConditionCheckResult(), # empty ) if not download.res or not download.res.failures: - self.signals.result.emit(download) + self.signals.result.emit(InstallQueueItemModel(options=self.options, download=download)) else: self.signals.failed.emit("\n".join(str(i) for i in download.res.failures)) except LgndrException as ret: @@ -421,3 +419,6 @@ class TagCheckBox(QCheckBox): def isChecked(self) -> Union[bool, List[str]]: return self.tags if super(TagCheckBox, self).isChecked() else False + + + diff --git a/rare/components/dialogs/launch_dialog.py b/rare/components/dialogs/launch_dialog.py index 7bef22f6..0018b003 100644 --- a/rare/components/dialogs/launch_dialog.py +++ b/rare/components/dialogs/launch_dialog.py @@ -282,6 +282,8 @@ class LaunchDialog(QDialog): if self.completed >= 2: logger.info("App starting") ApiResultsSingleton(self.api_results) + # FIXME: Add this to RareCore + self.core.lgd.entitlements = self.core.egs.get_user_entitlements() self.completed += 1 self.start_app.emit() diff --git a/rare/components/main_window.py b/rare/components/main_window.py index a6aee0fd..241dc4f9 100644 --- a/rare/components/main_window.py +++ b/rare/components/main_window.py @@ -164,9 +164,7 @@ class MainWindow(QMainWindow): defaultButton=QMessageBox.No, ) if reply == QMessageBox.Yes: - # clear queue - self.tab_widget.downloads_tab.queue_widget.update_queue([]) - self.tab_widget.downloads_tab.stop_download() + self.tab_widget.downloads_tab.stop_download(on_exit=True) else: e.ignore() return diff --git a/rare/components/tabs/__init__.py b/rare/components/tabs/__init__.py index 1bb950fc..915e4d74 100644 --- a/rare/components/tabs/__init__.py +++ b/rare/components/tabs/__init__.py @@ -32,11 +32,10 @@ class TabWidget(QTabWidget): # Downloads Tab after Games Tab to use populated RareCore games list if not self.args.offline: self.downloads_tab = DownloadsTab(self) - updates = list(self.rcore.updates) - self.addTab( - self.downloads_tab, - self.tr("Downloads {}").format(f"({len(updates) if updates else 0})"), - ) + # update dl tab text + self.addTab(self.downloads_tab, "") + self.__on_downloads_update_title(self.downloads_tab.queues_count()) + self.downloads_tab.update_title.connect(self.__on_downloads_update_title) self.store = Shop(self.core) self.addTab(self.store, self.tr("Store (Beta)")) @@ -71,12 +70,9 @@ class TabWidget(QTabWidget): # set current index # self.signals.set_main_tab_index.connect(self.setCurrentIndex) - # update dl tab text - self.signals.download.update_tab.connect(self.update_dl_tab_text) - # Open game list on click on Games tab button self.tabBarClicked.connect(self.mouse_clicked) - self.setIconSize(QSize(25, 25)) + self.setIconSize(QSize(24, 24)) # shortcuts QShortcut("Alt+1", self).activated.connect(lambda: self.setCurrentIndex(0)) @@ -84,18 +80,9 @@ class TabWidget(QTabWidget): QShortcut("Alt+3", self).activated.connect(lambda: self.setCurrentIndex(2)) QShortcut("Alt+4", self).activated.connect(lambda: self.setCurrentIndex(5)) - def update_dl_tab_text(self): - num_downloads = len( - set( - [i.options.app_name for i in self.downloads_tab.dl_queue] - + [i for i in self.downloads_tab.update_widgets.keys()] - ) - ) - - if num_downloads != 0: - self.setTabText(1, f"Downloads ({num_downloads})") - else: - self.setTabText(1, "Downloads") + @pyqtSlot(int) + def __on_downloads_update_title(self, num_downloads: int): + self.setTabText(self.indexOf(self.downloads_tab), self.tr("Downloads ({})").format(num_downloads)) def mouse_clicked(self, tab_num): if tab_num == 0: diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index c829a5de..c27b497b 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -1,303 +1,265 @@ import datetime +from ctypes import c_ulonglong from logging import getLogger -from typing import List, Dict, Union, Set, Optional +from typing import Union, Optional -from PyQt5.QtCore import QThread, pyqtSignal, QSettings, pyqtSlot +from PyQt5.QtCore import pyqtSignal, QSettings, pyqtSlot, QThreadPool from PyQt5.QtWidgets import ( QWidget, QMessageBox, - QVBoxLayout, - QLabel, - QPushButton, - QGroupBox, ) -from legendary.core import LegendaryCore -from legendary.models.game import Game, InstalledGame -from rare.components.dialogs.install_dialog import InstallDialog -from rare.components.tabs.downloads.dl_queue_widget import DlQueueWidget, DlWidget -from rare.components.tabs.downloads.download_thread import DownloadThread +from rare.components.dialogs.install_dialog import InstallDialog, InstallInfoWorker from rare.lgndr.models.downloading import UIUpdate from rare.models.game import RareGame from rare.models.install import InstallOptionsModel, InstallQueueItemModel -from rare.shared import RareCore, LegendaryCoreSingleton, GlobalSignalsSingleton +from rare.shared import RareCore, LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton from rare.ui.components.tabs.downloads.downloads_tab import Ui_DownloadsTab from rare.utils.misc import get_size, create_desktop_link +from .groups import UpdateGroup, QueueGroup +from .thread import DlThread, DlResultModel, DlResultCode +from .widgets import UpdateWidget, QueueWidget logger = getLogger("Download") -class DownloadsTab(QWidget, Ui_DownloadsTab): - thread: QThread - dl_queue: List[InstallQueueItemModel] = [] - dl_status = pyqtSignal(int) +def get_time(seconds: Union[int, float]) -> str: + return str(datetime.timedelta(seconds=seconds)) + + +class DownloadsTab(QWidget): + # int: number of updates + update_title = pyqtSignal(int) def __init__(self, parent=None): super(DownloadsTab, self).__init__(parent=parent) - self.setupUi(self) + self.ui = Ui_DownloadsTab() + self.ui.setupUi(self) self.rcore = RareCore.instance() self.core = LegendaryCoreSingleton() self.signals = GlobalSignalsSingleton() + self.args = ArgumentsSingleton() - self.active_game: Optional[Game] = None - self.analysis = None + self.thread: Optional[DlThread] = None + self.threadpool = QThreadPool(self) + self.threadpool.setMaxThreadCount(1) - self.kill_button.clicked.connect(self.stop_download) + self.ui.kill_button.clicked.connect(self.stop_download) - self.queue_widget = DlQueueWidget() - self.queue_widget.update_list.connect(self.update_dl_queue) - self.queue_scroll_contents_layout.addWidget(self.queue_widget) + self.queue_group = QueueGroup(self) + # lk: todo recreate update widget + self.queue_group.removed.connect(self.__on_queue_removed) + self.queue_group.force.connect(self.__on_queue_force) + self.ui.queue_scroll_contents_layout.addWidget(self.queue_group) - self.updates = QGroupBox(self.tr("Updates")) - self.updates.setObjectName("updates_group") - self.update_layout = QVBoxLayout(self.updates) - self.queue_scroll_contents_layout.addWidget(self.updates) + self.updates_group = UpdateGroup(self) + self.updates_group.enqueue.connect(self.__get_install_options) + self.ui.queue_scroll_contents_layout.addWidget(self.updates_group) - self.update_widgets: Dict[str, UpdateWidget] = {} + self.__check_updates() - self.update_text = QLabel(self.tr("No updates available")) - self.update_layout.addWidget(self.update_text) + self.signals.game.install.connect(self.__get_install_options) + self.signals.download.enqueue.connect(self.__add_update) + self.signals.download.dequeue.connect(self.__on_game_uninstalled) - has_updates = False + self.__reset_download() + + self.forced_item: Optional[InstallQueueItemModel] = None + self.on_exit = False + + def __check_updates(self): for rgame in self.rcore.updates: - has_updates = True - self.add_update(rgame) + self.__add_update(rgame) - self.queue_widget.item_removed.connect(self.queue_item_removed) - - self.signals.game.install.connect(self.get_install_options) - self.signals.game.uninstalled.connect(self.queue_item_removed) - self.signals.game.uninstalled.connect(self.remove_update) - - self.signals.download.enqueue_game.connect( - lambda app_name: self.add_update(app_name) - ) - self.signals.game.uninstalled.connect(self.game_uninstalled) - - self.reset_infos() - - def queue_item_removed(self, app_name): - if w := self.update_widgets.get(app_name): - w.update_button.setDisabled(False) - w.update_with_settings.setDisabled(False) - - def add_update(self, rgame: RareGame): - if old_widget := self.update_widgets.get(rgame.app_name, False): - old_widget.deleteLater() - self.update_widgets.pop(rgame.app_name) - widget = UpdateWidget(self.core, rgame, self) - self.update_layout.addWidget(widget) - self.update_widgets[rgame.app_name] = widget - widget.update_signal.connect(self.get_install_options) - if QSettings().value("auto_update", False, bool): - self.get_install_options( - InstallOptionsModel(app_name=rgame.app_name, update=True, silent=True) + def __add_update(self, update: Union[str,RareGame]): + if isinstance(update, str): + update = self.rcore.get_game(update) + if update.metadata.auto_update or QSettings().value("auto_update", False, bool): + self.__get_install_options( + InstallOptionsModel(app_name=update.app_name, update=True, silent=True) ) - widget.update_button.setDisabled(True) - self.update_text.setVisible(False) + else: + self.updates_group.append(update.game, update.igame) - def game_uninstalled(self, app_name): - # game in dl_queue - for i, item in enumerate(self.dl_queue): - if item.options.app_name == app_name: - self.dl_queue.pop(i) - self.queue_widget.update_queue(self.dl_queue) - break + @pyqtSlot(str) + def __on_queue_removed(self, app_name: str): + """ + Handle removing a queued item. + If the item exists in the updates (it means a repair was removed), re-enable the buttons. + If it doesn't exist in the updates, recreate the widget. + :param app_name: + :return: + """ + if self.updates_group.contains(app_name): + self.updates_group.set_widget_enabled(app_name, True) + else: + if self.rcore.get_game(app_name).is_installed: + self.__add_update(app_name) - # if game is updating - if self.active_game and self.active_game.app_name == app_name: + @pyqtSlot(InstallQueueItemModel) + def __on_queue_force(self, item: InstallQueueItemModel): + if self.thread: + self.stop_download() + self.forced_item = item + else: + self.__start_installation(item) + + @pyqtSlot(str) + def __on_game_uninstalled(self, app_name): + if self.thread and self.thread.item.options.app_name == app_name: self.stop_download() - # game has available update - if app_name in self.update_widgets.keys(): - self.remove_update(app_name) + if self.queue_group.contains(app_name): + self.queue_group.remove(app_name) - def remove_update(self, app_name): - if w := self.update_widgets.get(app_name): - w.deleteLater() - self.update_widgets.pop(app_name) + if self.updates_group.contains(app_name): + self.updates_group.remove(app_name) - if len(self.update_widgets) == 0: - self.update_text.setVisible(True) + self.update_title.emit(self.queues_count()) - self.signals.download.update_tab.emit() - - def update_dl_queue(self, dl_queue): - self.dl_queue = dl_queue - - def stop_download(self): + def stop_download(self, on_exit=False): self.thread.kill() - self.kill_button.setEnabled(False) + self.ui.kill_button.setEnabled(False) + # lk: if we are exitin Rare, waif for thread to finish + # `self.on_exit` control whether we try to add the download + # back in the queue. If we are on exit we wait for the thread + # to finish, we do not care about handling the result really + if on_exit: + self.on_exit = on_exit + self.thread.wait() - def install_game(self, queue_item: InstallQueueItemModel): - if self.active_game is None: - self.start_installation(queue_item) - else: - self.dl_queue.append(queue_item) - self.queue_widget.update_queue(self.dl_queue) + def __start_installation(self, item: InstallQueueItemModel): + thread = DlThread(item, self.rcore.get_game(item.options.app_name), self.core, self.args.debug) + thread.result.connect(self.__on_download_result) + thread.progress.connect(self.__on_download_progress) + thread.finished.connect(thread.deleteLater) + thread.start() + self.thread = thread + self.ui.kill_button.setDisabled(False) + self.ui.dl_name.setText(item.download.game.app_title) - def start_installation(self, queue_item: InstallQueueItemModel): - if self.dl_queue: - self.dl_queue.pop(0) - self.queue_widget.update_queue(self.dl_queue) - self.active_game = queue_item.download.game - self.thread = DownloadThread(self.core, queue_item) - self.thread.ret_status.connect(self.status) - self.thread.ui_update.connect(self.progress_update) - self.thread.start() - self.kill_button.setDisabled(False) - self.analysis = queue_item.download.analysis - self.dl_name.setText(self.active_game.app_title) + def queues_count(self) -> int: + return self.updates_group.count() + self.queue_group.count() - self.signals.progress.started.emit(self.active_game.app_name) + @pyqtSlot(InstallQueueItemModel) + def __on_info_worker_result(self, item: InstallQueueItemModel): + rgame = self.rcore.get_game(item.options.app_name) + self.queue_group.push_front(item, rgame.igame) + logger.info(f"Re-queued download for {item.download.game.app_name} ({item.download.game.app_title})") - @pyqtSlot(DownloadThread.ReturnStatus) - def status(self, result: DownloadThread.ReturnStatus): - if result.ret_code == result.ReturnCode.FINISHED: + @pyqtSlot(str) + def __on_info_worker_failed(self, message: str): + logger.error(f"Failed to re-queue stopped download with error: {message}") + + @pyqtSlot() + def __on_info_worker_finished(self): + logger.info("Download re-queue worker finished") + + @pyqtSlot(DlResultModel) + def __on_download_result(self, result: DlResultModel): + if result.code == DlResultCode.FINISHED: if result.shortcuts: - if not create_desktop_link(result.app_name, self.core, "desktop"): + if not create_desktop_link(result.item.options.app_name, self.core, "desktop"): # maybe add it to download summary, to show in finished downloads pass else: logger.info("Desktop shortcut written") + logger.info( + f"Download finished: {result.item.download.game.app_name} ({result.item.download.game.app_title})" + ) - self.dl_name.setText(self.tr("Download finished. Reload library")) - logger.info(f"Download finished: {self.active_game.app_title}") - - game = self.active_game - self.active_game = None - - if self.dl_queue: - if self.dl_queue[0].download.game.app_name == game.app_name: - self.dl_queue.pop(0) - self.queue_widget.update_queue(self.dl_queue) - - if game.app_name in self.update_widgets.keys(): - igame = self.core.get_installed_game(game.app_name) - if ( - self.core.get_asset( - game.app_name, igame.platform, False - ).build_version - == igame.version - ): - self.remove_update(game.app_name) - - self.signals.application.notify.emit(game.app_title) - self.signals.game.installed.emit([game.app_name]) - self.signals.download.update_tab.emit() - - self.signals.progress.finished.emit(game.app_name, True) - - self.reset_infos() - - if len(self.dl_queue) != 0: - self.start_installation(self.dl_queue[0]) + if result.item.options.overlay: + self.signals.application.overlay_installed.emit() else: - self.queue_widget.update_queue(self.dl_queue) + self.signals.application.notify.emit(result.item.download.game.app_name) - elif result.ret_code == result.ReturnCode.ERROR: - QMessageBox.warning(self, self.tr("Error"), f"Download error: {result.message}") + if self.updates_group.contains(result.item.options.app_name): + self.updates_group.set_widget_enabled(result.item.options.app_name, True) - elif result.ret_code == result.ReturnCode.STOPPED: - self.reset_infos() - if w := self.update_widgets.get(self.active_game.app_name): - w.update_button.setDisabled(False) - w.update_with_settings.setDisabled(False) - self.signals.progress.finished.emit(self.active_game.app_name, False) - self.active_game = None - if self.dl_queue: - self.start_installation(self.dl_queue[0]) + elif result.code == DlResultCode.ERROR: + QMessageBox.warning(self, self.tr("Error"), self.tr("Download error: {}").format(result.message)) + logger.error(f"Download error: {result.message}") - def reset_infos(self): - self.kill_button.setDisabled(True) - self.dl_name.setText(self.tr("No active download")) - self.progress_bar.setValue(0) - self.dl_speed.setText("n/a") - self.time_left.setText("n/a") - self.cache_used.setText("n/a") - self.downloaded.setText("n/a") - self.analysis = None + elif result.code == DlResultCode.STOPPED: + logger.info(f"Download stopped: {result.item.download.game.app_title}") + if not self.on_exit: + info_worker = InstallInfoWorker(self.core, result.item.options) + info_worker.signals.result.connect(self.__on_info_worker_result) + info_worker.signals.failed.connect(self.__on_info_worker_failed) + info_worker.signals.finished.connect(self.__on_info_worker_finished) + self.threadpool.start(info_worker) + else: + return - @pyqtSlot(str, UIUpdate) - def progress_update(self, app_name: str, ui_update: UIUpdate): - self.progress_bar.setValue( - 100 * ui_update.total_downloaded // self.analysis.dl_size - ) - self.dl_speed.setText(f"{get_size(ui_update.download_compressed_speed)}/s") - self.cache_used.setText( + self.update_title.emit(self.queues_count()) + + # lk: if we finished a repair and we have a disabled update, re-enable it + if self.updates_group.contains(result.item.options.app_name): + self.updates_group.set_widget_enabled(result.item.options.app_name, True) + + if result.code == DlResultCode.FINISHED and self.queue_group.count(): + self.__start_installation(self.queue_group.pop_front()) + elif result.code == DlResultCode.STOPPED and self.forced_item: + self.__start_installation(self.forced_item) + self.forced_item = None + else: + self.__reset_download() + + def __reset_download(self): + self.ui.kill_button.setDisabled(True) + self.ui.dl_name.setText(self.tr("No active download")) + self.ui.progress_bar.setValue(0) + self.ui.dl_speed.setText("n/a") + self.ui.time_left.setText("n/a") + self.ui.cache_used.setText("n/a") + self.ui.downloaded.setText("n/a") + self.thread = None + + @pyqtSlot(UIUpdate, c_ulonglong) + def __on_download_progress(self, ui_update: UIUpdate, dl_size: c_ulonglong): + self.ui.progress_bar.setValue(int(ui_update.progress)) + self.ui.dl_speed.setText(f"{get_size(ui_update.download_compressed_speed)}/s") + self.ui.cache_used.setText( f"{get_size(ui_update.cache_usage) if ui_update.cache_usage > 1023 else '0KB'}" ) - self.downloaded.setText( - f"{get_size(ui_update.total_downloaded)} / {get_size(self.analysis.dl_size)}" - ) - self.time_left.setText(self.get_time(ui_update.estimated_time_left)) - self.signals.progress.value.emit( - app_name, - 100 * ui_update.total_downloaded // self.analysis.dl_size + self.ui.downloaded.setText( + f"{get_size(ui_update.total_downloaded)} / {get_size(dl_size.value)}" ) + self.ui.time_left.setText(get_time(ui_update.estimated_time_left)) - def get_time(self, seconds: Union[int, float]) -> str: - return str(datetime.timedelta(seconds=seconds)) - - def on_install_dialog_closed(self, download_item: InstallQueueItemModel): - if download_item: - self.install_game(download_item) - # lk: In case the download in comming from game verification/repair - if w := self.update_widgets.get(download_item.options.app_name): - w.update_button.setDisabled(True) - w.update_with_settings.setDisabled(True) - # self.signals.set_main_tab_index.emit(1) + @pyqtSlot(InstallQueueItemModel) + def __on_install_dialog_closed(self, item: InstallQueueItemModel): + if item: + # lk: start update only if there is no other active thread and there is no queue + if self.thread is None and not self.queue_group.count(): + self.__start_installation(item) + else: + rgame = self.rcore.get_game(item.options.app_name) + self.queue_group.push_back(item, rgame.igame) + # lk: Handle repairing into the current version + # When we add something to the queue from repair, we might select to update or not + # if we do select to update with repair, we can remove the widget from the updates groups + # otherwise we disable it and keep it in the updates + if self.updates_group.contains(item.options.app_name): + if item.download.igame.version == self.updates_group.get_update_version(item.options.app_name): + self.updates_group.remove(item.options.app_name) + else: + self.updates_group.set_widget_enabled(item.options.app_name, False) else: - if w := self.update_widgets.get(download_item.options.app_name): - w.update_button.setDisabled(False) - w.update_with_settings.setDisabled(False) + if self.updates_group.contains(item.options.app_name): + self.updates_group.set_widget_enabled(item.options.app_name, True) - def get_install_options(self, options: InstallOptionsModel): + @pyqtSlot(InstallOptionsModel) + def __get_install_options(self, options: InstallOptionsModel): install_dialog = InstallDialog( - InstallQueueItemModel(options=options), - update=options.update, - silent=options.silent, + self.rcore.get_game(options.app_name), + options=options, parent=self, ) - install_dialog.result_ready.connect(self.on_install_dialog_closed) + install_dialog.result_ready.connect(self.__on_install_dialog_closed) install_dialog.execute() @property def is_download_active(self): - return self.active_game is not None - - -class UpdateWidget(QWidget): - update_signal = pyqtSignal(InstallOptionsModel) - - def __init__(self, core: LegendaryCore, rgame: RareGame, parent): - super(UpdateWidget, self).__init__(parent=parent) - self.core = core - self.rgame = rgame - - layout = QVBoxLayout() - self.title = QLabel(self.rgame.app_title) - layout.addWidget(self.title) - - self.update_button = QPushButton(self.tr("Update Game")) - self.update_button.clicked.connect(lambda: self.update_game(True)) - self.update_with_settings = QPushButton("Update with settings") - self.update_with_settings.clicked.connect(lambda: self.update_game(False)) - layout.addWidget(self.update_button) - layout.addWidget(self.update_with_settings) - layout.addWidget( - QLabel( - self.tr("Version: {} >> {}") - .format(self.rgame.version, self.rgame.remote_version) - ) - ) - - self.setLayout(layout) - - def update_game(self, auto: bool): - self.update_button.setDisabled(True) - self.update_with_settings.setDisabled(True) - self.update_signal.emit( - InstallOptionsModel(app_name=self.rgame.app_name, update=True, silent=auto) - ) # True if settings + return self.thread is not None diff --git a/rare/components/tabs/downloads/dl_queue_widget.py b/rare/components/tabs/downloads/dl_queue_widget.py deleted file mode 100644 index 18152273..00000000 --- a/rare/components/tabs/downloads/dl_queue_widget.py +++ /dev/null @@ -1,151 +0,0 @@ -from logging import getLogger - -from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import ( - QGroupBox, - QVBoxLayout, - QLabel, - QHBoxLayout, - QPushButton, - QWidget, -) - -from rare.models.install import InstallQueueItemModel -from rare.utils.misc import icon - -logger = getLogger("QueueWidget") - - -class DlWidget(QWidget): - move_up = pyqtSignal(str) # app_name - move_down = pyqtSignal(str) # app_name - remove = pyqtSignal(str) # app_name - - def __init__(self, index, queue_item: InstallQueueItemModel): - super(DlWidget, self).__init__() - self.app_name = queue_item.download.game.app_name - self.layout = QHBoxLayout() - - self.left_layout = QVBoxLayout() - self.move_up_button = QPushButton(icon("fa.arrow-up"), "") - if index == 0: - self.move_up_button.setDisabled(True) - self.move_up_button.clicked.connect(lambda: self.move_up.emit(self.app_name)) - self.move_up_button.setFixedWidth(20) - self.left_layout.addWidget(self.move_up_button) - - self.move_down_buttton = QPushButton(icon("fa.arrow-down"), "") - self.move_down_buttton.clicked.connect( - lambda: self.move_down.emit(self.app_name) - ) - self.left_layout.addWidget(self.move_down_buttton) - self.move_down_buttton.setFixedWidth(20) - - self.layout.addLayout(self.left_layout) - - self.right_layout = QVBoxLayout() - self.title = QLabel(queue_item.download.game.app_title) - self.right_layout.addWidget(self.title) - - dl_size = queue_item.download.analysis.dl_size - install_size = queue_item.download.analysis.install_size - - self.size = QHBoxLayout() - - self.size.addWidget( - QLabel( - self.tr("Download size: {} GB").format(round(dl_size / 1024 ** 3, 2)) - ) - ) - self.size.addWidget( - QLabel( - self.tr("Install size: {} GB").format( - round(install_size / 1024 ** 3, 2) - ) - ) - ) - self.right_layout.addLayout(self.size) - - self.delete = QPushButton(self.tr("Remove Download")) - self.delete.clicked.connect(lambda: self.remove.emit(self.app_name)) - self.right_layout.addWidget(self.delete) - - self.layout.addLayout(self.right_layout) - self.setLayout(self.layout) - - -class DlQueueWidget(QGroupBox): - update_list = pyqtSignal(list) - item_removed = pyqtSignal(str) - dl_queue = [] - - def __init__(self): - super(DlQueueWidget, self).__init__() - self.setTitle(self.tr("Download Queue")) - self.setLayout(QVBoxLayout()) - self.setObjectName("group") - self.text = QLabel(self.tr("No downloads in queue")) - self.layout().addWidget(self.text) - - def update_queue(self, dl_queue: list): - logger.debug( - "Update Queue " + ", ".join(i.download.game.app_title for i in dl_queue) - ) - self.dl_queue = dl_queue - - for item in (self.layout().itemAt(i) for i in range(self.layout().count())): - item.widget().deleteLater() - - if len(dl_queue) == 0: - self.layout().addWidget(QLabel(self.tr("No downloads in queue"))) - self.setLayout(self.layout()) - return - - for index, item in enumerate(dl_queue): - widget = DlWidget(index, item) - widget.remove.connect(self.remove) - widget.move_up.connect(self.move_up) - widget.move_down.connect(self.move_down) - self.layout().addWidget(widget) - if index + 1 == len(dl_queue): - widget.move_down_buttton.setDisabled(True) - - def remove(self, app_name): - for index, i in enumerate(self.dl_queue): - if i.download.game.app_name == app_name: - self.dl_queue.pop(index) - self.item_removed.emit(app_name) - break - else: - logger.warning(f"BUG! {app_name}") - return - self.update_list.emit(self.dl_queue) - self.update_queue(self.dl_queue) - - def move_up(self, app_name): - index: int - - for i, item in enumerate(self.dl_queue): - if item.download.game.app_name == app_name: - index = i - break - else: - logger.warning(f"Could not find appname {app_name}") - return - self.dl_queue.insert(index - 1, self.dl_queue.pop(index)) - self.update_list.emit(self.dl_queue) - self.update_queue(self.dl_queue) - - def move_down(self, app_name): - index: int - - for i, item in enumerate(self.dl_queue): - if item.download.game.app_name == app_name: - index = i - break - else: - logger.warning(f"Info: Could not find appname {app_name}") - return - self.dl_queue.insert(index + 1, self.dl_queue.pop(index)) - self.update_list.emit(self.dl_queue) - self.update_queue(self.dl_queue) diff --git a/rare/components/tabs/downloads/groups.py b/rare/components/tabs/downloads/groups.py new file mode 100644 index 00000000..8dcef8ef --- /dev/null +++ b/rare/components/tabs/downloads/groups.py @@ -0,0 +1,242 @@ +from argparse import Namespace +from collections import deque +from enum import IntEnum +from logging import getLogger +from typing import Optional + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt +from PyQt5.QtWidgets import ( + QGroupBox, + QVBoxLayout, + QLabel, QSizePolicy, QWidget, +) +from legendary.models.game import Game, InstalledGame + +from rare.components.tabs.downloads.widgets import QueueWidget, UpdateWidget +from rare.models.install import InstallOptionsModel, InstallQueueItemModel + +logger = getLogger("QueueGroup") + + +class UpdateGroup(QGroupBox): + enqueue = pyqtSignal(InstallOptionsModel) + + def __init__(self, parent=None): + super(UpdateGroup, self).__init__(parent=parent) + self.setObjectName(type(self).__name__) + self.setTitle(self.tr("Updates")) + self.text = QLabel(self.tr("No updates available")) + self.text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + # lk: For findChildren to work, the upates's layout has to be in a widget + self.container = QWidget(self) + self.container.setLayout(QVBoxLayout()) + self.container.layout().setContentsMargins(0, 0, 0, 0) + + self.setLayout(QVBoxLayout()) + self.layout().addWidget(self.text) + self.layout().addWidget(self.container) + + @staticmethod + def __widget_name(app_name: str) -> str: + return f"UpdateWidget_{app_name}" + + def __find_widget(self, app_name: str) -> Optional[UpdateWidget]: + return self.container.findChild(UpdateWidget, name=self.__widget_name(app_name)) + + def count(self) -> int: + return len(self.container.findChildren(UpdateWidget, options=Qt.FindDirectChildrenOnly)) + + def contains(self, app_name: str) -> bool: + return self.__find_widget(app_name) is not None + + def append(self, game: Game, igame: InstalledGame): + self.text.setVisible(False) + self.container.setVisible(True) + widget: UpdateWidget = self.__find_widget(game.app_name) + if widget is not None: + self.container.layout().removeWidget(widget) + widget.deleteLater() + widget = UpdateWidget(game, igame, parent=self.container) + widget.enqueue.connect(self.enqueue) + self.container.layout().addWidget(widget) + + def remove(self, app_name: str): + widget: UpdateWidget = self.__find_widget(app_name) + self.container.layout().removeWidget(widget) + widget.deleteLater() + self.text.setVisible(not bool(self.count())) + self.container.setVisible(bool(self.count())) + + def set_widget_enabled(self, app_name: str, enabled: bool): + widget: UpdateWidget = self.__find_widget(app_name) + widget.set_enabled(enabled) + + def get_update_version(self, app_name: str) -> str: + widget: UpdateWidget = self.__find_widget(app_name) + return widget.version() + + +class QueueGroup(QGroupBox): + removed = pyqtSignal(str) + force = pyqtSignal(InstallQueueItemModel) + + def __init__(self, parent=None): + super(QueueGroup, self).__init__(parent=parent) + self.setObjectName(type(self).__name__) + self.setTitle(self.tr("Queue")) + self.text = QLabel(self.tr("No downloads in queue"), self) + self.text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + # lk: For findChildren to work, the queue's layout has to be in a widget + self.container = QWidget(self) + self.container.setLayout(QVBoxLayout()) + self.container.layout().setContentsMargins(0, 0, 0, 0) + self.container.setVisible(False) + + self.setLayout(QVBoxLayout()) + self.layout().addWidget(self.text) + self.layout().addWidget(self.container) + + self.queue = deque() + + @staticmethod + def __widget_name(app_name:str) -> str: + return f"QueueWidget_{app_name}" + + def __find_widget(self, app_name: str) -> Optional[QueueWidget]: + return self.container.findChild(QueueWidget, name=self.__widget_name(app_name)) + + def contains(self, app_name: str) -> bool: + return self.__find_widget(app_name) is not None + + def count(self) -> int: + return len(self.queue) + + def __create_widget(self, item: InstallQueueItemModel, old_igame: InstalledGame) -> QueueWidget: + widget: QueueWidget = QueueWidget(item, old_igame, parent=self.container) + widget.toggle_arrows(self.queue.index(item.download.game.app_name), len(self.queue)) + widget.remove.connect(self.remove) + widget.force.connect(self.__on_force) + widget.move_up.connect(self.__on_move_up) + widget.move_down.connect(self.__on_move_down) + return widget + + def push_front(self, item: InstallQueueItemModel, old_igame: InstalledGame): + self.text.setVisible(False) + self.container.setVisible(True) + self.queue.appendleft(item.download.game.app_name) + widget = self.__create_widget(item, old_igame) + self.container.layout().insertWidget(0, widget) + if self.count() > 1: + app_name = self.queue[1] + other: QueueWidget = self.__find_widget(app_name) + other.toggle_arrows(1, len(self.queue)) + + def push_back(self, item: InstallQueueItemModel, old_igame: InstalledGame): + self.text.setVisible(False) + self.container.setVisible(True) + self.queue.append(item.download.game.app_name) + widget = self.__create_widget(item, old_igame) + self.container.layout().addWidget(widget) + if self.count() > 1: + app_name = self.queue[-2] + other: QueueWidget = self.__find_widget(app_name) + other.toggle_arrows(len(self.queue) - 2, len(self.queue)) + + def pop_front(self) -> InstallQueueItemModel: + app_name = self.queue.popleft() + widget: QueueWidget = self.__find_widget(app_name) + item = widget.item + widget.deleteLater() + self.text.setVisible(not bool(self.count())) + self.container.setVisible(bool(self.count())) + return item + + def __update_queue(self): + """ + check the first, second, last and second to last widgets in the list + and update their arrows + :return: None + """ + for idx in [0, 1]: + if self.count() > idx: + app_name = self.queue[idx] + widget: QueueWidget = self.__find_widget(app_name) + widget.toggle_arrows(idx, len(self.queue)) + for idx in [1, 2]: + if self.count() > idx: + app_name = self.queue[-idx] + widget: QueueWidget = self.__find_widget(app_name) + widget.toggle_arrows(len(self.queue) - idx, len(self.queue)) + + def __remove(self, app_name: str): + self.queue.remove(app_name) + widget: QueueWidget = self.__find_widget(app_name) + self.container.layout().removeWidget(widget) + widget.deleteLater() + self.__update_queue() + self.text.setVisible(not bool(self.count())) + self.container.setVisible(bool(self.count())) + + @pyqtSlot(str) + def remove(self, app_name: str): + self.__remove(app_name) + self.removed.emit(app_name) + + @pyqtSlot(InstallQueueItemModel) + def __on_force(self, item: InstallQueueItemModel): + self.__remove(item.options.app_name) + self.force.emit(item) + + class MoveDirection(IntEnum): + UP = -1 + DOWN = 1 + + def __move(self, app_name: str, direction: MoveDirection): + """ + Moved the widget for `app_name` up or down in the queue and the container + :param app_name: The app_name associated with the widget + :param direction: -1 to move up, +1 to move down + :return: None + """ + index = self.queue.index(app_name) + self.queue.remove(app_name) + self.queue.insert(index + int(direction), app_name) + widget: QueueWidget = self.__find_widget(app_name) + self.container.layout().insertWidget(index + int(direction), widget) + self.__update_queue() + + @pyqtSlot(str) + def __on_move_up(self, app_name: str): + self.__move(app_name, QueueGroup.MoveDirection.UP) + + @pyqtSlot(str) + def __on_move_down(self, app_name: str): + self.__move(app_name, QueueGroup.MoveDirection.DOWN) + + +if __name__ == "__main__": + import sys + from PyQt5.QtWidgets import QApplication, QDialog + from rare.utils.misc import set_style_sheet + + app = QApplication(sys.argv) + + set_style_sheet("RareStyle") + + queue_group = QueueGroup() + + dialog = QDialog() + dialog.setLayout(QVBoxLayout()) + dialog.layout().addWidget(queue_group) + for i in range(5): + rgame = Namespace(app_name=i, title=f"{i}", remote_version=f"{i}", version=f"{i}") + analysis = Namespace(dl_size=i*1024, install_size=i*2048) + game = Namespace(app_name=f"{i}") + download = Namespace(analysis=analysis, game=game) + model = InstallQueueItemModel(rgame=rgame, download=download, options=True) + queue_group.push_back(model) + dialog.layout().setSizeConstraint(QVBoxLayout.SetFixedSize) + dialog.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/rare/components/tabs/downloads/download_thread.py b/rare/components/tabs/downloads/thread.py similarity index 70% rename from rare/components/tabs/downloads/download_thread.py rename to rare/components/tabs/downloads/thread.py index 6e25cdd2..5d67511b 100644 --- a/rare/components/tabs/downloads/download_thread.py +++ b/rare/components/tabs/downloads/thread.py @@ -2,62 +2,75 @@ import os import platform import queue import time +from ctypes import c_ulonglong from dataclasses import dataclass from enum import IntEnum from logging import getLogger from typing import List, Optional, Dict from PyQt5.QtCore import QThread, pyqtSignal, QProcess -from legendary.core import LegendaryCore from rare.lgndr.cli import LegendaryCLI +from rare.lgndr.core import LegendaryCore from rare.lgndr.glue.monkeys import DLManagerSignals from rare.lgndr.models.downloading import UIUpdate +from rare.models.game import RareGame from rare.models.install import InstallQueueItemModel -from rare.shared import GlobalSignalsSingleton, ArgumentsSingleton logger = getLogger("DownloadThread") -class DownloadThread(QThread): - @dataclass - class ReturnStatus: - class ReturnCode(IntEnum): - ERROR = 1 - STOPPED = 2 - FINISHED = 3 +class DlResultCode(IntEnum): + ERROR = 1 + STOPPED = 2 + FINISHED = 3 - app_name: str - ret_code: ReturnCode = ReturnCode.ERROR - message: str = "" - dlcs: Optional[List[Dict]] = None - sync_saves: bool = False - tip_url: str = "" - shortcuts: bool = False +@dataclass +class DlResultModel: + item: InstallQueueItemModel + code: DlResultCode = DlResultCode.ERROR + message: str = "" + dlcs: Optional[List[Dict]] = None + sync_saves: bool = False + tip_url: str = "" + shortcuts: bool = False - ret_status = pyqtSignal(ReturnStatus) - ui_update = pyqtSignal(str, UIUpdate) +class DlThread(QThread): + result = pyqtSignal(DlResultModel) + progress = pyqtSignal(UIUpdate, c_ulonglong) - def __init__(self, core: LegendaryCore, item: InstallQueueItemModel): - super(DownloadThread, self).__init__() - self.signals = GlobalSignalsSingleton() + def __init__(self, item: InstallQueueItemModel, rgame: RareGame, core: LegendaryCore, debug: bool = False): + super(DlThread, self).__init__() + self.dlm_signals: DLManagerSignals = DLManagerSignals() self.core: LegendaryCore = core self.item: InstallQueueItemModel = item - self.dlm_signals: DLManagerSignals = DLManagerSignals() + self.dl_size = c_ulonglong(item.download.analysis.dl_size) + self.rgame = rgame + self.debug = debug + + def __result_emit(self, result): + if result.code == DlResultCode.FINISHED: + self.rgame.set_installed(True) + self.rgame.state = RareGame.State.IDLE + self.rgame.signals.progress.finish.emit(not result.code == DlResultCode.FINISHED) + self.result.emit(result) def run(self): cli = LegendaryCLI(self.core) self.item.download.dlm.logging_queue = cli.logging_queue - self.item.download.dlm.proc_debug = ArgumentsSingleton().debug - ret = DownloadThread.ReturnStatus(self.item.download.game.app_name) + self.item.download.dlm.proc_debug = self.debug + result = DlResultModel(self.item) start_t = time.time() try: self.item.download.dlm.start() + self.rgame.state = RareGame.State.DOWNLOADING + self.rgame.signals.progress.start.emit() time.sleep(1) while self.item.download.dlm.is_alive(): try: - self.ui_update.emit(self.item.download.game.app_name, - self.item.download.dlm.status_queue.get(timeout=1.0)) + status = self.item.download.dlm.status_queue.get(timeout=1.0) + self.rgame.signals.progress.update.emit(int(status.progress)) + self.progress.emit(status, self.dl_size) except queue.Empty: pass if self.dlm_signals.update: @@ -68,28 +81,29 @@ class DownloadThread(QThread): time.sleep(self.item.download.dlm.update_interval / 10) self.item.download.dlm.join() except Exception as e: + self.kill() + self.item.download.dlm.join() end_t = time.time() logger.error(f"Installation failed after {end_t - start_t:.02f} seconds.") logger.warning(f"The following exception occurred while waiting for the downloader to finish: {e!r}.") - ret.ret_code = ret.ReturnCode.ERROR - ret.message = f"{e!r}" - self.ret_status.emit(ret) + result.code = DlResultCode.ERROR + result.message = f"{e!r}" + self.__result_emit(result) return else: end_t = time.time() if self.dlm_signals.kill is True: logger.info(f"Download stopped after {end_t - start_t:.02f} seconds.") - ret.ret_code = ret.ReturnCode.STOPPED - self.ret_status.emit(ret) + result.code = DlResultCode.STOPPED + self.__result_emit(result) return logger.info(f"Download finished in {end_t - start_t:.02f} seconds.") - ret.ret_code = ret.ReturnCode.FINISHED + result.code = DlResultCode.FINISHED if self.item.options.overlay: - self.signals.application.overlay_installed.emit() self.core.finish_overlay_install(self.item.download.igame) - self.ret_status.emit(ret) + self.__result_emit(result) return if not self.item.options.no_install: @@ -105,9 +119,9 @@ class DownloadThread(QThread): dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name) if dlcs and not self.item.options.skip_dlcs: - ret.dlcs = [] + result.dlcs = [] for dlc in dlcs: - ret.dlcs.append( + result.dlcs.append( { "app_name": dlc.app_name, "app_title": dlc.app_title, @@ -119,11 +133,11 @@ class DownloadThread(QThread): self.item.download.game.supports_cloud_saves or self.item.download.game.supports_mac_cloud_saves ) and not self.item.download.game.is_dlc: - ret.sync_saves = True + result.sync_saves = True # show tip again after installation finishes so users hopefully actually see it if tip_url := self.core.get_game_tip(self.item.download.igame.app_name): - ret.tip_url = tip_url + result.tip_url = tip_url LegendaryCLI(self.core).install_game_cleanup( self.item.download.game, @@ -133,9 +147,9 @@ class DownloadThread(QThread): ) if not self.item.options.update and self.item.options.create_shortcut: - ret.shortcuts = True + result.shortcuts = True - self.ret_status.emit(ret) + self.__result_emit(result) def _handle_postinstall(self, postinstall, igame): logger.info("This game lists the following prerequisites to be installed:") diff --git a/rare/components/tabs/downloads/widgets.py b/rare/components/tabs/downloads/widgets.py new file mode 100644 index 00000000..ed1f5e66 --- /dev/null +++ b/rare/components/tabs/downloads/widgets.py @@ -0,0 +1,123 @@ +from typing import Optional + +from PyQt5.QtCore import pyqtSignal, Qt +from PyQt5.QtWidgets import QWidget, QFrame +from legendary.models.downloading import AnalysisResult +from legendary.models.game import Game, InstalledGame +from qtawesome import icon + +from rare.models.install import InstallQueueItemModel, InstallOptionsModel +from rare.shared import ImageManagerSingleton +from rare.ui.components.tabs.downloads.download_widget import Ui_DownloadWidget +from rare.ui.components.tabs.downloads.info_widget import Ui_InfoWidget +from rare.utils.misc import get_size, widget_object_name +from rare.widgets.image_widget import ImageWidget, ImageSize + + +class InfoWidget(QWidget): + def __init__( + self, + game: Game, + igame: InstalledGame, + analysis: Optional[AnalysisResult] = None, + old_igame: Optional[InstalledGame] = None, + parent=None, + ): + super(InfoWidget, self).__init__(parent=parent) + self.ui = Ui_InfoWidget() + self.ui.setupUi(self) + + self.image_manager = ImageManagerSingleton() + + self.ui.title.setText(game.app_title) + self.ui.remote_version.setText(old_igame.version if old_igame else game.app_version(igame.platform)) + self.ui.local_version.setText(igame.version) + self.ui.dl_size.setText(get_size(analysis.dl_size) if analysis else "") + self.ui.install_size.setText(get_size(analysis.install_size) if analysis else "") + + self.image = ImageWidget(self) + self.image.setFixedSize(ImageSize.Icon) + self.image.setPixmap(self.image_manager.get_pixmap(game.app_name, color=True)) + self.ui.image_layout.addWidget(self.image) + + self.ui.info_widget_layout.setAlignment(Qt.AlignTop) + + +class UpdateWidget(QFrame): + enqueue = pyqtSignal(InstallOptionsModel) + + def __init__(self, game: Game, igame: InstalledGame, parent=None): + super(UpdateWidget, self).__init__(parent=parent) + self.ui = Ui_DownloadWidget() + self.ui.setupUi(self) + # lk: setObjectName has to be after `setupUi` because it is also set in that function + self.setObjectName(widget_object_name(self, game.app_name)) + + self.game = game + self.igame = igame + + self.ui.queue_buttons.setVisible(False) + self.ui.move_buttons.setVisible(False) + + self.info_widget = InfoWidget(game, igame, parent=self) + self.ui.info_layout.addWidget(self.info_widget) + + self.ui.update_button.clicked.connect(lambda: self.update_game(True)) + self.ui.settings_button.clicked.connect(lambda: self.update_game(False)) + + def update_game(self, auto: bool): + self.ui.update_button.setDisabled(True) + self.ui.settings_button.setDisabled(True) + self.enqueue.emit(InstallOptionsModel(app_name=self.game.app_name, update=True, silent=auto)) # True if settings + + def set_enabled(self, enabled: bool): + self.ui.update_button.setEnabled(enabled) + self.ui.settings_button.setEnabled(enabled) + + def version(self) -> str: + return self.game.app_version(self.igame.platform) + + +class QueueWidget(QFrame): + # str: app_name + move_up = pyqtSignal(str) + # str: app_name + move_down = pyqtSignal(str) + # str: app_name + remove = pyqtSignal(str) + # InstallQueueItemModel + force = pyqtSignal(InstallQueueItemModel) + + + def __init__(self, item: InstallQueueItemModel, old_igame: InstalledGame, parent=None): + super(QueueWidget, self).__init__(parent=parent) + self.ui = Ui_DownloadWidget() + self.ui.setupUi(self) + # lk: setObjectName has to be after `setupUi` because it is also set in that function + self.setObjectName(widget_object_name(self, item.options.app_name)) + + self.item = item + + self.ui.update_buttons.setVisible(False) + + self.ui.move_up_button.setIcon(icon("fa.arrow-up")) + self.ui.move_up_button.clicked.connect( + lambda: self.move_up.emit(self.item.download.game.app_name) + ) + + self.ui.move_down_button.setIcon(icon("fa.arrow-down")) + self.ui.move_down_button.clicked.connect( + lambda: self.move_down.emit(self.item.download.game.app_name) + ) + + self.info_widget = InfoWidget( + item.download.game, item.download.igame, item.download.analysis, old_igame, parent=self + ) + self.ui.info_layout.addWidget(self.info_widget) + + self.ui.remove_button.clicked.connect(lambda: self.remove.emit(self.item.download.game.app_name)) + self.ui.force_button.clicked.connect(lambda: self.force.emit(self.item)) + + def toggle_arrows(self, index: int, length: int): + self.ui.move_up_button.setEnabled(bool(index)) + self.ui.move_down_button.setEnabled(bool(length - (index + 1))) diff --git a/rare/components/tabs/games/__init__.py b/rare/components/tabs/games/__init__.py index 9417cdcd..04be62d4 100644 --- a/rare/components/tabs/games/__init__.py +++ b/rare/components/tabs/games/__init__.py @@ -1,6 +1,6 @@ import time from logging import getLogger -from typing import Tuple, Dict, List, Set +from typing import Dict, List from PyQt5.QtCore import QSettings, Qt, pyqtSlot from PyQt5.QtWidgets import QStackedWidget, QVBoxLayout, QWidget, QScrollArea, QFrame @@ -39,14 +39,10 @@ class GamesTab(QStackedWidget): self.image_manager = ImageManagerSingleton() self.settings = QSettings() - self.widgets: Dict[str, Tuple[IconGameWidget, ListGameWidget]] = {} - self.game_updates: Set[RareGame] = set() self.active_filter: int = 0 self.game_list: List[Game] = self.api_results.game_list self.dlcs: Dict[str, List[Game]] = self.api_results.dlcs - self.bit32: List[str] = self.api_results.bit32_games - self.mac_games: List[str] = self.api_results.mac_games self.no_assets: List[Game] = self.api_results.no_asset_games self.game_utils = GameUtils(parent=self) @@ -71,18 +67,6 @@ class GamesTab(QStackedWidget): self.integrations_tabs.back_clicked.connect(lambda: self.setCurrentWidget(self.games)) self.addWidget(self.integrations_tabs) - for i in self.game_list: - if i.app_name.startswith("UE_4"): - pixmap = self.image_manager.get_pixmap(i.app_name) - if pixmap.isNull(): - continue - self.ue_name = i.app_name - logger.debug(f"Found Unreal AppName {self.ue_name}") - break - else: - logger.warning("No Unreal engine in library found") - self.ue_name = "" - self.no_asset_names = [] if not self.args.offline: for game in self.no_assets: @@ -108,9 +92,7 @@ class GamesTab(QStackedWidget): self.list_view.setLayout(QVBoxLayout(self.list_view)) self.list_view.layout().setContentsMargins(3, 3, 9, 3) self.list_view.layout().setAlignment(Qt.AlignTop) - self.library_controller = LibraryWidgetController( - self.icon_view, self.list_view, self - ) + self.library_controller = LibraryWidgetController(self.icon_view, self.list_view, self) self.icon_view_scroll.setWidget(self.icon_view) self.list_view_scroll.setWidget(self.list_view) self.view_stack.addWidget(self.icon_view_scroll) @@ -182,14 +164,15 @@ class GamesTab(QStackedWidget): # FIXME: Remove this when RareCore is in place def __create_game_with_dlcs(self, game: Game) -> RareGame: - rgame = RareGame(game, self.core, self.image_manager) - if rgame.has_update: - self.game_updates.add(rgame) + rgame = RareGame(self.core, self.image_manager, game) if game_dlcs := self.dlcs[rgame.game.catalog_item_id]: for dlc in game_dlcs: - rdlc = RareGame(dlc, self.core, self.image_manager) - if rdlc.has_update: - self.game_updates.add(rdlc) + rdlc = RareGame(self.core, self.image_manager, dlc) + self.rcore.add_game(rdlc) + # lk: plug dlc progress signals to the game's + rdlc.signals.progress.start.connect(rgame.signals.progress.start) + rdlc.signals.progress.update.connect(rgame.signals.progress.update) + rdlc.signals.progress.finish.connect(rgame.signals.progress.finish) rdlc.set_pixmap() rgame.owned_dlcs.append(rdlc) return rgame @@ -210,12 +193,11 @@ class GamesTab(QStackedWidget): def add_library_widget(self, rgame: RareGame): try: - icon_widget, list_widget = self.library_controller.add_game(rgame, self.game_utils, self) + icon_widget, list_widget = self.library_controller.add_game(rgame, self.game_utils) except Exception as e: raise e logger.error(f"{rgame.app_name} is broken. Don't add it to game list: {e}") return None, None - self.widgets[rgame.app_name] = (icon_widget, list_widget) icon_widget.show_info.connect(self.show_game_info) list_widget.show_info.connect(self.show_game_info) return icon_widget, list_widget diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py index f4bca665..5c5863a3 100644 --- a/rare/components/tabs/games/game_info/game_info.py +++ b/rare/components/tabs/games/game_info/game_info.py @@ -168,7 +168,7 @@ class GameInfo(QWidget): def verify_game(self, rgame: RareGame): self.ui.verify_widget.setCurrentIndex(1) - verify_worker = VerifyWorker(rgame, self.core, self.args) + verify_worker = VerifyWorker(self.core, self.args, rgame) verify_worker.signals.status.connect(self.verify_status) verify_worker.signals.result.connect(self.verify_result) verify_worker.signals.error.connect(self.verify_error) diff --git a/rare/components/tabs/games/game_widgets/__init__.py b/rare/components/tabs/games/game_widgets/__init__.py index c09da179..726ff958 100644 --- a/rare/components/tabs/games/game_widgets/__init__.py +++ b/rare/components/tabs/games/game_widgets/__init__.py @@ -4,9 +4,10 @@ from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtWidgets import QWidget from rare.lgndr.core import LegendaryCore +from rare.models.apiresults import ApiResults from rare.models.game import RareGame from rare.models.signals import GlobalSignals -from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton +from rare.shared import RareCore from rare.shared.game_utils import GameUtils from .icon_game_widget import IconGameWidget from .list_game_widget import ListGameWidget @@ -17,33 +18,21 @@ class LibraryWidgetController(QObject): super(LibraryWidgetController, self).__init__(parent=parent) self._icon_container: QWidget = icon_container self._list_container: QWidget = list_container - self.core: LegendaryCore = LegendaryCoreSingleton() - self.signals: GlobalSignals = GlobalSignalsSingleton() - self.api_results = ApiResultsSingleton() + self.rcore = RareCore.instance() + self.core: LegendaryCore = self.rcore.core() + self.signals: GlobalSignals = self.rcore.signals() + self.api_results: ApiResults = self.rcore.api_results() - self.signals.progress.started.connect(self.start_progress) - self.signals.progress.value.connect(self.update_progress) - self.signals.progress.finished.connect(self.finish_progress) + def add_game(self, rgame: RareGame, game_utils: GameUtils): + return self.add_widgets(rgame, game_utils) - self.signals.game.installed.connect(self.on_install) - self.signals.game.uninstalled.connect(self.on_uninstall) - self.signals.game.verified.connect(self.on_verified) - - def add_game(self, rgame: RareGame, game_utils: GameUtils, parent): - return self.add_widgets(rgame, game_utils, parent) - - def add_widgets(self, rgame: RareGame, game_utils: GameUtils, parent) -> Tuple[IconGameWidget, ListGameWidget]: - icon_widget = IconGameWidget(rgame, game_utils, parent) - list_widget = ListGameWidget(rgame, game_utils, parent) + def add_widgets(self, rgame: RareGame, game_utils: GameUtils) -> Tuple[IconGameWidget, ListGameWidget]: + icon_widget = IconGameWidget(rgame, game_utils, self._icon_container) + list_widget = ListGameWidget(rgame, game_utils, self._list_container) return icon_widget, list_widget - def __find_widget(self, app_name: str) -> Tuple[Union[IconGameWidget, None], Union[ListGameWidget, None]]: - iw = self._icon_container.findChild(IconGameWidget, app_name) - lw = self._list_container.findChild(ListGameWidget, app_name) - return iw, lw - @staticmethod - def __visibility(widget, filter_name, search_text) -> Tuple[bool, float]: + def __visibility(widget: Union[IconGameWidget,ListGameWidget], filter_name, search_text) -> Tuple[bool, float]: if filter_name == "installed": visible = widget.rgame.is_installed elif filter_name == "offline": @@ -92,7 +81,12 @@ class LibraryWidgetController(QObject): self._icon_container.layout().sort(lambda x: (sort_by not in x.widget().rgame.app_title.lower(),)) else: self._icon_container.layout().sort( - lambda x: ( + key=lambda x: ( + # Sort by grant date + # x.widget().rgame.is_installed, + # not x.widget().rgame.is_non_asset, + # x.widget().rgame.grant_date(), + # ), reverse=True not x.widget().rgame.is_installed, x.widget().rgame.is_non_asset, x.widget().rgame.app_title, @@ -103,6 +97,8 @@ class LibraryWidgetController(QObject): list_widgets.sort(key=lambda x: (sort_by not in x.rgame.app_title.lower(),)) else: list_widgets.sort( + # Sort by grant date + # key=lambda x: (x.rgame.is_installed, not x.rgame.is_non_asset, x.rgame.grant_date()), reverse=True key=lambda x: (not x.rgame.is_installed, x.rgame.is_non_asset, x.rgame.app_title) ) for idx, wl in enumerate(list_widgets): @@ -122,41 +118,19 @@ class LibraryWidgetController(QObject): new_icon_app_names = game_app_names.difference(icon_app_names) new_list_app_names = game_app_names.difference(list_app_names) for app_name in new_icon_app_names: - game = self.rare_core.get_game(app_name) + game = self.rcore.get_game(app_name) iw = IconGameWidget(game) self._icon_container.layout().addWidget(iw) for app_name in new_list_app_names: - game = self.rare_core.get_game(app_name) + game = self.rcore.get_game(app_name) lw = ListGameWidget(game) self._list_container.layout().addWidget(lw) self.sort_list() - @pyqtSlot(list) - def on_install(self, app_names: List[str]): - for app_name in app_names: - iw, lw = self.__find_widget(app_name) - if iw is not None: - iw.rgame.set_installed(True) - if lw is not None: - lw.rgame.set_installed(True) - self.sort_list() - - @pyqtSlot(str) - def on_uninstall(self, app_name: str): - iw, lw = self.__find_widget(app_name) - if iw is not None: - iw.rgame.set_installed(False) - if lw is not None: - lw.rgame.set_installed(False) - self.sort_list() - - @pyqtSlot(str) - def on_verified(self, app_name: str): - iw, lw = self.__find_widget(app_name) - if iw is not None: - iw.rgame.needs_verification = False - if lw is not None: - lw.rgame.needs_verification = False + def __find_widget(self, app_name: str) -> Tuple[Union[IconGameWidget, None], Union[ListGameWidget, None]]: + iw = self._icon_container.findChild(IconGameWidget, app_name) + lw = self._list_container.findChild(ListGameWidget, app_name) + return iw, lw # lk: this should go in downloads and happen once def __find_game_for_dlc(self, app_name: str) -> Optional[str]: @@ -174,27 +148,3 @@ class LibraryWidgetController(QObject): ) return game[0].app_name return app_name - - @pyqtSlot(str) - def start_progress(self, app_name: str): - iw, lw = self.__find_widget(app_name) - if iw is not None: - iw.rgame.start_progress() - if lw is not None: - lw.rgame.start_progress() - - @pyqtSlot(str, int) - def update_progress(self, app_name: str, progress: int): - iw, lw = self.__find_widget(app_name) - if iw is not None: - iw.rgame.update_progress(progress) - if lw is not None: - lw.rgame.update_progress(progress) - - @pyqtSlot(str, bool) - def finish_progress(self, app_name: str, stopped: bool): - iw, lw = self.__find_widget(app_name) - if iw is not None: - iw.rgame.finish_progress(not stopped, 0, "") - if lw is not None: - lw.rgame.finish_progress(not stopped, 0, "") diff --git a/rare/components/tabs/games/game_widgets/game_widget.py b/rare/components/tabs/games/game_widgets/game_widget.py index ba0646a7..76b686d5 100644 --- a/rare/components/tabs/games/game_widgets/game_widget.py +++ b/rare/components/tabs/games/game_widgets/game_widget.py @@ -108,7 +108,7 @@ class GameWidget(LibraryWidget): self.addAction(uninstall) uninstall.triggered.connect( lambda: self.signals.game.uninstalled.emit(self.rgame.app_name) - if self.game_utils.uninstall_game(self.rgame.app_name) + if self.game_utils.uninstall_game(self.rgame) else None ) diff --git a/rare/components/tabs/games/game_widgets/icon_game_widget.py b/rare/components/tabs/games/game_widgets/icon_game_widget.py index c9f14c19..b5e48908 100644 --- a/rare/components/tabs/games/game_widgets/icon_game_widget.py +++ b/rare/components/tabs/games/game_widgets/icon_game_widget.py @@ -1,6 +1,7 @@ from logging import getLogger +from typing import Optional -from PyQt5.QtCore import QEvent, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QEvent, pyqtSlot from rare.models.game import RareGame from rare.shared.game_utils import GameUtils @@ -12,9 +13,6 @@ logger = getLogger("IconGameWidget") class IconGameWidget(GameWidget): - is_ready = False - update_game = pyqtSignal() - def __init__(self, rgame: RareGame, game_utils: GameUtils, parent=None): super(IconGameWidget, self).__init__(rgame, game_utils, parent) self.setObjectName(f"{rgame.app_name}") @@ -30,7 +28,7 @@ class IconGameWidget(GameWidget): self.ui.install_btn.clicked.connect(self.install) self.ui.install_btn.setVisible(not self.rgame.is_installed) - self.game_utils.game_launched.connect(self.game_started) + # self.game_utils.game_launched.connect(self.game_started) self.is_ready = True self.ui.launch_btn.setEnabled(self.rgame.can_launch) @@ -41,13 +39,13 @@ class IconGameWidget(GameWidget): def set_status(self): super(IconGameWidget, self).set_status(self.ui.status_label) - def enterEvent(self, a0: QEvent = None) -> None: + def enterEvent(self, a0: Optional[QEvent] = None) -> None: if a0 is not None: a0.accept() self.ui.tooltip_label.setText(self.enterEventText) self.ui.enterAnimation(self) - def leaveEvent(self, a0: QEvent = None) -> None: + def leaveEvent(self, a0: Optional[QEvent] = None) -> None: if a0 is not None: a0.accept() self.ui.leaveAnimation(self) diff --git a/rare/components/tabs/games/game_widgets/icon_widget.py b/rare/components/tabs/games/game_widgets/icon_widget.py index e184ece5..e0572e83 100644 --- a/rare/components/tabs/games/game_widgets/icon_widget.py +++ b/rare/components/tabs/games/game_widgets/icon_widget.py @@ -133,7 +133,7 @@ class IconWidget(object): # layout for the image, holds the mini widget and a spacer item image_layout = QVBoxLayout() - image_layout.setContentsMargins(0, 0, 0, 0) + image_layout.setContentsMargins(2, 2, 2, 2) # layout for the mini widget, holds the top row and the info label mini_layout = QVBoxLayout() diff --git a/rare/components/tabs/games/game_widgets/list_game_widget.py b/rare/components/tabs/games/game_widgets/list_game_widget.py index 25bf6c42..71fd9acd 100644 --- a/rare/components/tabs/games/game_widgets/list_game_widget.py +++ b/rare/components/tabs/games/game_widgets/list_game_widget.py @@ -22,8 +22,6 @@ logger = getLogger("ListGameWidget") class ListGameWidget(GameWidget): - signal = pyqtSignal(str) - update_game = pyqtSignal() def __init__(self, rgame: RareGame, game_utils: GameUtils, parent=None): super(ListGameWidget, self).__init__(rgame, game_utils, parent) diff --git a/rare/components/tabs/games/integrations/eos_group.py b/rare/components/tabs/games/integrations/eos_group.py index e9feb511..3c5f67b4 100644 --- a/rare/components/tabs/games/integrations/eos_group.py +++ b/rare/components/tabs/games/integrations/eos_group.py @@ -209,7 +209,9 @@ class EOSGroup(QGroupBox, Ui_EosWidget): self.enabled_cb.setChecked(enabled) def install_overlay(self, update=False): - base_path = os.path.expanduser("~/legendary/.overlay") + base_path = os.path.join( + self.core.lgd.config.get("Legendary", "install_dir", fallback=os.path.expanduser("~/legendary")),".overlay" + ) if update: if not self.overlay: self.overlay_stack.setCurrentIndex(1) @@ -218,8 +220,9 @@ class EOSGroup(QGroupBox, Ui_EosWidget): return base_path = self.overlay.install_path - options = InstallOptionsModel(app_name="", base_path=base_path, - platform="Windows", overlay=True) + options = InstallOptionsModel( + app_name=eos.EOSOverlayApp.app_name, base_path=base_path, platform="Windows", overlay=True + ) self.signals.game.install.emit(options) diff --git a/rare/components/tray_icon.py b/rare/components/tray_icon.py index 6b6aa57b..381ad252 100644 --- a/rare/components/tray_icon.py +++ b/rare/components/tray_icon.py @@ -66,8 +66,7 @@ class TrayIcon(QSystemTrayIcon): a = QAction(rgame.app_title) a.setProperty("app_name", rgame.app_name) a.triggered.connect( - lambda: self.__parent.tab_widget.games_tab.game_utils.prepare_launch( - self.sender().property("app_name")) + lambda: self.rcore.get_game(self.sender().property("app_name")).launch() ) self.menu.insertAction(self.separator, a) self.game_actions.append(a) diff --git a/rare/models/game.py b/rare/models/game.py index 6a6b4400..bc8df777 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -12,11 +12,10 @@ from legendary.models.game import Game, InstalledGame, SaveGameFile from rare.lgndr.core import LegendaryCore from rare.models.install import InstallOptionsModel -from rare.shared.image_manager import ImageManager from rare.shared.game_process import GameProcess -from rare.utils.paths import data_dir +from rare.shared.image_manager import ImageManager from rare.utils.misc import get_rare_executable - +from rare.utils.paths import data_dir logger = getLogger("RareGame") @@ -32,25 +31,31 @@ class RareGame(QObject): @dataclass class Metadata: + auto_update: bool = False queued: bool = False queue_pos: Optional[int] = None last_played: Optional[datetime] = None + grant_date: Optional[datetime] = None tags: List[str] = field(default_factory=list) @classmethod def from_dict(cls, data: Dict): return cls( + auto_update=data.get("auto_update", False), queued=data.get("queued", False), queue_pos=data.get("queue_pos", None), - last_played=datetime.strptime(data.get("last_played", "None"), "%Y-%m-%dT%H:%M:%S.%f"), + last_played=datetime.fromisoformat(data["last_played"]) if data.get("last_played", None) else None, + grant_date=datetime.fromisoformat(data["grant_date"]) if data.get("grant_date", None) else None, tags=data.get("tags", []), ) def as_dict(self): return dict( + auto_update=self.auto_update, queued=self.queued, queue_pos=self.queue_pos, - last_played=self.last_played.strftime("%Y-%m-%dT%H:%M:%S.%f"), + last_played=self.last_played.isoformat() if self.last_played else None, + grant_date=self.grant_date.isoformat() if self.grant_date else None, tags=self.tags, ) @@ -68,9 +73,10 @@ class RareGame(QObject): class Game(QObject): install = pyqtSignal(InstallOptionsModel) - uninstalled = pyqtSignal() - launched = pyqtSignal() - finished = pyqtSignal() + installed = pyqtSignal(str) + uninstalled = pyqtSignal(str) + launched = pyqtSignal(str) + finished = pyqtSignal(str) def __init__(self): super(RareGame.Signals, self).__init__() @@ -78,7 +84,7 @@ class RareGame(QObject): self.widget = RareGame.Signals.Widget() self.game = RareGame.Signals.Game() - def __init__(self, game: Game, legendary_core: LegendaryCore, image_manager: ImageManager): + def __init__(self, legendary_core: LegendaryCore, image_manager: ImageManager, game: Game): super(RareGame, self).__init__() self.signals = RareGame.Signals() @@ -108,29 +114,30 @@ class RareGame(QObject): self.state = RareGame.State.IDLE - self.game_process = GameProcess(self) + self.game_process = GameProcess(self.game) self.game_process.launched.connect(self.__game_launched) self.game_process.finished.connect(self.__game_finished) if self.is_installed and not self.is_dlc: - self.game_process.connect(on_startup=True) + self.game_process.connect_to_server(on_startup=True) + # self.grant_date(True) @pyqtSlot(int) def __game_launched(self, code: int): - self.state = RareGame.State.RUNNING if code == GameProcess.Code.ON_STARTUP: return + self.state = RareGame.State.RUNNING self.metadata.last_played = datetime.now() self.__save_metadata() self.signals.game.launched.emit() @pyqtSlot(int) def __game_finished(self, exit_code: int): - self.state = RareGame.State.IDLE if exit_code == GameProcess.Code.ON_STARTUP: return + self.state = RareGame.State.IDLE self.signals.game.finished.emit() - __metadata_json: Optional[Dict] = None + __metadata_json: Dict = None @staticmethod def __load_metadata_json() -> Dict: @@ -270,9 +277,11 @@ class RareGame(QObject): @return None """ if installed: - self.igame = self.core.get_installed_game(self.app_name) + self.update_igame() + self.signals.game.installed.emit(self.app_name) else: self.igame = None + self.signals.game.uninstalled.emit(self.app_name) self.set_pixmap() @property @@ -401,14 +410,33 @@ class RareGame(QObject): @return bool If the game is an Origin game """ - return self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") == "Origin" + return ( + self.game.metadata.get("customAttributes", {}).get("ThirdPartyManagedApp", {}).get("value") + == "Origin" + ) + + def grant_date(self, force=False) -> datetime: + if self.metadata.grant_date is None or force: + entitlements = self.core.lgd.entitlements + matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements) + entitlement = next(matching, None) + grant_date = datetime.fromisoformat( + entitlement["grantDate"].replace("Z", "+00:00") + ) if entitlement else None + if force: + print(grant_date) + self.metadata.grant_date = grant_date + self.__save_metadata() + return self.metadata.grant_date @property def can_launch(self) -> bool: if self.is_installed: - if self.is_non_asset: + if self.is_origin: return True - if self.state == RareGame.State.RUNNING or self.needs_verification: + if not self.state == RareGame.State.IDLE or self.needs_verification: + return False + if self.is_foreign and not self.can_run_offline: return False return True return False @@ -442,23 +470,24 @@ class RareGame(QObject): def repair(self, repair_and_update): self.signals.game.install.emit( InstallOptionsModel( - app_name=self.app_name, repair_mode=True, repair_and_update=repair_and_update, update=True + app_name=self.app_name, repair_mode=True, repair_and_update=repair_and_update, update=repair_and_update ) ) def launch( - self, - offline: bool = False, - skip_update_check: bool = False, - wine_bin: str = None, - wine_pfx: str = None, - ask_sync_saves: bool = False, + self, + offline: bool = False, + skip_update_check: bool = False, + wine_bin: Optional[str] = None, + wine_pfx: Optional[str] = None, + ask_sync_saves: bool = False, ): - executable = get_rare_executable() - executable, args = executable[0], executable[1:] - args.extend([ - "start", self.app_name - ]) + if not self.can_launch: + return + + cmd_line = get_rare_executable() + executable, args = cmd_line[0], cmd_line[1:] + args.extend(["start", self.app_name]) if offline: args.append("--offline") if skip_update_check: @@ -473,4 +502,52 @@ class RareGame(QObject): # kill me, if I don't change it before commit QProcess.startDetached(executable, args) logger.info(f"Start new Process: ({executable} {' '.join(args)})") - self.game_process.connect(on_startup=False) + self.game_process.connect_to_server(on_startup=False) + + +class RareEosOverlay(QObject): + def __init__(self, legendary_core: LegendaryCore, image_manager: ImageManager, game: Game): + super(RareEosOverlay, self).__init__() + self.signals = RareGame.Signals() + + self.core = legendary_core + self.image_manager = image_manager + + self.game: Game = game + + # None if origin or not installed + self.igame: Optional[InstalledGame] = self.core.lgd.get_overlay_install_info() + + self.state = RareGame.State.IDLE + + @property + def app_name(self) -> str: + return self.igame.app_name if self.igame is not None else self.game.app_name + + @property + def app_title(self) -> str: + return self.igame.title if self.igame is not None else self.game.app_title + + @property + def title(self) -> str: + return self.app_title + + @property + def is_installed(self) -> bool: + return self.igame is not None + + def set_installed(self, installed: bool) -> None: + if installed: + self.igame = self.core.lgd.get_overlay_install_info() + self.signals.game.installed.emit(self.app_name) + else: + self.igame = None + self.signals.game.uninstalled.emit(self.app_name) + + @property + def is_mac(self) -> bool: + return False + + @property + def is_win32(self) -> bool: + return False diff --git a/rare/models/install.py b/rare/models/install.py index 6b5d6122..33afa730 100644 --- a/rare/models/install.py +++ b/rare/models/install.py @@ -58,8 +58,8 @@ class InstallDownloadModel: @dataclass class InstallQueueItemModel: - download: Optional[InstallDownloadModel] = None options: Optional[InstallOptionsModel] = None + download: Optional[InstallDownloadModel] = None def __bool__(self): return (self.download is not None) and (self.options is not None) diff --git a/rare/models/signals.py b/rare/models/signals.py index c9b6115f..e828756e 100644 --- a/rare/models/signals.py +++ b/rare/models/signals.py @@ -20,28 +20,18 @@ class GlobalSignals: # none update_tray = pyqtSignal() - class ProgressSignals(QObject): - # str: app_name - started = pyqtSignal(str) - # str: app_name, int: progress - value = pyqtSignal(str, int) - # str: app_name, bool: stopped - finished = pyqtSignal(str, bool) - class GameSignals(QObject): install = pyqtSignal(InstallOptionsModel) - # list: app_name - installed = pyqtSignal(list) + # str: app_name + installed = pyqtSignal(str) # str: app_name uninstalled = pyqtSignal(str) - # str: app_name - verified = pyqtSignal(str) class DownloadSignals(QObject): # str: app_name - enqueue_game = pyqtSignal(str) - # none - update_tab = pyqtSignal() + enqueue = pyqtSignal(str) + # str: app_name + dequeue = pyqtSignal(str) class DiscordRPCSignals(QObject): # str: app_title @@ -51,7 +41,6 @@ class GlobalSignals: def __init__(self): self.application = GlobalSignals.ApplicationSignals() - self.progress = GlobalSignals.ProgressSignals() self.game = GlobalSignals.GameSignals() self.download = GlobalSignals.DownloadSignals() self.discord_rpc = GlobalSignals.DiscordRPCSignals() @@ -59,8 +48,6 @@ class GlobalSignals: def deleteLater(self): self.application.deleteLater() del self.application - self.progress.deleteLater() - del self.progress self.game.deleteLater() del self.game self.download.deleteLater() diff --git a/rare/shared/game_process.py b/rare/shared/game_process.py index 0533a7a7..537f5190 100644 --- a/rare/shared/game_process.py +++ b/rare/shared/game_process.py @@ -46,7 +46,7 @@ class GameProcess(QObject): self.socket.readyRead.connect(self.__on_message) self.socket.disconnected.connect(self.__close) - def connect(self, on_startup: bool): + def connect_to_server(self, on_startup: bool): self.on_startup = on_startup self.timer.start(200) diff --git a/rare/shared/game_utils.py b/rare/shared/game_utils.py index 71f789e8..acdf974f 100644 --- a/rare/shared/game_utils.py +++ b/rare/shared/game_utils.py @@ -67,7 +67,6 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_co class GameUtils(QObject): finished = pyqtSignal(str, str) # app_name, error cloud_save_finished = pyqtSignal(str) - game_launched = pyqtSignal(RareGame) update_list = pyqtSignal(str) def __init__(self, parent=None): @@ -105,7 +104,8 @@ class GameUtils(QObject): success, message = uninstall_game(self.core, rgame.app_name, keep_files, keep_config) if not success: QMessageBox.warning(None, self.tr("Uninstall - {}").format(rgame.title), message, QMessageBox.Close) - self.signals.game.uninstalled.emit(rgame.app_name) + rgame.set_installed(False) + self.signals.download.dequeue.emit(rgame.app_name) return True def prepare_launch( diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index 60b9ae29..bce59aed 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -6,11 +6,12 @@ from logging import getLogger from typing import Optional, Dict, Iterator, Callable from PyQt5.QtCore import QObject +from legendary.lfs.eos import EOSOverlayApp from legendary.models.game import Game, SaveGameFile from rare.lgndr.core import LegendaryCore from rare.models.apiresults import ApiResults -from rare.models.game import RareGame +from rare.models.game import RareGame, RareEosOverlay from rare.models.signals import GlobalSignals from .image_manager import ImageManager @@ -24,11 +25,11 @@ class RareCore(QObject): if self._instance is not None: raise RuntimeError("RareCore already initialized") super(RareCore, self).__init__() - self._args: Optional[Namespace] = None - self._signals: Optional[GlobalSignals] = None - self._core: Optional[LegendaryCore] = None - self._image_manager: Optional[ImageManager] = None - self._api_results: Optional[ApiResults] = None + self.__args: Optional[Namespace] = None + self.__signals: Optional[GlobalSignals] = None + self.__core: Optional[LegendaryCore] = None + self.__image_manager: Optional[ImageManager] = None + self.__api_results: Optional[ApiResults] = None self.args(args) self.signals(init=True) @@ -37,6 +38,8 @@ class RareCore(QObject): self.__games: Dict[str, RareGame] = {} + self.__eos_overlay_rgame = RareEosOverlay(self.__core, self.__image_manager, EOSOverlayApp) + RareCore._instance = self @staticmethod @@ -46,31 +49,31 @@ class RareCore(QObject): return RareCore._instance def signals(self, init: bool = False) -> GlobalSignals: - if self._signals is None and not init: + if self.__signals is None and not init: raise RuntimeError("Uninitialized use of GlobalSignalsSingleton") - if self._signals is not None and init: + if self.__signals is not None and init: raise RuntimeError("GlobalSignals already initialized") if init: - self._signals = GlobalSignals() - return self._signals + self.__signals = GlobalSignals() + return self.__signals def args(self, args: Namespace = None) -> Optional[Namespace]: - if self._args is None and args is None: + if self.__args is None and args is None: raise RuntimeError("Uninitialized use of ArgumentsSingleton") - if self._args is not None and args is not None: + if self.__args is not None and args is not None: raise RuntimeError("Arguments already initialized") if args is not None: - self._args = args - return self._args + self.__args = args + return self.__args def core(self, init: bool = False) -> LegendaryCore: - if self._core is None and not init: + if self.__core is None and not init: raise RuntimeError("Uninitialized use of LegendaryCoreSingleton") - if self._core is not None and init: + if self.__core is not None and init: raise RuntimeError("LegendaryCore already initialized") if init: try: - self._core = LegendaryCore() + self.__core = LegendaryCore() except configparser.MissingSectionHeaderError as e: logger.warning(f"Config is corrupt: {e}") if config_path := os.environ.get("XDG_CONFIG_HOME"): @@ -79,70 +82,74 @@ class RareCore(QObject): path = os.path.expanduser("~/.config/legendary") with open(os.path.join(path, "config.ini"), "w") as config_file: config_file.write("[Legendary]") - self._core = LegendaryCore() - if "Legendary" not in self._core.lgd.config.sections(): - self._core.lgd.config.add_section("Legendary") - self._core.lgd.save_config() + self.__core = LegendaryCore() + if "Legendary" not in self.__core.lgd.config.sections(): + self.__core.lgd.config.add_section("Legendary") + self.__core.lgd.save_config() # workaround if egl sync enabled, but no programdata_path # programdata_path might be unset if logging in through the browser - if self._core.egl_sync_enabled: - if self._core.egl.programdata_path is None: - self._core.lgd.config.remove_option("Legendary", "egl_sync") - self._core.lgd.save_config() + if self.__core.egl_sync_enabled: + if self.__core.egl.programdata_path is None: + self.__core.lgd.config.remove_option("Legendary", "egl_sync") + self.__core.lgd.save_config() else: - if not os.path.exists(self._core.egl.programdata_path): - self._core.lgd.config.remove_option("Legendary", "egl_sync") - self._core.lgd.save_config() - return self._core + if not os.path.exists(self.__core.egl.programdata_path): + self.__core.lgd.config.remove_option("Legendary", "egl_sync") + self.__core.lgd.save_config() + return self.__core def image_manager(self, init: bool = False) -> ImageManager: - if self._image_manager is None and not init: + if self.__image_manager is None and not init: raise RuntimeError("Uninitialized use of ImageManagerSingleton") - if self._image_manager is not None and init: + if self.__image_manager is not None and init: raise RuntimeError("ImageManager already initialized") - if self._image_manager is None: - self._image_manager = ImageManager(self.signals(), self.core()) - return self._image_manager + if self.__image_manager is None: + self.__image_manager = ImageManager(self.signals(), self.core()) + return self.__image_manager def api_results(self, res: ApiResults = None) -> Optional[ApiResults]: - if self._api_results is None and res is None: + if self.__api_results is None and res is None: raise RuntimeError("Uninitialized use of ApiResultsSingleton") - if self._api_results is not None and res is not None: + if self.__api_results is not None and res is not None: raise RuntimeError("ApiResults already initialized") if res is not None: - self._api_results = res - return self._api_results + self.__api_results = res + return self.__api_results def deleteLater(self) -> None: - del self._api_results - self._api_results = None + del self.__api_results + self.__api_results = None - self._image_manager.deleteLater() - del self._image_manager - self._image_manager = None + self.__image_manager.deleteLater() + del self.__image_manager + self.__image_manager = None - self._core.exit() - del self._core - self._core = None + self.__core.exit() + del self.__core + self.__core = None - self._signals.deleteLater() - del self._signals - self._signals = None + self.__signals.deleteLater() + del self.__signals + self.__signals = None - del self._args - self._args = None + del self.__args + self.__args = None RareCore._instance = None super(RareCore, self).deleteLater() def get_game(self, app_name: str) -> RareGame: + if app_name == EOSOverlayApp.app_name: + return self.__eos_overlay_rgame return self.__games[app_name] def add_game(self, rgame: RareGame) -> None: - rgame.signals.game.install.connect(self._signals.game.install) - rgame.signals.game.finished.connect(self._signals.application.update_tray) - rgame.signals.game.finished.connect(lambda: self._signals.discord_rpc.set_title.emit("")) + rgame.signals.game.install.connect(self.__signals.game.install) + rgame.signals.game.installed.connect(self.__signals.game.installed) + rgame.signals.game.uninstalled.connect(self.__signals.game.uninstalled) + rgame.signals.game.finished.connect(self.__signals.application.update_tray) + rgame.signals.game.finished.connect(lambda: self.__signals.discord_rpc.set_title.emit("")) self.__games[rgame.app_name] = rgame def __filter_games(self, condition: Callable[[RareGame], bool]) -> Iterator[RareGame]: diff --git a/rare/shared/workers/verify.py b/rare/shared/workers/verify.py index 1a9bb1eb..d75551c7 100644 --- a/rare/shared/workers/verify.py +++ b/rare/shared/workers/verify.py @@ -23,7 +23,7 @@ class VerifyWorker(QRunnable): num: int = 0 total: int = 1 # set default to 1 to avoid DivisionByZero before it is initialized - def __init__(self, rgame: RareGame, core: LegendaryCore, args: Namespace): + def __init__(self, core: LegendaryCore, args: Namespace, rgame: RareGame): sys.excepthook = sys.__excepthook__ super(VerifyWorker, self).__init__() self.signals = VerifyWorker.Signals() diff --git a/rare/ui/components/tabs/downloads/download_widget.py b/rare/ui/components/tabs/downloads/download_widget.py index 1ef498a0..5b5b20d4 100644 --- a/rare/ui/components/tabs/downloads/download_widget.py +++ b/rare/ui/components/tabs/downloads/download_widget.py @@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_DownloadWidget(object): def setupUi(self, DownloadWidget): DownloadWidget.setObjectName("DownloadWidget") - DownloadWidget.resize(574, 70) + DownloadWidget.resize(332, 70) DownloadWidget.setWindowTitle("DownloadWidget") DownloadWidget.setFrameShape(QtWidgets.QFrame.StyledPanel) self.download_widget_layout = QtWidgets.QHBoxLayout(DownloadWidget) @@ -54,12 +54,12 @@ class Ui_DownloadWidget(object): self.queue_buttons_layout = QtWidgets.QVBoxLayout(self.queue_buttons) self.queue_buttons_layout.setContentsMargins(0, 0, 0, 0) self.queue_buttons_layout.setObjectName("queue_buttons_layout") + self.force_button = QtWidgets.QPushButton(self.queue_buttons) + self.force_button.setObjectName("force_button") + self.queue_buttons_layout.addWidget(self.force_button) self.remove_button = QtWidgets.QPushButton(self.queue_buttons) self.remove_button.setObjectName("remove_button") self.queue_buttons_layout.addWidget(self.remove_button, 0, QtCore.Qt.AlignTop) - self.force_button = QtWidgets.QPushButton(self.queue_buttons) - self.force_button.setObjectName("force_button") - self.queue_buttons_layout.addWidget(self.force_button, 0, QtCore.Qt.AlignTop) self.download_widget_layout.addWidget(self.queue_buttons) self.update_buttons = QtWidgets.QWidget(DownloadWidget) self.update_buttons.setObjectName("update_buttons") @@ -79,8 +79,8 @@ class Ui_DownloadWidget(object): def retranslateUi(self, DownloadWidget): _translate = QtCore.QCoreApplication.translate - self.remove_button.setText(_translate("DownloadWidget", "Remove from queue")) self.force_button.setText(_translate("DownloadWidget", "Update now")) + self.remove_button.setText(_translate("DownloadWidget", "Remove from queue")) self.update_button.setText(_translate("DownloadWidget", "Update game")) self.settings_button.setText(_translate("DownloadWidget", "Update with settings")) diff --git a/rare/ui/components/tabs/downloads/download_widget.ui b/rare/ui/components/tabs/downloads/download_widget.ui index 59d01820..a5fcb3d8 100644 --- a/rare/ui/components/tabs/downloads/download_widget.ui +++ b/rare/ui/components/tabs/downloads/download_widget.ui @@ -6,7 +6,7 @@ 0 0 - 574 + 332 70 diff --git a/rare/utils/misc.py b/rare/utils/misc.py index ce766d9e..29356dc4 100644 --- a/rare/utils/misc.py +++ b/rare/utils/misc.py @@ -17,7 +17,7 @@ from PyQt5.QtCore import ( QDir, ) from PyQt5.QtGui import QPalette, QColor, QImage -from PyQt5.QtWidgets import qApp, QStyleFactory +from PyQt5.QtWidgets import qApp, QStyleFactory, QWidget from legendary.core import LegendaryCore from legendary.models.game import Game from requests.exceptions import HTTPError @@ -350,3 +350,7 @@ def icon(icn_str: str, fallback: str = None, **kwargs): if kwargs.get("color"): kwargs["color"] = "red" return qtawesome.icon("ei.error", **kwargs) + + +def widget_object_name(widget: QWidget, app_name: str) -> str: + return f"{type(widget).__name__}_{app_name}" \ No newline at end of file