1
0
Fork 0
mirror of synced 2024-06-21 12:00:25 +12:00

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>
This commit is contained in:
loathingKernel 2023-01-21 02:15:06 +02:00
parent 1470aa9eb3
commit 4063195b4d
28 changed files with 954 additions and 771 deletions

View file

@ -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'<h3>{header} "{self.game.app_title}"</h3>')
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'<h3>{header} "{self.rgame.app_title}"</h3>')
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

View file

@ -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()

View file

@ -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

View file

@ -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:

View file

@ -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: <b>{}</b> >> <b>{}</b>")
.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

View file

@ -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)

View file

@ -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_())

View file

@ -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:")

View file

@ -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)))

View file

@ -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

View file

@ -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)

View file

@ -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, "")

View file

@ -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
)

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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(

View file

@ -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]:

View file

@ -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()

View file

@ -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"))

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>574</width>
<width>332</width>
<height>70</height>
</rect>
</property>

View file

@ -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}"