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