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