From 9b10a487043fc556756368702abccd86c2aaacd7 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:29:52 +0200 Subject: [PATCH 01/29] ImageManager: Reduce thread count even more --- rare/shared/image_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rare/shared/image_manager.py b/rare/shared/image_manager.py index e3085d8d..3a2ba479 100644 --- a/rare/shared/image_manager.py +++ b/rare/shared/image_manager.py @@ -82,7 +82,7 @@ class ImageManager(QObject): self.device = ImageSize.Preset(1, QApplication.instance().devicePixelRatio()) self.threadpool = QThreadPool() - self.threadpool.setMaxThreadCount(6) + self.threadpool.setMaxThreadCount(4) def __img_dir(self, app_name: str) -> Path: return self.image_dir.joinpath(app_name) From 446cbd6c67df5decae3f55ecd3f99365ee63c572 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:25:56 +0200 Subject: [PATCH 02/29] Lgdnr: Check for old_igame before writing tags --- rare/lgndr/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rare/lgndr/cli.py b/rare/lgndr/cli.py index d0a4e8d2..f8263732 100644 --- a/rare/lgndr/cli.py +++ b/rare/lgndr/cli.py @@ -341,7 +341,7 @@ class LegendaryCLI(LegendaryCLIReal): self.core.uninstall_tag(old_igame) self.core.install_game(old_igame) - if old_igame.install_tags: + if old_igame and old_igame.install_tags: self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(old_igame.install_tags)) self.core.lgd.save_config() From 4e2ffaae93ccd3db5a8f163320fe52d24ece0847 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 11 Jan 2024 21:51:01 +0200 Subject: [PATCH 03/29] ImageSize: Use pixel ratio 1 for image downloads --- rare/models/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rare/models/image.py b/rare/models/image.py index 68a87c9e..c4145c08 100644 --- a/rare/models/image.py +++ b/rare/models/image.py @@ -58,10 +58,10 @@ class ImageSize: def base(self) -> 'ImageSize.Preset': return self.__base - Image = Preset(1, 2) + Image = Preset(1, 1) """! @brief Size and pixel ratio of the image on disk""" - ImageWide = Preset(1, 2, Orientation.Wide) + ImageWide = Preset(1, 1, Orientation.Wide) """! @brief Size and pixel ratio for wide 16/9 image on disk""" Display = Preset(1, 1, base=Image) From 50276384cc78d7b6d619858ef6a923bc2b296102 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 11 Jan 2024 21:37:04 +0200 Subject: [PATCH 04/29] QtRequests: Use QNetworkAccessManager's queue --- rare/utils/qt_requests.py | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/rare/utils/qt_requests.py b/rare/utils/qt_requests.py index 23f4dfc9..ebd1003f 100644 --- a/rare/utils/qt_requests.py +++ b/rare/utils/qt_requests.py @@ -8,7 +8,6 @@ import orjson from PyQt5.QtCore import QObject, pyqtSignal, QUrl, QUrlQuery, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QNetworkDiskCache -REQUEST_LIMIT = 8 USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36" RequestHandler = TypeVar("RequestHandler", bound=Callable[[Union[Dict, bytes]], None]) @@ -33,7 +32,6 @@ class QtRequests(QObject): self.log = getLogger(f"{type(self).__name__}_{type(parent).__name__}") self.manager = QNetworkAccessManager(self) self.manager.finished.connect(self.__on_finished) - self.manager.finished.connect(self.__process_next) self.cache = None if cache is not None: self.log.debug("Using cache dir %s", cache) @@ -44,8 +42,7 @@ class QtRequests(QObject): self.log.debug("Manager is authorized") self.token = token - self.__pending_requests = [] - self.__active_requests = {} + self.__active_requests: Dict[QNetworkReply, RequestQueueItem] = {} @staticmethod def __prepare_query(url, params) -> QUrl: @@ -58,7 +55,7 @@ class QtRequests(QObject): def __prepare_request(self, item: RequestQueueItem) -> QNetworkRequest: request = QNetworkRequest(item.url) - request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json;charset=UTF-8") + request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json; charset=UTF-8") request.setHeader(QNetworkRequest.UserAgentHeader, USER_AGENT) request.setAttribute(QNetworkRequest.RedirectPolicyAttribute, QNetworkRequest.NoLessSafeRedirectPolicy) if self.cache is not None: @@ -76,10 +73,7 @@ class QtRequests(QObject): def post(self, url: str, handler: RequestHandler, payload: dict): item = RequestQueueItem(method="post", url=QUrl(url), payload=payload, handlers=[handler]) - if len(self.__active_requests) < REQUEST_LIMIT: - self.__post(item) - else: - self.__pending_requests.append(item) + self.__post(item) def __get(self, item: RequestQueueItem): request = self.__prepare_request(item) @@ -90,10 +84,7 @@ class QtRequests(QObject): def get(self, url: str, handler: RequestHandler, payload: Dict = None, params: Dict = None): url = self.__prepare_query(url, params) if params is not None else QUrl(url) item = RequestQueueItem(method="get", url=url, payload=payload, handlers=[handler]) - if len(self.__active_requests) < REQUEST_LIMIT: - self.__get(item) - else: - self.__pending_requests.append(item) + self.__get(item) def __on_error(self, error: QNetworkReply.NetworkError) -> None: self.log.error(error) @@ -102,19 +93,9 @@ class QtRequests(QObject): def __parse_content_type(header) -> Tuple[str, str]: # lk: this looks weird but `cgi` is deprecated, PEP 594 suggests this way of parsing MIME m = Message() - m['content-type'] = header + m["content-type"] = header return m.get_content_type(), m.get_content_charset() - def __process_next(self): - if self.__pending_requests: - item = self.__pending_requests.pop(0) - if item.method == "post": - self.__post(item) - elif item.method == "get": - self.__get(item) - else: - raise NotImplementedError - @pyqtSlot(QNetworkReply) def __on_finished(self, reply: QNetworkReply): item = self.__active_requests.pop(reply, None) From 9d3e9fecea52f0a4bc9ffd71b02f9163a0127b7d Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:33:45 +0200 Subject: [PATCH 05/29] GameProcess: Add more information in the failure message when a timeout occurs --- rare/shared/game_process.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rare/shared/game_process.py b/rare/shared/game_process.py index b12b690a..07dbe2b1 100644 --- a/rare/shared/game_process.py +++ b/rare/shared/game_process.py @@ -64,7 +64,14 @@ class GameProcess(QObject): self.tried_connections += 1 if self.tried_connections > 50: # 10 seconds - QMessageBox.warning(None, "Error", self.tr("Connection to game process failed (Timeout)")) + QMessageBox.warning( + None, + self.tr("Error - {}").format(self.game.app_title), + self.tr( + "Connection to game launcher for {} failed due to timeout.\n" + "This is usually do it the game or Rare's game launcher already running" + ).format(self.game.app_name) + ) self.timer.stop() self.finished.emit(GameProcess.Code.TIMEOUT) From 971c31e8f42cb1cb89ac40af20a1ccebee90480f Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Tue, 9 Jan 2024 16:32:53 +0200 Subject: [PATCH 06/29] SelectiveWidget: Create specialized widget for selecting optional downloads Because there are two dialogs for editing optional downloads, refactor the separate implementations to select them into a common reusable widget and use it in SelectiveDialog and InstallDialog. --- rare/components/dialogs/install_dialog.py | 125 ++++++++----------- rare/components/dialogs/selective_dialog.py | 78 ++---------- rare/ui/components/dialogs/install_dialog.py | 2 +- rare/ui/components/dialogs/install_dialog.ui | 4 +- rare/widgets/collapsible_widget.py | 30 +++-- rare/widgets/selective_widget.py | 55 ++++++++ requirements-flatpak.txt | 2 +- requirements-full.txt | 2 +- requirements.txt | 2 +- 9 files changed, 139 insertions(+), 161 deletions(-) create mode 100644 rare/widgets/selective_widget.py diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index 49c5f10a..1f5828f7 100644 --- a/rare/components/dialogs/install_dialog.py +++ b/rare/components/dialogs/install_dialog.py @@ -6,8 +6,7 @@ from typing import Tuple, List, Union, Optional from PyQt5.QtCore import QThreadPool, QSettings from PyQt5.QtCore import pyqtSignal, pyqtSlot from PyQt5.QtGui import QShowEvent -from PyQt5.QtWidgets import QFileDialog, QCheckBox, QWidget, QVBoxLayout, QFormLayout -from legendary.utils.selective_dl import get_sdl_appname +from PyQt5.QtWidgets import QFileDialog, QCheckBox, QWidget, QFormLayout from rare.models.game import RareGame from rare.models.install import InstallDownloadModel, InstallQueueItemModel, InstallOptionsModel @@ -15,18 +14,46 @@ from rare.shared.workers.install_info import InstallInfoWorker from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog from rare.ui.components.dialogs.install_dialog_advanced import Ui_InstallDialogAdvanced from rare.utils.misc import format_size, icon -from rare.widgets.dialogs import ActionDialog, dialog_title_game from rare.widgets.collapsible_widget import CollapsibleFrame +from rare.widgets.dialogs import ActionDialog, dialog_title_game from rare.widgets.indicator_edit import PathEdit, IndicatorReasonsCommon +from rare.widgets.selective_widget import SelectiveWidget class InstallDialogAdvanced(CollapsibleFrame): def __init__(self, parent=None): - widget = QWidget(parent) - title = widget.tr("Advanced options") + super(InstallDialogAdvanced, self).__init__(parent=parent) + + title = self.tr("Advanced options") + self.setTitle(title) + + widget = QWidget(parent=self) self.ui = Ui_InstallDialogAdvanced() self.ui.setupUi(widget) - super(InstallDialogAdvanced, self).__init__(widget=widget, title=title, parent=parent) + self.setWidget(widget) + + +class InstallDialogSelective(CollapsibleFrame): + stateChanged: pyqtSignal = pyqtSignal() + + def __init__(self, rgame: RareGame, parent=None): + super(InstallDialogSelective, self).__init__(parent=parent) + title = self.tr("Optional downloads") + self.setTitle(title) + self.setEnabled(bool(rgame.sdl_name)) + + self.selective_widget: SelectiveWidget = None + self.rgame = rgame + + def update_list(self, platform: str): + if self.selective_widget is not None: + self.selective_widget.deleteLater() + self.selective_widget = SelectiveWidget(self.rgame, platform, parent=self) + self.selective_widget.stateChanged.connect(self.stateChanged) + self.setWidget(self.selective_widget) + + def install_tags(self): + return self.selective_widget.install_tags() class InstallDialog(ActionDialog): @@ -64,7 +91,8 @@ class InstallDialog(ActionDialog): self.advanced = InstallDialogAdvanced(parent=self) self.ui.advanced_layout.addWidget(self.advanced) - self.selectable = CollapsibleFrame(widget=None, title=self.tr("Optional downloads"), parent=self) + self.selectable = InstallDialogSelective(rgame, parent=self) + self.selectable.stateChanged.connect(self.option_changed) self.ui.selectable_layout.addWidget(self.selectable) self.options_changed = False @@ -82,13 +110,14 @@ class InstallDialog(ActionDialog): self.install_dir_edit = PathEdit( path=base_path, file_mode=QFileDialog.DirectoryOnly, - edit_func=self.option_changed, - save_func=self.save_install_edit, + edit_func=self.install_dir_edit_callback, + save_func=self.install_dir_save_callback, parent=self, ) self.ui.main_layout.setWidget( self.ui.main_layout.getWidgetPosition(self.ui.install_dir_label)[0], - QFormLayout.FieldRole, self.install_dir_edit + QFormLayout.FieldRole, + self.install_dir_edit, ) self.install_dir_edit.setDisabled(rgame.is_installed) @@ -102,10 +131,10 @@ class InstallDialog(ActionDialog): self.ui.platform_combo.addItems(reversed(rgame.platforms)) combo_text = rgame.igame.platform if rgame.is_installed else rgame.default_platform self.ui.platform_combo.setCurrentIndex(self.ui.platform_combo.findText(combo_text)) - self.ui.platform_combo.currentIndexChanged.connect(lambda i: self.option_changed(None)) + self.ui.platform_combo.currentIndexChanged.connect(self.option_changed) self.ui.platform_combo.currentIndexChanged.connect(self.check_incompatible_platform) self.ui.platform_combo.currentIndexChanged.connect(self.reset_install_dir) - self.ui.platform_combo.currentIndexChanged.connect(self.reset_sdl_list) + self.ui.platform_combo.currentTextChanged.connect(self.selectable.update_list) self.ui.platform_label.setDisabled(rgame.is_installed) self.ui.platform_combo.setDisabled(rgame.is_installed) @@ -131,11 +160,8 @@ class InstallDialog(ActionDialog): lambda: self.non_reload_option_changed("shortcut") ) - self.selectable_checks: List[TagCheckBox] = [] - self.config_tags: Optional[List[str]] = None - self.reset_install_dir(self.ui.platform_combo.currentIndex()) - self.reset_sdl_list(self.ui.platform_combo.currentIndex()) + self.selectable.update_list(self.ui.platform_combo.currentText()) self.check_incompatible_platform(self.ui.platform_combo.currentIndex()) self.accept_button.setEnabled(False) @@ -180,7 +206,7 @@ class InstallDialog(ActionDialog): def showEvent(self, a0: QShowEvent) -> None: if a0.spontaneous(): return super().showEvent(a0) - self.save_install_edit(self.install_dir_edit.text()) + self.install_dir_save_callback(self.install_dir_edit.text()) super().showEvent(a0) def execute(self): @@ -197,68 +223,30 @@ class InstallDialog(ActionDialog): default_dir = self.core.get_default_install_dir(platform) self.install_dir_edit.setText(default_dir) - @pyqtSlot(int) - def reset_sdl_list(self, index: int): - platform = self.ui.platform_combo.itemText(index) - for cb in self.selectable_checks: - cb.disconnect() - cb.deleteLater() - self.selectable_checks.clear() - - 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.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: - sdl_data = self.core.get_sdl_data(sdl_name, platform=platform) - if sdl_data: - widget = QWidget(self.selectable) - layout = QVBoxLayout(widget) - layout.setSpacing(0) - for tag, info in sdl_data.items(): - cb = TagCheckBox(info["name"].strip(), info["description"].strip(), info["tags"]) - if tag == "__required": - cb.setChecked(True) - cb.setDisabled(True) - if self.config_tags is not None: - if all(elem in self.config_tags for elem in info["tags"]): - cb.setChecked(True) - layout.addWidget(cb) - self.selectable_checks.append(cb) - for cb in self.selectable_checks: - cb.stateChanged.connect(self.option_changed) - self.selectable.setWidget(widget) - else: - self.selectable.setDisabled(True) - @pyqtSlot(int) def check_incompatible_platform(self, index: int): platform = self.ui.platform_combo.itemText(index) if platform == "Mac" and pf.system() != "Darwin": self.error_box( self.tr("Warning"), - self.tr("You will not be able to run the game if you select {} as platform").format(platform) + self.tr("You will not be able to run the game if you select {} as platform").format(platform), ) else: self.error_box() def get_options(self): self.__options.base_path = "" if self.rgame.is_installed else self.install_dir_edit.text() + self.__options.platform = self.ui.platform_combo.currentText() + self.__options.create_shortcut = self.ui.shortcut_check.isChecked() 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.__options.install_tag = [""] - for cb in self.selectable_checks: - if data := cb.isChecked(): - # noinspection PyTypeChecker - self.__options.install_tag.extend(data) + self.__options.install_tag = self.selectable.install_tags() + self.__options.reset_sdl = True def get_download_info(self): self.__download = None @@ -279,13 +267,17 @@ class InstallDialog(ActionDialog): self.get_options() self.get_download_info() - def option_changed(self, path) -> Tuple[bool, str, int]: + @pyqtSlot() + def option_changed(self): self.options_changed = True self.accept_button.setEnabled(False) self.action_button.setEnabled(not self.active()) + + def install_dir_edit_callback(self, path: str) -> Tuple[bool, str, int]: + self.option_changed() return True, path, IndicatorReasonsCommon.VALID - def save_install_edit(self, path: str): + def install_dir_save_callback(self, path: str): if not os.path.exists(path): return _, _, free_space = shutil.disk_usage(path) @@ -369,12 +361,6 @@ class InstallDialog(ActionDialog): self.__queue_item = InstallQueueItemModel(options=self.__options, download=self.__download) def reject_handler(self): - # FIXME: This is implemented through the selective downloads dialog now. remove soon - # if self.config_tags is not None: - # config_helper.set_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.rgame.app_name, 'install_tags') self.__queue_item = InstallQueueItemModel(options=self.__options, download=None) @@ -387,6 +373,3 @@ class TagCheckBox(QCheckBox): def isChecked(self) -> Union[bool, List[str]]: return self.tags if super(TagCheckBox, self).isChecked() else False - - - diff --git a/rare/components/dialogs/selective_dialog.py b/rare/components/dialogs/selective_dialog.py index 864eb657..1c3b5031 100644 --- a/rare/components/dialogs/selective_dialog.py +++ b/rare/components/dialogs/selective_dialog.py @@ -1,18 +1,11 @@ -from typing import List, Union, Optional - from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import ( - QLabel, - QVBoxLayout, - QCheckBox, - QLayout, QGroupBox, -) -from legendary.utils.selective_dl import get_sdl_appname +from PyQt5.QtWidgets import QLabel, QVBoxLayout, QLayout, QGroupBox from rare.models.game import RareGame from rare.models.install import SelectiveDownloadsModel -from rare.widgets.dialogs import ButtonDialog, dialog_title_game from rare.utils.misc import icon +from rare.widgets.dialogs import ButtonDialog, dialog_title_game +from rare.widgets.selective_widget import SelectiveWidget class SelectiveDialog(ButtonDialog): @@ -25,20 +18,18 @@ class SelectiveDialog(ButtonDialog): title_label = QLabel(f"

{dialog_title_game(header, rgame.app_title)}

", self) - self.core = rgame.core self.rgame = rgame + self.selective_widget = SelectiveWidget(rgame, rgame.igame.platform, self) - selectable_group = QGroupBox(self.tr("Optional downloads"), self) - self.selectable_layout = QVBoxLayout(selectable_group) - self.selectable_layout.setSpacing(0) - - self.selectable_checks: List[TagCheckBox] = [] - self.config_tags: Optional[List[str]] = None + container = QGroupBox(self.tr("Optional downloads"), self) + container_layout = QVBoxLayout(container) + container_layout.setContentsMargins(0, 0, 0, 0) + container_layout.addWidget(self.selective_widget) layout = QVBoxLayout() layout.setSizeConstraint(QLayout.SetFixedSize) layout.addWidget(title_label) - layout.addWidget(selectable_group) + layout.addWidget(container) self.setCentralLayout(layout) @@ -47,62 +38,13 @@ class SelectiveDialog(ButtonDialog): self.options: SelectiveDownloadsModel = SelectiveDownloadsModel(rgame.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: - self.create_sdl_list() - else: - self.options.accepted = True - self.accept() - - def create_sdl_list(self): - platform = self.rgame.igame.platform - for cb in self.selectable_checks: - cb.disconnect() - cb.deleteLater() - self.selectable_checks.clear() - - 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.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: - sdl_data = self.core.get_sdl_data(sdl_name, platform=platform) - if sdl_data: - for tag, info in sdl_data.items(): - cb = TagCheckBox(info["name"].strip(), info["description"].strip(), info["tags"]) - if tag == "__required": - cb.setChecked(True) - cb.setDisabled(True) - if self.config_tags is not None: - if all(elem in self.config_tags for elem in info["tags"]): - cb.setChecked(True) - self.selectable_layout.addWidget(cb) - self.selectable_checks.append(cb) - def done_handler(self): self.result_ready.emit(self.rgame, self.options) def accept_handler(self): - install_tag = [""] - for cb in self.selectable_checks: - if data := cb.isChecked(): - # noinspection PyTypeChecker - install_tag.extend(data) self.options.accepted = True - self.options.install_tag = install_tag + self.options.install_tag = self.selective_widget.install_tags() def reject_handler(self): self.options.accepted = False self.options.install_tag = None - - -class TagCheckBox(QCheckBox): - def __init__(self, text, desc, tags: List[str], parent=None): - super(TagCheckBox, self).__init__(parent) - self.setText(text) - self.setToolTip(desc) - self.tags = tags - - def isChecked(self) -> Union[bool, List[str]]: - return self.tags if super(TagCheckBox, self).isChecked() else False diff --git a/rare/ui/components/dialogs/install_dialog.py b/rare/ui/components/dialogs/install_dialog.py index 10c5d871..8dc5c664 100644 --- a/rare/ui/components/dialogs/install_dialog.py +++ b/rare/ui/components/dialogs/install_dialog.py @@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_InstallDialog(object): def setupUi(self, InstallDialog): InstallDialog.setObjectName("InstallDialog") - InstallDialog.resize(179, 204) + InstallDialog.resize(197, 216) InstallDialog.setWindowTitle("InstallDialog") self.main_layout = QtWidgets.QFormLayout(InstallDialog) self.main_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) diff --git a/rare/ui/components/dialogs/install_dialog.ui b/rare/ui/components/dialogs/install_dialog.ui index e47e3eaf..d16cf4d4 100644 --- a/rare/ui/components/dialogs/install_dialog.ui +++ b/rare/ui/components/dialogs/install_dialog.ui @@ -6,8 +6,8 @@ 0 0 - 179 - 204 + 197 + 216 diff --git a/rare/widgets/collapsible_widget.py b/rare/widgets/collapsible_widget.py index 86ce6969..bd5d8ab6 100644 --- a/rare/widgets/collapsible_widget.py +++ b/rare/widgets/collapsible_widget.py @@ -101,9 +101,7 @@ class CollapsibleBase(object): class CollapsibleFrame(QFrame, CollapsibleBase): - def __init__( - self, widget: QWidget = None, title: str = "", button_text: str = "", animation_duration: int = 200, parent=None - ): + def __init__(self, animation_duration: int = 200, parent=None): super(CollapsibleFrame, self).__init__(parent=parent) self.setup(animation_duration) self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) @@ -111,11 +109,10 @@ class CollapsibleFrame(QFrame, CollapsibleBase): self.toggle_button = QToolButton(self) self.toggle_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.toggle_button.setIcon(icon("fa.arrow-right")) - self.toggle_button.setText(button_text) self.toggle_button.setCheckable(True) self.toggle_button.setChecked(False) - self.title_label = QLabel(title) + self.title_label = QLabel(self) font = self.title_label.font() font.setBold(True) self.title_label.setFont(font) @@ -134,8 +131,11 @@ class CollapsibleFrame(QFrame, CollapsibleBase): self.toggle_button.clicked.connect(self.animationStart) - if widget is not None: - self.setWidget(widget) + def setTitle(self, title: str): + self.title_label.setText(title) + + def setText(self, text: str): + self.toggle_button.setText(text) def isChecked(self) -> bool: return self.toggle_button.isChecked() @@ -156,12 +156,9 @@ class CollapsibleFrame(QFrame, CollapsibleBase): class CollapsibleGroupBox(QGroupBox, CollapsibleBase): - def __init__( - self, widget: QWidget = None, title: str = "", animation_duration: int = 200, parent=None - ): + def __init__(self, animation_duration: int = 200, parent=None): super(CollapsibleGroupBox, self).__init__(parent=parent) self.setup(animation_duration) - self.setTitle(title) self.setCheckable(True) self.setChecked(False) @@ -173,9 +170,6 @@ class CollapsibleGroupBox(QGroupBox, CollapsibleBase): self.toggled.connect(self.animationStart) - if widget is not None: - self.setWidget(widget) - def isChecked(self) -> bool: return super(CollapsibleGroupBox, self).isChecked() @@ -202,7 +196,9 @@ if __name__ == "__main__": ui_frame = Ui_InstallDialogAdvanced() widget_frame = QWidget() ui_frame.setupUi(widget_frame) - collapsible_frame = CollapsibleFrame(widget_frame, title="Frame me!") + collapsible_frame = CollapsibleFrame() + collapsible_frame.setWidget(widget_frame) + collapsible_frame.setTitle("Frame me!") collapsible_frame.setDisabled(False) def replace_func_frame(state): @@ -217,7 +213,9 @@ if __name__ == "__main__": ui_group = Ui_InstallDialogAdvanced() widget_group = QWidget() ui_group.setupUi(widget_group) - collapsible_group = CollapsibleGroupBox(widget_group, title="Group me!") + collapsible_group = CollapsibleGroupBox() + collapsible_group.setWidget(widget_group) + collapsible_group.setTitle("Group me!") collapsible_group.setDisabled(False) def replace_func_group(state): diff --git a/rare/widgets/selective_widget.py b/rare/widgets/selective_widget.py new file mode 100644 index 00000000..85cb1385 --- /dev/null +++ b/rare/widgets/selective_widget.py @@ -0,0 +1,55 @@ +from typing import List, Union + +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtWidgets import QCheckBox, QWidget, QVBoxLayout +from legendary.utils.selective_dl import get_sdl_appname + +from rare.models.game import RareGame + + +class TagCheckBox(QCheckBox): + def __init__(self, text, desc, tags: List[str], parent=None): + super(TagCheckBox, self).__init__(parent) + self.setText(text) + self.setToolTip(desc) + self.tags = tags + + def isChecked(self) -> Union[bool, List[str]]: + return self.tags if super(TagCheckBox, self).isChecked() else False + + +class SelectiveWidget(QWidget): + stateChanged: pyqtSignal = pyqtSignal() + + def __init__(self, rgame: RareGame, platform: str, parent=None): + super().__init__(parent=parent) + + main_layout = QVBoxLayout(self) + main_layout.setSpacing(0) + + core = rgame.core + + config_tags = core.lgd.config.get(rgame.app_name, "install_tags", fallback=None) + config_disable_sdl = core.lgd.config.getboolean(rgame.app_name, "disable_sdl", fallback=False) + sdl_name = get_sdl_appname(rgame.app_name) + if not config_disable_sdl and sdl_name is not None: + sdl_data = core.get_sdl_data(sdl_name, platform=platform) + if sdl_data: + for tag, info in sdl_data.items(): + cb = TagCheckBox(info["name"].strip(), info["description"].strip(), info["tags"]) + if tag == "__required": + cb.setChecked(True) + cb.setDisabled(True) + if config_tags is not None: + if all(elem in config_tags for elem in info["tags"]): + cb.setChecked(True) + cb.stateChanged.connect(self.stateChanged) + main_layout.addWidget(cb) + + def install_tags(self): + install_tags = [""] + for cb in self.findChildren(TagCheckBox, options=Qt.FindDirectChildrenOnly): + if data := cb.isChecked(): + # noinspection PyTypeChecker + install_tags.extend(data) + return install_tags diff --git a/requirements-flatpak.txt b/requirements-flatpak.txt index 2f8872a6..893a7e2f 100644 --- a/requirements-flatpak.txt +++ b/requirements-flatpak.txt @@ -1,4 +1,4 @@ -requests +requests<3.0 QtAwesome setuptools legendary-gl>=0.20.34 diff --git a/requirements-full.txt b/requirements-full.txt index f8dbdeb8..55b8e26f 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -1,4 +1,4 @@ -requests +requests<3.0 PyQt5 QtAwesome setuptools diff --git a/requirements.txt b/requirements.txt index b8298181..6d4303ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -requests +requests<3.0 PyQt5 QtAwesome setuptools From 6cbad745dfe6aa71cbadf70266ef08582f2eea8f Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:57:00 +0200 Subject: [PATCH 07/29] SelectiveWidget: Disabled parent if there aren't any SDLs. --- rare/widgets/selective_widget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rare/widgets/selective_widget.py b/rare/widgets/selective_widget.py index 85cb1385..527697cc 100644 --- a/rare/widgets/selective_widget.py +++ b/rare/widgets/selective_widget.py @@ -45,6 +45,9 @@ class SelectiveWidget(QWidget): cb.setChecked(True) cb.stateChanged.connect(self.stateChanged) main_layout.addWidget(cb) + self.parent().setDisabled(False) + else: + self.parent().setDisabled(True) def install_tags(self): install_tags = [""] From 29bd7b81cb6daaa39afa6a96f0a33a7761b4f17b Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:41:23 +0200 Subject: [PATCH 08/29] Launcher: Restore compatibility with legendary 0.22.34 --- rare/launcher/lgd_helper.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/rare/launcher/lgd_helper.py b/rare/launcher/lgd_helper.py index 38c21254..4f6ab032 100644 --- a/rare/launcher/lgd_helper.py +++ b/rare/launcher/lgd_helper.py @@ -105,9 +105,15 @@ def get_game_params(rgame: RareGameSlim, args: InitArgs, launch_args: LaunchArgs app_name = rgame.game.metadata['mainGameItem']['releaseInfo'][0]['appId'] rgame.igame = rgame.core.get_installed_game(app_name) - params: LaunchParameters = rgame.core.get_launch_parameters( - app_name=rgame.game.app_name, offline=args.offline, addon_app_name=rgame.igame.app_name - ) + try: + params: LaunchParameters = rgame.core.get_launch_parameters( + app_name=rgame.game.app_name, offline=args.offline, addon_app_name=rgame.igame.app_name + ) + except TypeError: + logger.warning("Using older get_launch_parameters due to legendary version") + params: LaunchParameters = rgame.core.get_launch_parameters( + app_name=rgame.game.app_name, offline=args.offline + ) full_params = [] launch_args.environment = QProcessEnvironment.systemEnvironment() From 2a0f80a9f028b13144afc1db69b65193ea1f3a58 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 8 Sep 2023 17:07:43 +0300 Subject: [PATCH 09/29] RareGame: RareEosOverlay now implements its procedures internally. The new methods are the following * `has_update`: to check for updates * `is_enabled`: checks if the overlay is enabled (on wine platform, on prefix) * `enable`: enables the overlay (on wine platforms, on prefix, for app_name) * `disable`: disables the overlay (on wine platforms, on prefix, for app_name) * `install`: Install using Rare's existing facilities * `uninstall`: Uninstall using Rare's existing facilities --- rare/models/game.py | 63 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/rare/models/game.py b/rare/models/game.py index ea701015..f45b8709 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -9,12 +9,13 @@ from typing import List, Optional, Dict, Set from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool from PyQt5.QtGui import QPixmap +from legendary.lfs import eos from legendary.models.game import Game, InstalledGame from legendary.utils.selective_dl import get_sdl_appname from rare.lgndr.core import LegendaryCore -from rare.models.install import InstallOptionsModel, UninstallOptionsModel from rare.models.base_game import RareGameBase, RareGameSlim +from rare.models.install import InstallOptionsModel, UninstallOptionsModel from rare.shared.game_process import GameProcess from rare.shared.image_manager import ImageManager from rare.utils.paths import data_dir, get_rare_executable @@ -572,3 +573,63 @@ class RareEosOverlay(RareGameBase): else: self.igame = None self.signals.game.uninstalled.emit(self.app_name) + + @property + def has_update(self) -> bool: + self.core.check_for_overlay_updates() + return self.core.overlay_update_available + + def is_enabled(self, prefix: Optional[str] = None): + reg_paths = eos.query_registry_entries(prefix) + return reg_paths["overlay_path"] and self.core.is_overlay_install(reg_paths["overlay_path"]) + + def enable( + self, prefix: Optional[str] = None, app_name: Optional[str] = None, path: Optional[str] = None + ) -> bool: + if not self.is_installed or self.is_enabled(prefix): + return False + if not path: + path = self.igame.install_path + reg_paths = eos.query_registry_entries(prefix) + if old_path := reg_paths["overlay_path"]: + if os.path.normpath(old_path) == path: + logger.info(f"Overlay already enabled, nothing to do.") + return True + else: + logger.info(f'Updating overlay registry entries from "{old_path}" to "{path}"') + eos.remove_registry_entries(prefix) + eos.add_registry_entries(path, prefix) + logger.info(f"Enabled overlay at: {path} for prefix: {prefix}") + return True + + def disable(self, prefix: Optional[str] = None, app_name: Optional[str] = None) -> bool: + if not self.is_enabled(prefix): + return False + logger.info(f"Disabling overlay (removing registry keys) for prefix: {prefix}") + eos.remove_registry_entries(prefix) + return True + + def install(self) -> bool: + if not self.is_idle: + return False + if self.is_installed: + base_path = self.igame.install_path + else: + base_path = os.path.join( + self.core.lgd.config.get("Legendary", "install_dir", fallback=os.path.expanduser("~/legendary")), + ".overlay" + ) + self.signals.game.install.emit( + InstallOptionsModel( + app_name=self.app_name, base_path=base_path, platform="Windows", overlay=True + ) + ) + return True + + def uninstall(self) -> bool: + if not self.is_idle or not self.is_installed: + return False + self.signals.game.uninstall.emit( + UninstallOptionsModel(app_name=self.app_name) + ) + return True From a246f4ee1650906f29f9fb7540e7afed30ddb09a Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:38:39 +0300 Subject: [PATCH 10/29] EosGroup: Remake the UI and integrate with Rare's facilities * Uninstalling the Overlay now goes through the same procedure as uninstalling any other game. * The available prefixes are now listed instead of hiding them inside a combo box. * Each listing works indepedently to enable/disable the Overlay for the prefix --- rare/components/dialogs/uninstall_dialog.py | 10 +- rare/components/tabs/downloads/__init__.py | 5 +- .../tabs/games/integrations/__init__.py | 4 +- .../tabs/games/integrations/eos_group.py | 296 +++++++++--------- rare/models/install.py | 9 +- rare/models/signals.py | 6 +- rare/shared/rare_core.py | 10 +- rare/shared/workers/uninstall.py | 43 ++- .../tabs/games/integrations/eos_widget.py | 160 ++++------ .../tabs/games/integrations/eos_widget.ui | 287 +++++++---------- 10 files changed, 375 insertions(+), 455 deletions(-) diff --git a/rare/components/dialogs/uninstall_dialog.py b/rare/components/dialogs/uninstall_dialog.py index c6f24cae..dd6be379 100644 --- a/rare/components/dialogs/uninstall_dialog.py +++ b/rare/components/dialogs/uninstall_dialog.py @@ -23,14 +23,21 @@ class UninstallDialog(ButtonDialog): self.keep_files = QCheckBox(self.tr("Keep files")) self.keep_files.setChecked(bool(options.keep_files)) + self.keep_files.setEnabled(not rgame.is_overlay) self.keep_config = QCheckBox(self.tr("Keep configuation")) self.keep_config.setChecked(bool(options.keep_config)) + self.keep_config.setEnabled(not rgame.is_overlay) + + self.keep_overlay_keys = QCheckBox(self.tr("Keep EOS Overlay registry keys")) + self.keep_overlay_keys.setChecked(bool(options.keep_overlay_keys)) + self.keep_overlay_keys.setEnabled(rgame.is_overlay) layout = QVBoxLayout() layout.addWidget(title_label) layout.addWidget(self.keep_files) layout.addWidget(self.keep_config) + layout.addWidget(self.keep_overlay_keys) self.setCentralLayout(layout) @@ -51,7 +58,8 @@ class UninstallDialog(ButtonDialog): True, self.keep_files.isChecked(), self.keep_config.isChecked(), + self.keep_overlay_keys.isChecked(), ) def reject_handler(self): - self.options.values = (None, None, None) + self.options.values = (None, None, None, None) diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index 9166f724..85df8598 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -238,10 +238,7 @@ class DownloadsTab(QWidget): else: logger.info(f"Created desktop link {result.shortcut_name} for {result.options.app_name}") - if result.options.overlay: - self.signals.application.overlay_installed.emit() - else: - self.signals.application.notify.emit(result.options.app_name) + self.signals.application.notify.emit(result.options.app_name) if self.updates_group.contains(result.options.app_name): self.updates_group.set_widget_enabled(result.options.app_name, True) diff --git a/rare/components/tabs/games/integrations/__init__.py b/rare/components/tabs/games/integrations/__init__.py index 0a3bf0fb..0ecbc088 100644 --- a/rare/components/tabs/games/integrations/__init__.py +++ b/rare/components/tabs/games/integrations/__init__.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, QSpacerItem, QSizePoli from rare.widgets.side_tab import SideTabWidget from .egl_sync_group import EGLSyncGroup -from .eos_group import EOSGroup +from .eos_group import EosGroup from .import_group import ImportGroup from .ubisoft_group import UbisoftGroup @@ -35,7 +35,7 @@ class IntegrationsTabs(SideTabWidget): self, ) self.ubisoft_group = UbisoftGroup(self.eos_ubisoft) - self.eos_group = EOSGroup(self.eos_ubisoft) + self.eos_group = EosGroup(self.eos_ubisoft) self.eos_ubisoft.addWidget(self.eos_group) self.eos_ubisoft.addWidget(self.ubisoft_group) self.eos_ubisoft_index = self.addTab(self.eos_ubisoft, self.tr("Epic Overlay and Ubisoft")) diff --git a/rare/components/tabs/games/integrations/eos_group.py b/rare/components/tabs/games/integrations/eos_group.py index 9e541d25..154c820f 100644 --- a/rare/components/tabs/games/integrations/eos_group.py +++ b/rare/components/tabs/games/integrations/eos_group.py @@ -1,161 +1,189 @@ import os import platform from logging import getLogger -from typing import List +from typing import Optional -from PyQt5.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool -from PyQt5.QtWidgets import QGroupBox, QMessageBox +from PyQt5.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool, Qt, pyqtSlot, QSize +from PyQt5.QtWidgets import QGroupBox, QMessageBox, QFrame, QHBoxLayout, QSizePolicy, QLabel, QPushButton, QFormLayout from legendary.lfs import eos -from rare.models.install import InstallOptionsModel -from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton +from rare.lgndr.core import LegendaryCore +from rare.models.game import RareEosOverlay +from rare.shared import RareCore from rare.ui.components.tabs.games.integrations.eos_widget import Ui_EosWidget +from rare.utils import config_helper from rare.utils.misc import icon +from rare.widgets.elide_label import ElideLabel logger = getLogger("EpicOverlay") -def get_wine_prefixes() -> List[str]: - prefixes = list() - if os.path.exists(p := os.path.expanduser("~/.wine")): - prefixes.append(p) - - for name, section in LegendaryCoreSingleton().lgd.config.items(): - pfx = section.get("WINEPREFIX") or section.get("wine_prefix") - if pfx and pfx not in prefixes: - prefixes.append(pfx) - - return prefixes - - class CheckForUpdateWorker(QRunnable): class CheckForUpdateSignals(QObject): update_available = pyqtSignal(bool) - def __init__(self): + def __init__(self, core: LegendaryCore): super(CheckForUpdateWorker, self).__init__() self.signals = self.CheckForUpdateSignals() self.setAutoDelete(True) - self.core = LegendaryCoreSingleton() + self.core = core def run(self) -> None: self.core.check_for_overlay_updates() self.signals.update_available.emit(self.core.overlay_update_available) -class EOSGroup(QGroupBox): +class EosPrefixWidget(QFrame): + def __init__(self, overlay: RareEosOverlay, prefix: Optional[str], parent=None): + super(EosPrefixWidget, self).__init__(parent=parent) + self.setFrameShape(QFrame.StyledPanel) + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + self.indicator = QLabel(parent=self) + self.indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred) + + self.label = ElideLabel( + prefix if prefix is not None else "Epic Online Services Overlay", + parent=self + ) + + self.button = QPushButton(parent=self) + self.button.setMinimumWidth(150) + self.button.clicked.connect(self.action) + + layout = QHBoxLayout(self) + layout.setContentsMargins(-1, 0, 0, 0) + layout.addWidget(self.indicator) + layout.addWidget(self.label, stretch=1) + layout.addWidget(self.button) + + self.overlay = overlay + self.prefix = prefix + + self.overlay.signals.game.installed.connect(self.update_state) + self.overlay.signals.game.uninstalled.connect(self.update_state) + + self.update_state() + + @pyqtSlot() + def update_state(self): + if not self.overlay.is_installed: + self.setDisabled(True) + self.button.setText(self.tr("Unavailable")) + self.indicator.setPixmap(icon("fa.circle-o", color="grey").pixmap(20, 20)) + return + + self.setDisabled(False) + if self.overlay.is_enabled(self.prefix): + self.button.setText(self.tr("Disable overlay")) + self.indicator.setPixmap( + icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)) + ) + else: + self.button.setText(self.tr("Enable overlay")) + self.indicator.setPixmap( + icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20)) + ) + + @pyqtSlot() + def action(self): + if self.overlay.is_enabled(self.prefix): + self.overlay.disable(prefix=self.prefix) + else: + self.overlay.enable(prefix=self.prefix) + self.update_state() + + +class EosGroup(QGroupBox): def __init__(self, parent=None): - super(EOSGroup, self).__init__(parent=parent) + super(EosGroup, self).__init__(parent=parent) self.ui = Ui_EosWidget() self.ui.setupUi(self) # lk: set object names for CSS properties self.ui.install_button.setObjectName("InstallButton") - self.ui.install_button.setIcon(icon("ri.install-line")) self.ui.uninstall_button.setObjectName("UninstallButton") + + self.ui.install_page_layout.setAlignment(Qt.AlignTop) + self.ui.info_page_layout.setAlignment(Qt.AlignTop) + + self.ui.install_button.setIcon(icon("ri.install-line")) self.ui.uninstall_button.setIcon(icon("ri.uninstall-line")) - self.core = LegendaryCoreSingleton() - self.signals = GlobalSignalsSingleton() + self.installed_path_label = ElideLabel(parent=self) + self.installed_version_label = ElideLabel(parent=self) - self.prefix_enabled = False + self.ui.info_label_layout.setWidget(0, QFormLayout.FieldRole, self.installed_version_label) + self.ui.info_label_layout.setWidget(1, QFormLayout.FieldRole, self.installed_path_label) - self.ui.enabled_cb.stateChanged.connect(self.change_enable) + self.rcore = RareCore.instance() + self.core = self.rcore.core() + self.signals = self.rcore.signals() + self.overlay = self.rcore.get_overlay() + + self.overlay.signals.game.installed.connect(self.install_finished) + self.overlay.signals.game.uninstalled.connect(self.uninstall_finished) + + self.ui.install_button.clicked.connect(self.install_overlay) + self.ui.update_button.clicked.connect(self.install_overlay) self.ui.uninstall_button.clicked.connect(self.uninstall_overlay) - self.ui.update_button.setVisible(False) - self.overlay = self.core.lgd.get_overlay_install_info() - - self.signals.application.overlay_installed.connect(self.overlay_installation_finished) - self.signals.application.prefix_updated.connect(self.update_prefixes) - - self.ui.update_check_button.clicked.connect(self.check_for_update) - self.ui.install_button.clicked.connect(self.install_overlay) - self.ui.update_button.clicked.connect(lambda: self.install_overlay(True)) - - if self.overlay: # installed - self.ui.installed_version_lbl.setText(f"{self.overlay.version}") - self.ui.installed_path_lbl.setText(f"{self.overlay.install_path}") - self.ui.overlay_stack.setCurrentIndex(0) + if self.overlay.is_installed: # installed + self.installed_version_label.setText(f"{self.overlay.version}") + self.installed_path_label.setText(self.overlay.install_path) + self.ui.overlay_stack.setCurrentWidget(self.ui.info_page) else: - self.ui.overlay_stack.setCurrentIndex(1) - self.ui.enable_frame.setDisabled(True) - - if platform.system() == "Windows": - self.current_prefix = None - self.ui.select_pfx_combo.setVisible(False) - else: - self.current_prefix = os.path.expanduser("~/.wine") \ - if os.path.exists(os.path.expanduser("~/.wine")) \ - else None - pfxs = get_wine_prefixes() - for pfx in pfxs: - self.ui.select_pfx_combo.addItem(pfx.replace(os.path.expanduser("~/"), "~/")) - if not pfxs: - self.ui.enable_frame.setDisabled(True) - else: - self.ui.select_pfx_combo.setCurrentIndex(0) - - self.ui.select_pfx_combo.currentIndexChanged.connect(self.update_select_combo) - if pfxs: - self.update_select_combo(None) - - self.ui.enabled_info_label.setText("") + self.ui.overlay_stack.setCurrentWidget(self.ui.install_page) + self.ui.update_button.setEnabled(False) self.threadpool = QThreadPool.globalInstance() + def showEvent(self, a0) -> None: + self.check_for_update() + self.update_prefixes() + super().showEvent(a0) + def update_prefixes(self): - logger.debug("Updated prefixes") - pfxs = get_wine_prefixes() # returns /home/whatever - self.ui.select_pfx_combo.clear() + for widget in self.findChildren(EosPrefixWidget, options=Qt.FindDirectChildrenOnly): + widget.deleteLater() - for pfx in pfxs: - self.ui.select_pfx_combo.addItem(pfx.replace(os.path.expanduser("~/"), "~/")) - - if self.current_prefix in pfxs: - self.ui.select_pfx_combo.setCurrentIndex( - self.ui.select_pfx_combo.findText(self.current_prefix.replace(os.path.expanduser("~/"), "~/"))) + if platform.system() != "Windows": + prefixes = config_helper.get_wine_prefixes() + if platform.system() == "Darwin": + # TODO: add crossover support + pass + for prefix in prefixes: + widget = EosPrefixWidget(self.overlay, prefix) + self.ui.eos_layout.addWidget(widget) + logger.debug("Updated prefixes") + else: + widget = EosPrefixWidget(self.overlay, None) + self.ui.eos_layout.addWidget(widget) def check_for_update(self): - def worker_finished(update_available): - self.ui.update_button.setVisible(update_available) - self.ui.update_check_button.setDisabled(False) - if not update_available: - self.ui.update_check_button.setText(self.tr("No update available")) + if not self.overlay.is_installed: + return - self.ui.update_check_button.setDisabled(True) - worker = CheckForUpdateWorker() + def worker_finished(update_available): + self.ui.update_button.setEnabled(update_available) + + worker = CheckForUpdateWorker(self.core) worker.signals.update_available.connect(worker_finished) QThreadPool.globalInstance().start(worker) - def overlay_installation_finished(self): - self.overlay = self.core.lgd.get_overlay_install_info() - - if not self.overlay: - logger.error("Something went wrong, when installing overlay") - QMessageBox.warning(self, "Error", self.tr("Something went wrong, when installing overlay")) + @pyqtSlot() + def install_finished(self): + if not self.overlay.is_installed: + logger.error("Something went wrong while installing overlay") + QMessageBox.warning(self, "Error", self.tr("Something went wrong while installing Overlay")) return + self.ui.overlay_stack.setCurrentWidget(self.ui.info_page) + self.installed_version_label.setText(f"{self.overlay.version}") + self.installed_path_label.setText(self.overlay.install_path) + self.ui.update_button.setEnabled(False) - self.ui.overlay_stack.setCurrentIndex(0) - self.ui.installed_version_lbl.setText(f"{self.overlay.version}") - self.ui.installed_path_lbl.setText(f"{self.overlay.install_path}") - - self.ui.update_button.setVisible(False) - - self.ui.enable_frame.setEnabled(True) - - def update_select_combo(self, i: None): - if i is None: - i = self.ui.select_pfx_combo.currentIndex() - prefix = os.path.expanduser(self.ui.select_pfx_combo.itemText(i)) - if platform.system() != "Windows" and not os.path.isfile(os.path.join(prefix, "user.reg")): - return - self.current_prefix = prefix - reg_paths = eos.query_registry_entries(self.current_prefix) - - overlay_enabled = False - if reg_paths['overlay_path'] and self.core.is_overlay_install(reg_paths['overlay_path']): - overlay_enabled = True - self.ui.enabled_cb.setChecked(overlay_enabled) + @pyqtSlot() + def uninstall_finished(self): + self.ui.overlay_stack.setCurrentWidget(self.ui.install_page) def change_enable(self): enabled = self.ui.enabled_cb.isChecked() @@ -170,7 +198,7 @@ class EOSGroup(QGroupBox): logger.info("Disabled Epic Overlay") self.ui.enabled_info_label.setText(self.tr("Disabled")) else: - if not self.overlay: + if not self.overlay.is_installed: available_installs = self.core.search_overlay_installs(self.current_prefix) if not available_installs: logger.error('No EOS overlay installs found!') @@ -209,51 +237,13 @@ class EOSGroup(QGroupBox): self.ui.enabled_info_label.setText(self.tr("Enabled")) logger.info(f'Enabled overlay at: {path}') - def update_checkbox(self): - reg_paths = eos.query_registry_entries(self.current_prefix) - enabled = False - if reg_paths['overlay_path'] and self.core.is_overlay_install(reg_paths['overlay_path']): - enabled = True - self.ui.enabled_cb.setChecked(enabled) - - def install_overlay(self, update=False): - base_path = os.path.join(self.core.get_default_install_dir(), ".overlay") - if update: - if not self.overlay: - self.ui.overlay_stack.setCurrentIndex(1) - self.ui.enable_frame.setDisabled(True) - QMessageBox.warning(self, "Warning", self.tr("Overlay is not installed. Could not update")) - return - base_path = self.overlay.install_path - - options = InstallOptionsModel( - app_name=eos.EOSOverlayApp.app_name, base_path=base_path, platform="Windows", overlay=True - ) - - self.signals.game.install.emit(options) + @pyqtSlot() + def install_overlay(self): + self.overlay.install() def uninstall_overlay(self): - if not self.core.is_overlay_installed(): - logger.error('No legendary-managed overlay installation found.') - self.ui.overlay_stack.setCurrentIndex(1) + if not self.overlay.is_installed: + logger.error('No Rare-managed overlay installation found.') + self.ui.overlay_stack.setCurrentWidget(self.ui.install_page) return - - if QMessageBox.No == QMessageBox.question( - self, "Uninstall Overlay", self.tr("Do you want to uninstall overlay?"), - QMessageBox.Yes | QMessageBox.No, QMessageBox.No - ): - return - if platform.system() == "Windows": - eos.remove_registry_entries(None) - else: - for prefix in [self.ui.select_pfx_combo.itemText(i) for i in range(self.ui.select_pfx_combo.count())]: - logger.info(f"Removing registry entries from {prefix}") - try: - eos.remove_registry_entries(os.path.expanduser(prefix)) - except Exception as e: - logger.warning(f"{prefix}: {e}") - - self.core.remove_overlay_install() - self.ui.overlay_stack.setCurrentIndex(1) - - self.ui.enable_frame.setDisabled(True) + self.overlay.uninstall() diff --git a/rare/models/install.py b/rare/models/install.py index 69123d0b..0a4965d6 100644 --- a/rare/models/install.py +++ b/rare/models/install.py @@ -86,6 +86,7 @@ class UninstallOptionsModel: accepted: bool = None keep_files: bool = None keep_config: bool = None + keep_overlay_keys: bool = None def __bool__(self): return ( @@ -93,20 +94,21 @@ class UninstallOptionsModel: and (self.accepted is not None) and (self.keep_files is not None) and (self.keep_config is not None) + and (self.keep_overlay_keys is not None) ) @property - def values(self) -> Tuple[bool, bool, bool]: + def values(self) -> Tuple[bool, bool, bool, bool]: """ This model's options :return: Tuple of `accepted` `keep_files` `keep_config` `keep_overlay_keys` """ - return self.accepted, self.keep_config, self.keep_files + return self.accepted, self.keep_config, self.keep_files, self.keep_overlay_keys @values.setter - def values(self, values: Tuple[bool, bool, bool]): + def values(self, values: Tuple[bool, bool, bool, bool]): """ Set this model's options @@ -117,6 +119,7 @@ class UninstallOptionsModel: self.accepted = values[0] self.keep_files = values[1] self.keep_config = values[2] + self.keep_overlay_keys = values[3] @dataclass diff --git a/rare/models/signals.py b/rare/models/signals.py index 8377e2d3..3c614237 100644 --- a/rare/models/signals.py +++ b/rare/models/signals.py @@ -10,14 +10,12 @@ class GlobalSignals: class ApplicationSignals(QObject): # int: exit code - quit = pyqtSignal(int) + quit = pyqtSignal(int) # str: app_title notify = pyqtSignal(str) # none prefix_updated = pyqtSignal() # none - overlay_installed = pyqtSignal() - # none update_tray = pyqtSignal() # none update_statusbar = pyqtSignal() @@ -58,4 +56,4 @@ class GlobalSignals: self.download.deleteLater() del self.download self.discord_rpc.deleteLater() - del self.discord_rpc \ No newline at end of file + del self.discord_rpc diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index 6000a3b9..31a80f06 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -66,6 +66,8 @@ class RareCore(QObject): self.__library: Dict[str, RareGame] = {} self.__eos_overlay = RareEosOverlay(self.__core, EOSOverlayApp) + self.__eos_overlay.signals.game.install.connect(self.__signals.game.install) + self.__eos_overlay.signals.game.uninstall.connect(self.__signals.game.uninstall) RareCore.__instance = self @@ -224,12 +226,12 @@ class RareCore(QObject): for dlc in rgame.owned_dlcs: if dlc.is_installed: logger.info(f'Uninstalling DLC "{dlc.app_name}" ({dlc.app_title})...') - uninstall_game(self.__core, dlc.app_name, keep_files=True, keep_config=True) + uninstall_game(self.__core, dlc, keep_files=True, keep_config=True) dlc.igame = None logger.info( f'Removing "{rgame.app_title}" because "{rgame.igame.install_path}" does not exist...' ) - uninstall_game(self.__core, rgame.app_name, keep_files=True, keep_config=True) + uninstall_game(self.__core, rgame, keep_files=True, keep_config=True) logger.info(f"Uninstalled {rgame.app_title}, because no game files exist") rgame.igame = None return @@ -254,6 +256,9 @@ class RareCore(QObject): return self.__eos_overlay return self.__library[app_name] + def get_overlay(self): + return self.get_game(EOSOverlayApp.app_name) + def __add_game(self, rgame: RareGame) -> None: rgame.signals.download.enqueue.connect(self.__signals.download.enqueue) rgame.signals.download.dequeue.connect(self.__signals.download.dequeue) @@ -345,7 +350,6 @@ class RareCore(QObject): start_time = time.time() try: entitlements = self.__core.egs.get_user_entitlements() - self.__core.lgd.entitlements = entitlements for game in self.__library.values(): game.grant_date() except (HTTPError, ConnectionError) as e: diff --git a/rare/shared/workers/uninstall.py b/rare/shared/workers/uninstall.py index 76207fe7..9b154ce8 100644 --- a/rare/shared/workers/uninstall.py +++ b/rare/shared/workers/uninstall.py @@ -1,7 +1,10 @@ +import platform from logging import getLogger +from typing import Tuple from PyQt5.QtCore import QObject, pyqtSignal from legendary.core import LegendaryCore +from legendary.lfs.eos import remove_registry_entries from rare.lgndr.cli import LegendaryCLI from rare.lgndr.glue.arguments import LgndrUninstallGameArgs @@ -16,14 +19,36 @@ logger = getLogger("UninstallWorker") # TODO: You can use RareGame directly here once this is called inside RareCore and skip metadata fetch -def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False): - game = core.get_game(app_name) +def uninstall_game( + core: LegendaryCore, rgame: RareGame, keep_files=False, keep_config=False, keep_overlay_keys=False +) -> Tuple[bool, str]: + if rgame.is_overlay: + logger.info('Deleting overlay installation...') + core.remove_overlay_install() + + if keep_overlay_keys: + return True, "" + + logger.info('Removing registry entries...') + if platform.system() != "Window": + prefixes = config_helper.get_wine_prefixes() + if platform.system() == "Darwin": + # TODO: add crossover support + pass + if prefixes is not None: + for prefix in prefixes: + remove_registry_entries(prefix) + logger.debug("Removed registry entries for prefix %s", prefix) + else: + remove_registry_entries(None) + + return True, "" # remove shortcuts link if desktop_links_supported(): for link_type in desktop_link_types(): link_path = desktop_link_path( - game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value"), link_type + rgame.game.metadata.get("customAttributes", {}).get("FolderName", {}).get("value"), link_type ) if link_path.exists(): link_path.unlink(missing_ok=True) @@ -31,7 +56,7 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_co status = LgndrIndirectStatus() LegendaryCLI(core).uninstall_game( LgndrUninstallGameArgs( - app_name=app_name, + app_name=rgame.app_name, keep_files=keep_files, skip_uninstaller=False, yes=True, @@ -40,8 +65,8 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_co ) if not keep_config: logger.info("Removing sections in config file") - config_helper.remove_section(app_name) - config_helper.remove_section(f"{app_name}.env") + config_helper.remove_section(rgame.app_name) + config_helper.remove_section(f"{rgame.app_name}.env") config_helper.save_config() @@ -62,7 +87,11 @@ class UninstallWorker(Worker): def run_real(self) -> None: self.rgame.state = RareGame.State.UNINSTALLING success, message = uninstall_game( - self.core, self.rgame.app_name, self.options.keep_files, self.options.keep_config + self.core, + self.rgame, + self.options.keep_files, + self.options.keep_config, + self.options.keep_overlay_keys, ) self.rgame.state = RareGame.State.IDLE self.signals.result.emit(self.rgame, success, message) diff --git a/rare/ui/components/tabs/games/integrations/eos_widget.py b/rare/ui/components/tabs/games/integrations/eos_widget.py index 8b856256..3efcf3f9 100644 --- a/rare/ui/components/tabs/games/integrations/eos_widget.py +++ b/rare/ui/components/tabs/games/integrations/eos_widget.py @@ -14,102 +14,71 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_EosWidget(object): def setupUi(self, EosWidget): EosWidget.setObjectName("EosWidget") - EosWidget.resize(586, 146) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(EosWidget.sizePolicy().hasHeightForWidth()) - EosWidget.setSizePolicy(sizePolicy) + EosWidget.resize(464, 98) EosWidget.setWindowTitle("GroupBox") - self.eos_layout = QtWidgets.QHBoxLayout(EosWidget) + self.eos_layout = QtWidgets.QVBoxLayout(EosWidget) self.eos_layout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) self.eos_layout.setObjectName("eos_layout") self.overlay_stack = QtWidgets.QStackedWidget(EosWidget) - self.overlay_stack.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.overlay_stack.setFrameShadow(QtWidgets.QFrame.Raised) self.overlay_stack.setObjectName("overlay_stack") - self.overlay_info_page = QtWidgets.QWidget() - self.overlay_info_page.setObjectName("overlay_info_page") - self.overlay_info_layout = QtWidgets.QFormLayout(self.overlay_info_page) - self.overlay_info_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.overlay_info_layout.setFormAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.overlay_info_layout.setObjectName("overlay_info_layout") - self.installed_version_info_lbl = QtWidgets.QLabel(self.overlay_info_page) - self.installed_version_info_lbl.setObjectName("installed_version_info_lbl") - self.overlay_info_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.installed_version_info_lbl) - self.installed_version_lbl = QtWidgets.QLabel(self.overlay_info_page) - self.installed_version_lbl.setText("error") - self.installed_version_lbl.setObjectName("installed_version_lbl") - self.overlay_info_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.installed_version_lbl) - self.installed_path_info_lbl = QtWidgets.QLabel(self.overlay_info_page) - self.installed_path_info_lbl.setObjectName("installed_path_info_lbl") - self.overlay_info_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.installed_path_info_lbl) - self.installed_path_lbl = QtWidgets.QLabel(self.overlay_info_page) - self.installed_path_lbl.setText("error") - self.installed_path_lbl.setObjectName("installed_path_lbl") - self.overlay_info_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.installed_path_lbl) - self.info_buttons_layout = QtWidgets.QHBoxLayout() - self.info_buttons_layout.setObjectName("info_buttons_layout") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.info_buttons_layout.addItem(spacerItem) - self.uninstall_button = QtWidgets.QPushButton(self.overlay_info_page) - self.uninstall_button.setMinimumSize(QtCore.QSize(120, 0)) - self.uninstall_button.setObjectName("uninstall_button") - self.info_buttons_layout.addWidget(self.uninstall_button) - self.update_check_button = QtWidgets.QPushButton(self.overlay_info_page) - self.update_check_button.setMinimumSize(QtCore.QSize(120, 0)) - self.update_check_button.setObjectName("update_check_button") - self.info_buttons_layout.addWidget(self.update_check_button) - self.update_button = QtWidgets.QPushButton(self.overlay_info_page) - self.update_button.setMinimumSize(QtCore.QSize(120, 0)) - self.update_button.setObjectName("update_button") - self.info_buttons_layout.addWidget(self.update_button) - self.overlay_info_layout.setLayout(3, QtWidgets.QFormLayout.SpanningRole, self.info_buttons_layout) - spacerItem1 = QtWidgets.QSpacerItem(6, 6, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.overlay_info_layout.setItem(2, QtWidgets.QFormLayout.SpanningRole, spacerItem1) - self.overlay_stack.addWidget(self.overlay_info_page) - self.overlay_install_page = QtWidgets.QWidget() - self.overlay_install_page.setObjectName("overlay_install_page") - self.overlay_install_layout = QtWidgets.QFormLayout(self.overlay_install_page) - self.overlay_install_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.overlay_install_layout.setFormAlignment(QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft) - self.overlay_install_layout.setObjectName("overlay_install_layout") - self.label = QtWidgets.QLabel(self.overlay_install_page) - self.label.setObjectName("label") - self.overlay_install_layout.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.label) - self.install_buttons_layout = QtWidgets.QHBoxLayout() - self.install_buttons_layout.setObjectName("install_buttons_layout") - spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.install_buttons_layout.addItem(spacerItem2) - self.install_button = QtWidgets.QPushButton(self.overlay_install_page) - self.install_button.setMinimumSize(QtCore.QSize(120, 0)) + self.install_page = QtWidgets.QWidget() + self.install_page.setObjectName("install_page") + self.install_page_layout = QtWidgets.QHBoxLayout(self.install_page) + self.install_page_layout.setContentsMargins(0, 0, 0, 0) + self.install_page_layout.setObjectName("install_page_layout") + self.install_label_layout = QtWidgets.QFormLayout() + self.install_label_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.install_label_layout.setFormAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.install_label_layout.setObjectName("install_label_layout") + self.install_label = QtWidgets.QLabel(self.install_page) + self.install_label.setObjectName("install_label") + self.install_label_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.install_label) + self.install_text = QtWidgets.QLabel(self.install_page) + self.install_text.setObjectName("install_text") + self.install_label_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.install_text) + self.install_page_layout.addLayout(self.install_label_layout) + self.install_button_layout = QtWidgets.QVBoxLayout() + self.install_button_layout.setContentsMargins(-1, -1, 0, -1) + self.install_button_layout.setObjectName("install_button_layout") + self.install_button = QtWidgets.QPushButton(self.install_page) + self.install_button.setMinimumSize(QtCore.QSize(140, 0)) self.install_button.setObjectName("install_button") - self.install_buttons_layout.addWidget(self.install_button) - self.overlay_install_layout.setLayout(2, QtWidgets.QFormLayout.SpanningRole, self.install_buttons_layout) - spacerItem3 = QtWidgets.QSpacerItem(6, 6, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.overlay_install_layout.setItem(1, QtWidgets.QFormLayout.SpanningRole, spacerItem3) - self.overlay_stack.addWidget(self.overlay_install_page) + self.install_button_layout.addWidget(self.install_button) + self.install_page_layout.addLayout(self.install_button_layout) + self.install_page_layout.setStretch(0, 1) + self.overlay_stack.addWidget(self.install_page) + self.info_page = QtWidgets.QWidget() + self.info_page.setObjectName("info_page") + self.info_page_layout = QtWidgets.QHBoxLayout(self.info_page) + self.info_page_layout.setContentsMargins(0, 0, 0, 0) + self.info_page_layout.setObjectName("info_page_layout") + self.info_label_layout = QtWidgets.QFormLayout() + self.info_label_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.info_label_layout.setFormAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.info_label_layout.setObjectName("info_label_layout") + self.version_label = QtWidgets.QLabel(self.info_page) + self.version_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.version_label.setObjectName("version_label") + self.info_label_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.version_label) + self.path_label = QtWidgets.QLabel(self.info_page) + self.path_label.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.path_label.setObjectName("path_label") + self.info_label_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.path_label) + self.info_page_layout.addLayout(self.info_label_layout) + self.info_button_layout = QtWidgets.QVBoxLayout() + self.info_button_layout.setObjectName("info_button_layout") + self.update_button = QtWidgets.QPushButton(self.info_page) + self.update_button.setMinimumSize(QtCore.QSize(140, 0)) + self.update_button.setObjectName("update_button") + self.info_button_layout.addWidget(self.update_button) + self.uninstall_button = QtWidgets.QPushButton(self.info_page) + self.uninstall_button.setMinimumSize(QtCore.QSize(140, 0)) + self.uninstall_button.setObjectName("uninstall_button") + self.info_button_layout.addWidget(self.uninstall_button) + self.info_page_layout.addLayout(self.info_button_layout) + self.info_page_layout.setStretch(0, 1) + self.overlay_stack.addWidget(self.info_page) self.eos_layout.addWidget(self.overlay_stack) - self.enable_frame = QtWidgets.QFrame(EosWidget) - self.enable_frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.enable_frame.setFrameShadow(QtWidgets.QFrame.Raised) - self.enable_frame.setObjectName("enable_frame") - self.enable_layout = QtWidgets.QVBoxLayout(self.enable_frame) - self.enable_layout.setObjectName("enable_layout") - self.select_pfx_combo = QtWidgets.QComboBox(self.enable_frame) - self.select_pfx_combo.setObjectName("select_pfx_combo") - self.enable_layout.addWidget(self.select_pfx_combo) - self.enabled_cb = QtWidgets.QCheckBox(self.enable_frame) - self.enabled_cb.setObjectName("enabled_cb") - self.enable_layout.addWidget(self.enabled_cb) - self.enabled_info_label = QtWidgets.QLabel(self.enable_frame) - font = QtGui.QFont() - font.setItalic(True) - self.enabled_info_label.setFont(font) - self.enabled_info_label.setText("") - self.enabled_info_label.setObjectName("enabled_info_label") - self.enable_layout.addWidget(self.enabled_info_label) - self.eos_layout.addWidget(self.enable_frame) self.retranslateUi(EosWidget) self.overlay_stack.setCurrentIndex(0) @@ -117,14 +86,13 @@ class Ui_EosWidget(object): def retranslateUi(self, EosWidget): _translate = QtCore.QCoreApplication.translate EosWidget.setTitle(_translate("EosWidget", "Epic Overlay")) - self.installed_version_info_lbl.setText(_translate("EosWidget", "Version")) - self.installed_path_info_lbl.setText(_translate("EosWidget", "Location")) - self.uninstall_button.setText(_translate("EosWidget", "Uninstall")) - self.update_check_button.setText(_translate("EosWidget", "Check for update")) - self.update_button.setText(_translate("EosWidget", "Update")) - self.label.setText(_translate("EosWidget", "Epic Overlay Services is not installed")) + self.install_label.setText(_translate("EosWidget", "Status:")) + self.install_text.setText(_translate("EosWidget", "Epic Online Services Overlay is not installed")) self.install_button.setText(_translate("EosWidget", "Install")) - self.enabled_cb.setText(_translate("EosWidget", "Activated")) + self.version_label.setText(_translate("EosWidget", "Version:")) + self.path_label.setText(_translate("EosWidget", "Path:")) + self.update_button.setText(_translate("EosWidget", "Update")) + self.uninstall_button.setText(_translate("EosWidget", "Uninstall")) if __name__ == "__main__": diff --git a/rare/ui/components/tabs/games/integrations/eos_widget.ui b/rare/ui/components/tabs/games/integrations/eos_widget.ui index 84e017bf..81dad003 100644 --- a/rare/ui/components/tabs/games/integrations/eos_widget.ui +++ b/rare/ui/components/tabs/games/integrations/eos_widget.ui @@ -6,179 +6,73 @@ 0 0 - 586 - 146 + 464 + 98 - - - 0 - 0 - - GroupBox Epic Overlay - + QLayout::SetDefaultConstraint - - QFrame::StyledPanel - - - QFrame::Raised - 0 - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + 0 - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft + + 0 - - - - Version + + 0 + + + 0 + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - error + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - Location - - - - - - - error - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 120 - 0 - - + + - Uninstall + Status: - - - - - 120 - 0 - - + + - Check for update - - - - - - - - 120 - 0 - - - - Update + Epic Online Services Overlay is not installed - - - - Qt::Vertical + + + + 0 - - - 6 - 6 - - - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft - - - - - Epic Overlay Services is not installed - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - 120 + 140 0 @@ -189,57 +83,86 @@ - - - - Qt::Vertical + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - 6 - 6 - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - + + + + Version: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Path: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + 140 + 0 + + + + Update + + + + + + + + 140 + 0 + + + + Uninstall + + + + - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - Activated - - - - - - - - true - - - - - - - - - - From 3c43da15945b908438ed7ad28951b4a6b1a36c4e Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sun, 10 Sep 2023 19:08:47 +0300 Subject: [PATCH 11/29] RareCore: Add options to exclude non-asset games and entitlements from startup * Add options in settings to not request non-asset games and entitlements. This greatly improves startup time but reduces functionality a bit. Both options are disabled by default. * Remove the old options to exclude Win32 and Mac games as they were no longer relevant. * Add a performance context manager to log execution time when used. * Fixed an issue with UbisoftGroup where multiple threads would spawn. --- rare/components/tabs/settings/legendary.py | 13 +++ rare/shared/rare_core.py | 33 ++++--- rare/shared/workers/fetch.py | 98 +++++++++++-------- rare/ui/components/tabs/settings/legendary.py | 16 ++- rare/ui/components/tabs/settings/legendary.ui | 26 ++++- 5 files changed, 127 insertions(+), 59 deletions(-) diff --git a/rare/components/tabs/settings/legendary.py b/rare/components/tabs/settings/legendary.py index c4254608..2b08a01b 100644 --- a/rare/components/tabs/settings/legendary.py +++ b/rare/components/tabs/settings/legendary.py @@ -116,6 +116,19 @@ class LegendarySettings(QWidget, Ui_LegendarySettings): lambda: self.settings.setValue("unreal_meta", self.fetch_unreal_check.isChecked()) ) + self.exclude_non_asset_check.setChecked( + self.settings.value("exclude_non_asset", False, bool) + ) + self.exclude_non_asset_check.stateChanged.connect( + lambda: self.settings.setValue("exclude_non_asset", self.exclude_non_asset_check.isChecked()) + ) + self.exclude_entitlements_check.setChecked( + self.settings.value("exclude_entitlements", False, bool) + ) + self.exclude_entitlements_check.stateChanged.connect( + lambda: self.settings.setValue("exclude_entitlements", self.exclude_entitlements_check.isChecked()) + ) + self.refresh_metadata_button.clicked.connect(self.refresh_metadata) # FIXME: Disable the button for now because it interferes with RareCore self.refresh_metadata_button.setEnabled(False) diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index 31a80f06..3582304a 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -9,12 +9,13 @@ from typing import Dict, Iterator, Callable, Optional, List, Union, Iterable, Tu from PyQt5.QtCore import QObject, pyqtSignal, QSettings, pyqtSlot, QThreadPool, QRunnable, QTimer from legendary.lfs.eos import EOSOverlayApp from legendary.models.game import Game, SaveGameFile -from requests import HTTPError +from requests.exceptions import HTTPError, ConnectionError from rare.lgndr.core import LegendaryCore from rare.models.base_game import RareSaveGame from rare.models.game import RareGame, RareEosOverlay from rare.models.signals import GlobalSignals +from rare.utils.metrics import timelogger from .image_manager import ImageManager from .workers import ( QueueWorker, @@ -50,7 +51,7 @@ class RareCore(QObject): self.__core: Optional[LegendaryCore] = None self.__image_manager: Optional[ImageManager] = None - self.__start_time = time.time() + self.__start_time = time.perf_counter() self.args(args) self.signals(init=True) @@ -310,12 +311,12 @@ class RareCore(QObject): self.progress.emit(15, self.tr("Preparing library")) self.__add_games_and_dlcs(*result) self.progress.emit(100, self.tr("Launching Rare")) - logger.debug(f"Fetch time {time.time() - self.__start_time} seconds") + logger.debug(f"Fetch time {time.perf_counter() - self.__start_time} seconds") QTimer.singleShot(100, self.__post_init) self.completed.emit() def fetch(self): - self.__start_time = time.time() + self.__start_time = time.perf_counter() fetch_worker = FetchWorker(self.__core, self.__args) fetch_worker.signals.progress.connect(self.progress) fetch_worker.signals.result.connect(self.__on_fetch_result) @@ -323,10 +324,10 @@ class RareCore(QObject): def fetch_saves(self): def __fetch() -> None: - start_time = time.time() saves_dict: Dict[str, List[SaveGameFile]] = {} try: - saves_list = self.__core.get_save_games() + with timelogger(logger, "Request saves"): + saves_list = self.__core.get_save_games() for s in saves_list: if s.app_name not in saves_dict.keys(): saves_dict[s.app_name] = [s] @@ -337,26 +338,28 @@ class RareCore(QObject): continue self.__library[app_name].load_saves(saves) except (HTTPError, ConnectionError) as e: - logger.error(f"Exception while fetching saves from EGS: {e}") + logger.error(f"Exception while fetching saves from EGS.") + logger.error(e) return - logger.debug(f"Saves: {len(saves_dict)}") - logger.debug(f"Request saves: {time.time() - start_time} seconds") + logger.info(f"Saves: {len(saves_dict)}") saves_worker = QRunnable.create(__fetch) QThreadPool.globalInstance().start(saves_worker) def fetch_entitlements(self) -> None: def __fetch() -> None: - start_time = time.time() try: - entitlements = self.__core.egs.get_user_entitlements() + if (entitlements := self.__core.lgd.entitlements) is None: + with timelogger(logger, "Request entitlements"): + entitlements = self.__core.egs.get_user_entitlements() + self.__core.lgd.entitlements = entitlements for game in self.__library.values(): game.grant_date() except (HTTPError, ConnectionError) as e: - logger.error(f"Failed to retrieve user entitlements from EGS: {e}") + logger.error(f"Exception while fetching entitlements from EGS.") + logger.error(e) return - logger.debug(f"Entitlements: {len(list(entitlements))}") - logger.debug(f"Request Entitlements: {time.time() - start_time} seconds") + logger.info(f"Entitlements: {len(list(entitlements))}") entitlements_worker = QRunnable.create(__fetch) QThreadPool.globalInstance().start(entitlements_worker) @@ -368,7 +371,7 @@ class RareCore(QObject): def __post_init(self) -> None: if not self.__args.offline: self.fetch_saves() - self.fetch_entitlements() + # self.fetch_entitlements() self.resolve_origin() @property diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py index b18ea83f..a1173ad5 100644 --- a/rare/shared/workers/fetch.py +++ b/rare/shared/workers/fetch.py @@ -1,13 +1,13 @@ import platform -import time from argparse import Namespace from enum import IntEnum from logging import getLogger from PyQt5.QtCore import QObject, pyqtSignal, QSettings -from requests.exceptions import ConnectionError, HTTPError +from requests.exceptions import HTTPError, ConnectionError from rare.lgndr.core import LegendaryCore +from rare.utils.metrics import timelogger from .worker import Worker logger = getLogger("FetchWorker") @@ -32,13 +32,11 @@ class FetchWorker(Worker): def run_real(self): # Fetch regular EGL games with assets - start_time = time.time() - want_unreal = self.settings.value("unreal_meta", False, bool) or self.args.debug want_win32 = self.settings.value("win32_meta", False, bool) want_macos = self.settings.value("macos_meta", False, bool) need_macos = platform.system() == "Darwin" - need_windows = not any([want_win32, want_macos, need_macos, self.args.debug]) + need_windows = not any([want_win32, want_macos, need_macos, self.args.debug]) and not self.args.offline if want_win32 or self.args.debug: logger.info( @@ -47,9 +45,10 @@ class FetchWorker(Worker): "with" if want_unreal else "without" ) self.signals.progress.emit(00, self.signals.tr("Updating game metadata for Windows")) - self.core.get_game_and_dlc_list( - update_assets=not self.args.offline, platform="Win32", skip_ue=not want_unreal - ) + with timelogger(logger, "Request Win32 games"): + self.core.get_game_and_dlc_list( + update_assets=not self.args.offline, platform="Win32", skip_ue=not want_unreal + ) if need_macos or want_macos or self.args.debug: logger.info( @@ -58,9 +57,10 @@ class FetchWorker(Worker): "with" if want_unreal else "without" ) self.signals.progress.emit(15, self.signals.tr("Updating game metadata for macOS")) - self.core.get_game_and_dlc_list( - update_assets=not self.args.offline, platform="Mac", skip_ue=not want_unreal - ) + with timelogger(logger, "Request macOS games"): + self.core.get_game_and_dlc_list( + update_assets=not self.args.offline, platform="Mac", skip_ue=not want_unreal + ) if need_windows: self.signals.progress.emit(00, self.signals.tr("Updating game metadata for Windows")) @@ -68,39 +68,53 @@ class FetchWorker(Worker): "Requesting Windows metadata, %s Unreal engine", "with" if want_unreal else "without" ) - games, dlc_dict = self.core.get_game_and_dlc_list( - update_assets=need_windows, platform="Windows", skip_ue=not want_unreal - ) - logger.debug(f"Games {len(games)}, games with DLCs {len(dlc_dict)}") - logger.debug(f"Request games: {time.time() - start_time} seconds") + with timelogger(logger, "Request Windows games"): + games, dlc_dict = self.core.get_game_and_dlc_list( + update_assets=need_windows, platform="Windows", skip_ue=not want_unreal + ) + logger.info(f"Games: %s. Games with DLCs: %s", len(games), len(dlc_dict)) + + want_non_asset = not self.settings.value("exclude_non_asset", False, bool) + want_entitlements = not self.settings.value("exclude_entitlements", False, bool) # Fetch non-asset games - self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata")) - start_time = time.time() - try: - na_games, na_dlc_dict = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False) - except (HTTPError, ConnectionError) as e: - logger.warning(f"Exception while fetching non asset games from EGS: {e}") - na_games, na_dlc_dict = ([], {}) - # FIXME: - # This is here because of broken appIds from Epic: - # https://discord.com/channels/826881530310819914/884510635642216499/1111321692703305729 - # There is a tab character in the appId of Fallout New Vegas: Honest Hearts DLC, this breaks metadata storage - # on Windows as they can't handle tabs at the end of the filename (?) - # Legendary and Heroic are also affected, but it completely breaks Rare, so dodge it for now pending a fix. - except Exception as e: - logger.error(f"Exception while fetching non asset games from EGS: {e}") - na_games, na_dlc_dict = ([], {}) - logger.debug(f"Non-asset {len(na_games)}, games with non-asset DLCs {len(na_dlc_dict)}") - logger.debug(f"Request non-asset: {time.time() - start_time} seconds") + if want_non_asset: + self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata")) + try: + with timelogger(logger, "Request non-asset"): + na_games, na_dlc_dict = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False) + except (HTTPError, ConnectionError) as e: + logger.error("Network exception while fetching non asset games from EGS.") + logger.error(e) + na_games, na_dlc_dict = ([], {}) + # FIXME: + # This is here because of broken appIds from Epic: + # https://discord.com/channels/826881530310819914/884510635642216499/1111321692703305729 + # There is a tab character in the appId of Fallout New Vegas: Honest Hearts DLC, this breaks metadata storage + # on Windows as they can't handle tabs at the end of the filename (?) + # Legendary and Heroic are also affected, but it completely breaks Rare, so dodge it for now pending a fix. + except Exception as e: + logger.error("General exception while fetching non asset games from EGS.") + logger.error(e) + na_games, na_dlc_dict = ([], {}) + logger.info("Non-asset: %s. Non-asset with DLCs: %s", len(na_games), len(na_dlc_dict)) - # Combine the two games lists and the two dlc dictionaries between regular and non-asset results - games += na_games - for catalog_id, dlcs in na_dlc_dict.items(): - if catalog_id in dlc_dict.keys(): - dlc_dict[catalog_id] += dlcs - else: - dlc_dict[catalog_id] = dlcs - logger.debug(f"Games {len(games)}, games with DLCs {len(dlc_dict)}") + # Combine the two games lists and the two dlc dictionaries between regular and non-asset results + games += na_games + for catalog_id, dlcs in na_dlc_dict.items(): + if catalog_id in dlc_dict.keys(): + dlc_dict[catalog_id] += dlcs + else: + dlc_dict[catalog_id] = dlcs + logger.info(f"Games: {len(games)}. Games with DLCs: {len(dlc_dict)}") + if want_entitlements: + # Get entitlements, Ubisoft integration also uses them + self.signals.progress.emit(40, self.signals.tr("Updating entitlements")) + with timelogger(logger, "Request entitlements"): + entitlements = self.core.egs.get_user_entitlements() + self.core.lgd.entitlements = entitlements + logger.info(f"Entitlements: {len(list(entitlements))}") + + self.signals.progress.emit(50, self.signals.tr("Preparing games")) self.signals.result.emit((games, dlc_dict), FetchWorker.Result.COMBINED) diff --git a/rare/ui/components/tabs/settings/legendary.py b/rare/ui/components/tabs/settings/legendary.py index 829a60ab..a8306397 100644 --- a/rare/ui/components/tabs/settings/legendary.py +++ b/rare/ui/components/tabs/settings/legendary.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/legendary.ui' # -# Created by: PyQt5 UI code generator 5.15.10 +# Created by: PyQt5 UI code generator 5.15.7 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -139,6 +139,12 @@ class Ui_LegendarySettings(object): self.fetch_unreal_check = QtWidgets.QCheckBox(self.metadata_group) self.fetch_unreal_check.setObjectName("fetch_unreal_check") self.verticalLayout_2.addWidget(self.fetch_unreal_check) + self.exclude_non_asset_check = QtWidgets.QCheckBox(self.meta_group) + self.exclude_non_asset_check.setObjectName("exclude_non_asset_check") + self.verticalLayout_2.addWidget(self.exclude_non_asset_check) + self.exclude_entitlements_check = QtWidgets.QCheckBox(self.meta_group) + self.exclude_entitlements_check.setObjectName("exclude_entitlements_check") + self.verticalLayout_2.addWidget(self.exclude_entitlements_check) self.metadata_info = QtWidgets.QLabel(self.metadata_group) font = QtGui.QFont() font.setItalic(True) @@ -175,6 +181,14 @@ class Ui_LegendarySettings(object): self.fetch_win32_check.setText(_translate("LegendarySettings", "Include Win32 games")) self.fetch_macos_check.setText(_translate("LegendarySettings", "Include macOS games")) self.fetch_unreal_check.setText(_translate("LegendarySettings", "Include Unreal engine")) + self.exclude_non_asset_check.setToolTip(_translate("LegendarySettings", "Do not load metadata for non-asset games (i.e. Origin games) on start-up.\n" + "\n" + "Disabling this greatly improves start-up time, but some games might not be visible in your library.")) + self.exclude_non_asset_check.setText(_translate("LegendarySettings", "Exclude non-asset games")) + self.exclude_entitlements_check.setToolTip(_translate("LegendarySettings", "Do not load entitlement data (i.e game\'s date of purchase) on start-up.\n" + "\n" + "Disabling this greatly improves start-up time, but some library filters may not work.")) + self.exclude_entitlements_check.setText(_translate("LegendarySettings", "Exclude entitlements")) self.metadata_info.setText(_translate("LegendarySettings", "Restart Rare to apply")) self.refresh_metadata_button.setText(_translate("LegendarySettings", "Refresh metadata")) diff --git a/rare/ui/components/tabs/settings/legendary.ui b/rare/ui/components/tabs/settings/legendary.ui index 65e69f1c..9f1a9c4c 100644 --- a/rare/ui/components/tabs/settings/legendary.ui +++ b/rare/ui/components/tabs/settings/legendary.ui @@ -231,7 +231,7 @@ Platforms - + @@ -253,6 +253,30 @@ + + + + Do not load metadata for non-asset games (i.e. Origin games) on start-up. + +Disabling this greatly improves start-up time, but some games might not be visible in your library. + + + Exclude non-asset games + + + + + + + Do not load entitlement data (i.e game's date of purchase) on start-up. + +Disabling this greatly improves start-up time, but some library filters may not work. + + + Exclude entitlements + + + From 89486f882e1749659e2c2cff18179b5c71431511 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sun, 10 Sep 2023 19:11:03 +0300 Subject: [PATCH 12/29] EOSGroup: Add the option to select which overlay installation to enable --- .../tabs/games/integrations/eos_group.py | 157 +++++++++--------- rare/models/game.py | 32 +++- rare/shared/workers/uninstall.py | 2 +- 3 files changed, 107 insertions(+), 84 deletions(-) diff --git a/rare/components/tabs/games/integrations/eos_group.py b/rare/components/tabs/games/integrations/eos_group.py index 154c820f..7cd8c163 100644 --- a/rare/components/tabs/games/integrations/eos_group.py +++ b/rare/components/tabs/games/integrations/eos_group.py @@ -4,8 +4,17 @@ from logging import getLogger from typing import Optional from PyQt5.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool, Qt, pyqtSlot, QSize -from PyQt5.QtWidgets import QGroupBox, QMessageBox, QFrame, QHBoxLayout, QSizePolicy, QLabel, QPushButton, QFormLayout -from legendary.lfs import eos +from PyQt5.QtWidgets import ( + QGroupBox, + QMessageBox, + QFrame, + QHBoxLayout, + QSizePolicy, + QLabel, + QPushButton, + QFormLayout, + QComboBox, +) from rare.lgndr.core import LegendaryCore from rare.models.game import RareEosOverlay @@ -42,55 +51,101 @@ class EosPrefixWidget(QFrame): self.indicator = QLabel(parent=self) self.indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred) - self.label = ElideLabel( - prefix if prefix is not None else "Epic Online Services Overlay", - parent=self - ) + self.prefix_label = ElideLabel(prefix if prefix is not None else overlay.app_title, parent=self) + self.overlay_label = ElideLabel(parent=self) + self.overlay_label.setDisabled(True) + + self.path_select = QComboBox(self) + self.path_select.setMaximumWidth(150) + self.path_select.setMinimumWidth(150) self.button = QPushButton(parent=self) self.button.setMinimumWidth(150) - self.button.clicked.connect(self.action) layout = QHBoxLayout(self) layout.setContentsMargins(-1, 0, 0, 0) layout.addWidget(self.indicator) - layout.addWidget(self.label, stretch=1) + layout.addWidget(self.prefix_label, stretch=2) + layout.addWidget(self.overlay_label, stretch=3) + layout.addWidget(self.path_select) layout.addWidget(self.button) self.overlay = overlay self.prefix = prefix + self.path_select.currentIndexChanged.connect(self.path_changed) + self.button.clicked.connect(self.action) self.overlay.signals.game.installed.connect(self.update_state) self.overlay.signals.game.uninstalled.connect(self.update_state) self.update_state() + @pyqtSlot(int) + def path_changed(self, index: int) -> None: + path = self.path_select.itemData(index, Qt.UserRole) + active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" + if self.overlay.is_enabled(self.prefix) and (path == active_path): + self.button.setText(self.tr("Disable overlay")) + else: + self.button.setText(self.tr("Enable overlay")) + @pyqtSlot() - def update_state(self): - if not self.overlay.is_installed: + def update_state(self) -> None: + active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" + + self.overlay_label.setText(f"{active_path}<\i>") + self.path_select.clear() + + if not self.overlay.is_installed and not self.overlay.available_paths(self.prefix): self.setDisabled(True) - self.button.setText(self.tr("Unavailable")) self.indicator.setPixmap(icon("fa.circle-o", color="grey").pixmap(20, 20)) + self.overlay_label.setText(self.overlay.active_path(self.prefix)) + self.button.setText(self.tr("Unavailable")) return self.setDisabled(False) + if self.overlay.is_enabled(self.prefix): - self.button.setText(self.tr("Disable overlay")) - self.indicator.setPixmap( - icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20)) - ) + self.indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20))) else: - self.button.setText(self.tr("Enable overlay")) - self.indicator.setPixmap( - icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20)) - ) + self.indicator.setPixmap(icon("fa.times-circle-o", color="red").pixmap(QSize(20, 20))) + + install_path = os.path.normpath(p) if (p := self.overlay.install_path) else "" + + self.path_select.addItem("Auto-detect", "") + self.path_select.setItemData(0, "Auto-detect", Qt.ToolTipRole) + for path in self.overlay.available_paths(self.prefix): + path = os.path.normpath(path) + self.path_select.addItem("Legendary-managed" if path == install_path else "EGL-managed", path) + self.path_select.setItemData(self.path_select.findData(path), path, Qt.ToolTipRole) + self.path_select.setCurrentIndex(self.path_select.findData(active_path)) @pyqtSlot() - def action(self): - if self.overlay.is_enabled(self.prefix): - self.overlay.disable(prefix=self.prefix) + def action(self) -> None: + path = self.path_select.currentData(Qt.UserRole) + active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" + install_path = os.path.normpath(p) if (p := self.overlay.install_path) else "" + if self.overlay.is_enabled(self.prefix) and (path == active_path): + if not self.overlay.disable(prefix=self.prefix): + QMessageBox.warning( + self, "Warning", + self.tr("Failed to completely disable the active EOS Overlay.{}").format( + self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.") + if active_path != install_path else "" + ), + ) else: - self.overlay.enable(prefix=self.prefix) + self.overlay.disable(prefix=self.prefix) + if not self.overlay.enable(prefix=self.prefix, path=path): + QMessageBox.warning( + self, + "Warning", + self.tr("Failed to completely enable EOS overlay.{}").format( + self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.") + if active_path != install_path + else "" + ), + ) self.update_state() @@ -129,7 +184,7 @@ class EosGroup(QGroupBox): if self.overlay.is_installed: # installed self.installed_version_label.setText(f"{self.overlay.version}") - self.installed_path_label.setText(self.overlay.install_path) + self.installed_path_label.setText(os.path.normpath(self.overlay.install_path)) self.ui.overlay_stack.setCurrentWidget(self.ui.info_page) else: self.ui.overlay_stack.setCurrentWidget(self.ui.install_page) @@ -185,65 +240,13 @@ class EosGroup(QGroupBox): def uninstall_finished(self): self.ui.overlay_stack.setCurrentWidget(self.ui.install_page) - def change_enable(self): - enabled = self.ui.enabled_cb.isChecked() - if not enabled: - try: - eos.remove_registry_entries(self.current_prefix) - except PermissionError: - logger.error("Can't disable eos overlay") - QMessageBox.warning(self, "Error", self.tr( - "Failed to disable Overlay. Probably it is installed by Epic Games Launcher")) - return - logger.info("Disabled Epic Overlay") - self.ui.enabled_info_label.setText(self.tr("Disabled")) - else: - if not self.overlay.is_installed: - available_installs = self.core.search_overlay_installs(self.current_prefix) - if not available_installs: - logger.error('No EOS overlay installs found!') - return - path = available_installs[0] - else: - path = self.overlay.install_path - - if not self.core.is_overlay_install(path): - logger.error(f'Not a valid Overlay installation: {path}') - self.ui.select_pfx_combo.removeItem(self.ui.select_pfx_combo.currentIndex()) - return - - path = os.path.normpath(path) - reg_paths = eos.query_registry_entries(self.current_prefix) - if old_path := reg_paths["overlay_path"]: - if os.path.normpath(old_path) == path: - logger.info(f'Overlay already enabled, nothing to do.') - return - else: - logger.info(f'Updating overlay registry entries from "{old_path}" to "{path}"') - try: - eos.remove_registry_entries(self.current_prefix) - except PermissionError: - logger.error("Can't disable eos overlay") - QMessageBox.warning(self, "Error", self.tr( - "Failed to disable Overlay. Probably it is installed by Epic Games Launcher")) - return - try: - eos.add_registry_entries(path, self.current_prefix) - except PermissionError: - logger.error("Failed to disable eos overlay") - QMessageBox.warning(self, "Error", self.tr( - "Failed to enable EOS overlay. Maybe it is already installed by Epic Games Launcher")) - return - self.ui.enabled_info_label.setText(self.tr("Enabled")) - logger.info(f'Enabled overlay at: {path}') - @pyqtSlot() def install_overlay(self): self.overlay.install() def uninstall_overlay(self): if not self.overlay.is_installed: - logger.error('No Rare-managed overlay installation found.') + logger.error("No Legendary-managed overlay installation found.") self.ui.overlay_stack.setCurrentWidget(self.ui.install_page) return self.overlay.uninstall() diff --git a/rare/models/game.py b/rare/models/game.py index f45b8709..557a8549 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -583,13 +583,23 @@ class RareEosOverlay(RareGameBase): reg_paths = eos.query_registry_entries(prefix) return reg_paths["overlay_path"] and self.core.is_overlay_install(reg_paths["overlay_path"]) + def active_path(self, prefix: Optional[str] = None) -> str: + path = eos.query_registry_entries(prefix)["overlay_path"] + return path if path and self.core.is_overlay_install(path) else "" + + def available_paths(self, prefix: Optional[str] = None) -> List[str]: + return self.core.search_overlay_installs(prefix) + def enable( - self, prefix: Optional[str] = None, app_name: Optional[str] = None, path: Optional[str] = None + self, prefix: Optional[str] = None, path: Optional[str] = None ) -> bool: - if not self.is_installed or self.is_enabled(prefix): + if self.is_enabled(prefix): return False if not path: - path = self.igame.install_path + if self.is_installed: + path = self.igame.install_path + else: + path = self.available_paths(prefix)[-1] reg_paths = eos.query_registry_entries(prefix) if old_path := reg_paths["overlay_path"]: if os.path.normpath(old_path) == path: @@ -598,15 +608,25 @@ class RareEosOverlay(RareGameBase): else: logger.info(f'Updating overlay registry entries from "{old_path}" to "{path}"') eos.remove_registry_entries(prefix) - eos.add_registry_entries(path, prefix) + try: + eos.add_registry_entries(path, prefix) + except PermissionError as e: + logger.error("Exception while writing registry to enable the overlay .") + logger.error(e) + return False logger.info(f"Enabled overlay at: {path} for prefix: {prefix}") return True - def disable(self, prefix: Optional[str] = None, app_name: Optional[str] = None) -> bool: + def disable(self, prefix: Optional[str] = None) -> bool: if not self.is_enabled(prefix): return False logger.info(f"Disabling overlay (removing registry keys) for prefix: {prefix}") - eos.remove_registry_entries(prefix) + try: + eos.remove_registry_entries(prefix) + except PermissionError as e: + logger.error("Exception while writing registry to disable the overlay.") + logger.error(e) + return False return True def install(self) -> bool: diff --git a/rare/shared/workers/uninstall.py b/rare/shared/workers/uninstall.py index 9b154ce8..98fe21b7 100644 --- a/rare/shared/workers/uninstall.py +++ b/rare/shared/workers/uninstall.py @@ -40,7 +40,7 @@ def uninstall_game( remove_registry_entries(prefix) logger.debug("Removed registry entries for prefix %s", prefix) else: - remove_registry_entries(None) + remove_registry_entries() return True, "" From 889a7cd116a3fd90df4dd8d5f205c105e1374bad Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:50:59 +0300 Subject: [PATCH 13/29] RareGame: Use the callable directly instead of a lambda to create workers. --- rare/models/base_game.py | 4 ++-- rare/models/game.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/rare/models/base_game.py b/rare/models/base_game.py index 0cd1611c..ab34b466 100644 --- a/rare/models/base_game.py +++ b/rare/models/base_game.py @@ -268,7 +268,7 @@ class RareGameSlim(RareGameBase): return if thread: - worker = QRunnable.create(lambda: _upload()) + worker = QRunnable.create(_upload) QThreadPool.globalInstance().start(worker) else: _upload() @@ -293,7 +293,7 @@ class RareGameSlim(RareGameBase): return if thread: - worker = QRunnable.create(lambda: _download()) + worker = QRunnable.create(_download) QThreadPool.globalInstance().start(worker) else: _download() diff --git a/rare/models/game.py b/rare/models/game.py index 557a8549..80c90457 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -434,9 +434,12 @@ class RareGame(RareGameSlim): elapsed_time = abs(datetime.utcnow() - self.metadata.steam_date) if self.metadata.steam_grade is not None and elapsed_time.days < 3: return self.metadata.steam_grade - worker = QRunnable.create( - lambda: self.set_steam_grade(get_rating(self.core, self.app_name)) - ) + + def _set_steam_grade(): + rating = get_rating(self.core, self.app_name) + self.set_steam_grade(rating) + + worker = QRunnable.create(_set_steam_grade) QThreadPool.globalInstance().start(worker) return "pending" @@ -447,9 +450,10 @@ class RareGame(RareGameSlim): self.signals.widget.update.emit() def grant_date(self, force=False) -> datetime: + if (entitlements := self.core.lgd.entitlements) is None: + return self.metadata.grant_date if self.metadata.grant_date is None or force: logger.debug("Grant date for %s not found in metadata, resolving", self.app_name) - entitlements = self.core.lgd.entitlements matching = filter(lambda ent: ent["namespace"] == self.game.namespace, entitlements) entitlement = next(matching, None) grant_date = datetime.fromisoformat( From acbe8836cc8895f947c10532b0938d3870e41a8c Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Wed, 13 Sep 2023 21:23:01 +0300 Subject: [PATCH 14/29] RareEosOverlay: Protect against invalid prefixes --- rare/models/game.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rare/models/game.py b/rare/models/game.py index 80c90457..0c305089 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -583,16 +583,29 @@ class RareEosOverlay(RareGameBase): self.core.check_for_overlay_updates() return self.core.overlay_update_available - def is_enabled(self, prefix: Optional[str] = None): - reg_paths = eos.query_registry_entries(prefix) + def is_enabled(self, prefix: Optional[str] = None) -> bool: + try: + reg_paths = eos.query_registry_entries(prefix) + except ValueError as e: + logger.info("%s %s", e, prefix) + return False return reg_paths["overlay_path"] and self.core.is_overlay_install(reg_paths["overlay_path"]) def active_path(self, prefix: Optional[str] = None) -> str: - path = eos.query_registry_entries(prefix)["overlay_path"] + try: + path = eos.query_registry_entries(prefix)["overlay_path"] + except ValueError as e: + logger.info("%s %s", e, prefix) + return "" return path if path and self.core.is_overlay_install(path) else "" def available_paths(self, prefix: Optional[str] = None) -> List[str]: - return self.core.search_overlay_installs(prefix) + try: + installs = self.core.search_overlay_installs(prefix) + except ValueError as e: + logger.info("%s %s", e, prefix) + return [] + return installs def enable( self, prefix: Optional[str] = None, path: Optional[str] = None From 7ae06ff5d8312ea2a3325070cbb13f5e9cb1afe8 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 15 Sep 2023 00:40:28 +0300 Subject: [PATCH 15/29] RareCore: Move entitlements request into an independent worker Yes, we are back at this. Entitlements are important to have early as Ubisoft redemption requires them, and they don't depend on anything else. * Move config helper initialization into RareCore to make it available earlier. --- rare/components/__init__.py | 2 +- rare/shared/rare_core.py | 73 ++++++++++++++++++--------------- rare/shared/workers/__init__.py | 2 +- rare/shared/workers/fetch.py | 58 +++++++++++++++----------- 4 files changed, 78 insertions(+), 57 deletions(-) diff --git a/rare/components/__init__.py b/rare/components/__init__.py index e76dacf1..f4c9cce4 100644 --- a/rare/components/__init__.py +++ b/rare/components/__init__.py @@ -12,7 +12,7 @@ from requests import HTTPError from rare.components.dialogs.launch_dialog import LaunchDialog from rare.components.main_window import MainWindow from rare.shared import RareCore -from rare.utils import config_helper, paths +from rare.utils import paths from rare.utils.misc import ExitCodes from rare.widgets.rare_app import RareApp, RareAppException diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index 3582304a..9afcaac2 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -16,12 +16,15 @@ from rare.models.base_game import RareSaveGame from rare.models.game import RareGame, RareEosOverlay from rare.models.signals import GlobalSignals from rare.utils.metrics import timelogger +from rare.utils import config_helper from .image_manager import ImageManager from .workers import ( QueueWorker, VerifyWorker, MoveWorker, FetchWorker, + GamesDlcsWorker, + EntitlementsWorker, OriginWineWorker, ) from .workers.uninstall import uninstall_game @@ -70,6 +73,10 @@ class RareCore(QObject): self.__eos_overlay.signals.game.install.connect(self.__signals.game.install) self.__eos_overlay.signals.game.uninstall.connect(self.__signals.game.uninstall) + self.__fetch_progress: int = 0 + self.__fetched_games_dlcs: bool = False + self.__fetched_entitlements: bool = False + RareCore.__instance = self def enqueue_worker(self, rgame: RareGame, worker: QueueWorker): @@ -303,24 +310,45 @@ class RareCore(QObject): logger.info(f'Marking "{rgame.app_title}" as not installed because an exception has occurred...') logger.error(e) rgame.set_installed(False) - self.progress.emit(int(idx/length * 80) + 20, self.tr("Loaded {}").format(rgame.app_title)) + progress = int(idx/length * self.__fetch_progress) + (100 - self.__fetch_progress) + self.progress.emit(progress, self.tr("Loaded {}").format(rgame.app_title)) + + @pyqtSlot(int, str) + def __on_fetch_progress(self, increment: int, message: str): + self.__fetch_progress += increment + self.progress.emit(self.__fetch_progress, message) @pyqtSlot(object, int) - def __on_fetch_result(self, result: Tuple[List, Dict], res_type: int): - logger.info(f"Got API results for {FetchWorker.Result(res_type).name}") - self.progress.emit(15, self.tr("Preparing library")) - self.__add_games_and_dlcs(*result) - self.progress.emit(100, self.tr("Launching Rare")) - logger.debug(f"Fetch time {time.perf_counter() - self.__start_time} seconds") - QTimer.singleShot(100, self.__post_init) - self.completed.emit() + def __on_fetch_result(self, result: Tuple, result_type: int): + if result_type == FetchWorker.Result.GAMESDLCS: + self.__add_games_and_dlcs(*result) + self.__fetched_games_dlcs = True + + if result_type == FetchWorker.Result.ENTITLEMENTS: + self.__core.lgd.entitlements = result + self.__fetched_entitlements = True + + logger.info(f"Acquired data for {FetchWorker.Result(result_type).name}") + + if all([self.__fetched_games_dlcs, self.__fetched_entitlements]): + logger.debug(f"Fetch time {time.perf_counter() - self.__start_time} seconds") + self.progress.emit(100, self.tr("Launching Rare")) + self.completed.emit() + QTimer.singleShot(100, self.__post_init) def fetch(self): self.__start_time = time.perf_counter() - fetch_worker = FetchWorker(self.__core, self.__args) - fetch_worker.signals.progress.connect(self.progress) - fetch_worker.signals.result.connect(self.__on_fetch_result) - QThreadPool.globalInstance().start(fetch_worker) + + games_dlcs_worker = GamesDlcsWorker(self.__core, self.__args) + games_dlcs_worker.signals.progress.connect(self.__on_fetch_progress) + games_dlcs_worker.signals.result.connect(self.__on_fetch_result) + + entitlements_worker = EntitlementsWorker(self.__core, self.__args) + entitlements_worker.signals.progress.connect(self.__on_fetch_progress) + entitlements_worker.signals.result.connect(self.__on_fetch_result) + + QThreadPool.globalInstance().start(games_dlcs_worker) + QThreadPool.globalInstance().start(entitlements_worker) def fetch_saves(self): def __fetch() -> None: @@ -346,24 +374,6 @@ class RareCore(QObject): saves_worker = QRunnable.create(__fetch) QThreadPool.globalInstance().start(saves_worker) - def fetch_entitlements(self) -> None: - def __fetch() -> None: - try: - if (entitlements := self.__core.lgd.entitlements) is None: - with timelogger(logger, "Request entitlements"): - entitlements = self.__core.egs.get_user_entitlements() - self.__core.lgd.entitlements = entitlements - for game in self.__library.values(): - game.grant_date() - except (HTTPError, ConnectionError) as e: - logger.error(f"Exception while fetching entitlements from EGS.") - logger.error(e) - return - logger.info(f"Entitlements: {len(list(entitlements))}") - - entitlements_worker = QRunnable.create(__fetch) - QThreadPool.globalInstance().start(entitlements_worker) - def resolve_origin(self) -> None: origin_worker = OriginWineWorker(self.__core, list(self.origin_games)) QThreadPool.globalInstance().start(origin_worker) @@ -371,7 +381,6 @@ class RareCore(QObject): def __post_init(self) -> None: if not self.__args.offline: self.fetch_saves() - # self.fetch_entitlements() self.resolve_origin() @property diff --git a/rare/shared/workers/__init__.py b/rare/shared/workers/__init__.py index b11df089..12d816c0 100644 --- a/rare/shared/workers/__init__.py +++ b/rare/shared/workers/__init__.py @@ -1,4 +1,4 @@ -from .fetch import FetchWorker +from .fetch import FetchWorker, GamesDlcsWorker, EntitlementsWorker from .install_info import InstallInfoWorker from .move import MoveWorker from .uninstall import UninstallWorker diff --git a/rare/shared/workers/fetch.py b/rare/shared/workers/fetch.py index a1173ad5..1d4fe761 100644 --- a/rare/shared/workers/fetch.py +++ b/rare/shared/workers/fetch.py @@ -15,9 +15,9 @@ logger = getLogger("FetchWorker") class FetchWorker(Worker): class Result(IntEnum): - GAMES = 1 - NON_ASSET = 2 - COMBINED = 3 + ERROR = 0 + GAMESDLCS = 1 + ENTITLEMENTS = 2 class Signals(QObject): progress = pyqtSignal(int, str) @@ -30,7 +30,33 @@ class FetchWorker(Worker): self.args = args self.settings = QSettings() + +class EntitlementsWorker(FetchWorker): + def __init__(self, core: LegendaryCore, args: Namespace): + super(EntitlementsWorker, self).__init__(core, args) + def run_real(self): + entitlements = () + want_entitlements = not self.settings.value("exclude_entitlements", False, bool) + if want_entitlements: + # Get entitlements, Ubisoft integration also uses them + self.signals.progress.emit(0, self.signals.tr("Updating entitlements")) + with timelogger(logger, "Request entitlements"): + entitlements = self.core.egs.get_user_entitlements() + self.core.lgd.entitlements = entitlements + logger.info(f"Entitlements: %s", len(list(entitlements))) + self.signals.result.emit(entitlements, FetchWorker.Result.ENTITLEMENTS) + return + + +class GamesDlcsWorker(FetchWorker): + + def __init__(self, core: LegendaryCore, args: Namespace): + super(GamesDlcsWorker, self).__init__(core, args) + self.exclude_non_asset = QSettings().value("exclude_non_asset", False, bool) + + def run_real(self): + # Fetch regular EGL games with assets want_unreal = self.settings.value("unreal_meta", False, bool) or self.args.debug want_win32 = self.settings.value("win32_meta", False, bool) @@ -74,25 +100,19 @@ class FetchWorker(Worker): ) logger.info(f"Games: %s. Games with DLCs: %s", len(games), len(dlc_dict)) - want_non_asset = not self.settings.value("exclude_non_asset", False, bool) - want_entitlements = not self.settings.value("exclude_entitlements", False, bool) - # Fetch non-asset games + want_non_asset = not self.settings.value("exclude_non_asset", False, bool) if want_non_asset: self.signals.progress.emit(30, self.signals.tr("Updating non-asset game metadata")) try: with timelogger(logger, "Request non-asset"): na_games, na_dlc_dict = self.core.get_non_asset_library_items(force_refresh=False, skip_ue=False) except (HTTPError, ConnectionError) as e: - logger.error("Network exception while fetching non asset games from EGS.") + logger.error(f"Network error while fetching non asset games") logger.error(e) na_games, na_dlc_dict = ([], {}) - # FIXME: - # This is here because of broken appIds from Epic: - # https://discord.com/channels/826881530310819914/884510635642216499/1111321692703305729 - # There is a tab character in the appId of Fallout New Vegas: Honest Hearts DLC, this breaks metadata storage - # on Windows as they can't handle tabs at the end of the filename (?) - # Legendary and Heroic are also affected, but it completely breaks Rare, so dodge it for now pending a fix. + # NOTE: This is here because of broken appIds from Epic + # https://discord.com/channels/826881530310819914/884510635642216499/1111321692703305729 except Exception as e: logger.error("General exception while fetching non asset games from EGS.") logger.error(e) @@ -108,13 +128,5 @@ class FetchWorker(Worker): dlc_dict[catalog_id] = dlcs logger.info(f"Games: {len(games)}. Games with DLCs: {len(dlc_dict)}") - if want_entitlements: - # Get entitlements, Ubisoft integration also uses them - self.signals.progress.emit(40, self.signals.tr("Updating entitlements")) - with timelogger(logger, "Request entitlements"): - entitlements = self.core.egs.get_user_entitlements() - self.core.lgd.entitlements = entitlements - logger.info(f"Entitlements: {len(list(entitlements))}") - - self.signals.progress.emit(50, self.signals.tr("Preparing games")) - self.signals.result.emit((games, dlc_dict), FetchWorker.Result.COMBINED) + self.signals.progress.emit(40, self.signals.tr("Preparing library")) + self.signals.result.emit((games, dlc_dict), FetchWorker.Result.GAMESDLCS) From a605bddffaebc9f04c0deb7ff7e495e2ed72a968 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:09:57 +0200 Subject: [PATCH 16/29] RareGame: Move `sdl_name` to RareGameBase InstallDialog uses that property so RareEosOverlay should have it too. Also calculate the base_path for the overlay case instead of passing it as argument --- rare/components/dialogs/install_dialog.py | 3 ++- rare/models/base_game.py | 5 +++++ rare/models/game.py | 12 ++---------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index 1f5828f7..a8f23c25 100644 --- a/rare/components/dialogs/install_dialog.py +++ b/rare/components/dialogs/install_dialog.py @@ -235,7 +235,8 @@ class InstallDialog(ActionDialog): self.error_box() def get_options(self): - self.__options.base_path = "" if self.rgame.is_installed else self.install_dir_edit.text() + base_path = os.path.join(self.install_dir_edit.text(), ".overlay" if self.__options.overlay else "") + self.__options.base_path = "" if self.rgame.is_installed else base_path self.__options.platform = self.ui.platform_combo.currentText() self.__options.create_shortcut = self.ui.shortcut_check.isChecked() self.__options.max_workers = self.advanced.ui.max_workers_spin.value() diff --git a/rare/models/base_game.py b/rare/models/base_game.py index ab34b466..0033530a 100644 --- a/rare/models/base_game.py +++ b/rare/models/base_game.py @@ -8,6 +8,7 @@ from typing import Optional, List, Tuple from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool, QSettings from legendary.lfs import eos from legendary.models.game import SaveGameFile, SaveGameStatus, Game, InstalledGame +from legendary.utils.selective_dl import get_sdl_appname from rare.lgndr.core import LegendaryCore from rare.models.install import UninstallOptionsModel, InstallOptionsModel @@ -178,6 +179,10 @@ class RareGameBase(QObject): except AttributeError: return False + @property + def sdl_name(self) -> Optional[str]: + return get_sdl_appname(self.app_name) + @property def version(self) -> str: """! diff --git a/rare/models/game.py b/rare/models/game.py index 0c305089..6c255a24 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -11,7 +11,6 @@ from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool from PyQt5.QtGui import QPixmap from legendary.lfs import eos from legendary.models.game import Game, InstalledGame -from legendary.utils.selective_dl import get_sdl_appname from rare.lgndr.core import LegendaryCore from rare.models.base_game import RareGameBase, RareGameSlim @@ -413,10 +412,6 @@ class RareGame(RareGameSlim): else self.app_title ) - @property - def sdl_name(self) -> Optional[str]: - return get_sdl_appname(self.app_name) - @property def save_path(self) -> Optional[str]: return super(RareGame, self).save_path @@ -628,7 +623,7 @@ class RareEosOverlay(RareGameBase): try: eos.add_registry_entries(path, prefix) except PermissionError as e: - logger.error("Exception while writing registry to enable the overlay .") + logger.error("Exception while writing registry to enable the overlay.") logger.error(e) return False logger.info(f"Enabled overlay at: {path} for prefix: {prefix}") @@ -652,10 +647,7 @@ class RareEosOverlay(RareGameBase): if self.is_installed: base_path = self.igame.install_path else: - base_path = os.path.join( - self.core.lgd.config.get("Legendary", "install_dir", fallback=os.path.expanduser("~/legendary")), - ".overlay" - ) + base_path = self.core.get_default_install_dir() self.signals.game.install.emit( InstallOptionsModel( app_name=self.app_name, base_path=base_path, platform="Windows", overlay=True From 451017e2e21447f3b34b79defa095947a567bd74 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:10:30 +0200 Subject: [PATCH 17/29] Ui: Fix and issue with legendary settings UI file --- rare/ui/components/tabs/settings/legendary.py | 34 +++++++++---------- rare/ui/components/tabs/settings/legendary.ui | 4 +-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/rare/ui/components/tabs/settings/legendary.py b/rare/ui/components/tabs/settings/legendary.py index a8306397..8bdcc871 100644 --- a/rare/ui/components/tabs/settings/legendary.py +++ b/rare/ui/components/tabs/settings/legendary.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/legendary.ui' # -# Created by: PyQt5 UI code generator 5.15.7 +# Created by: PyQt5 UI code generator 5.15.10 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_LegendarySettings(object): def setupUi(self, LegendarySettings): LegendarySettings.setObjectName("LegendarySettings") - LegendarySettings.resize(681, 456) + LegendarySettings.resize(608, 420) LegendarySettings.setWindowTitle("LegendarySettings") self.legendary_layout = QtWidgets.QHBoxLayout(LegendarySettings) self.legendary_layout.setObjectName("legendary_layout") @@ -128,32 +128,32 @@ class Ui_LegendarySettings(object): self.right_layout.addWidget(self.cleanup_group) self.metadata_group = QtWidgets.QGroupBox(LegendarySettings) self.metadata_group.setObjectName("metadata_group") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.metadata_group) - self.verticalLayout_2.setObjectName("verticalLayout_2") + self.metadata_layout = QtWidgets.QVBoxLayout(self.metadata_group) + self.metadata_layout.setObjectName("metadata_layout") self.fetch_win32_check = QtWidgets.QCheckBox(self.metadata_group) self.fetch_win32_check.setObjectName("fetch_win32_check") - self.verticalLayout_2.addWidget(self.fetch_win32_check) + self.metadata_layout.addWidget(self.fetch_win32_check) self.fetch_macos_check = QtWidgets.QCheckBox(self.metadata_group) self.fetch_macos_check.setObjectName("fetch_macos_check") - self.verticalLayout_2.addWidget(self.fetch_macos_check) + self.metadata_layout.addWidget(self.fetch_macos_check) self.fetch_unreal_check = QtWidgets.QCheckBox(self.metadata_group) self.fetch_unreal_check.setObjectName("fetch_unreal_check") - self.verticalLayout_2.addWidget(self.fetch_unreal_check) - self.exclude_non_asset_check = QtWidgets.QCheckBox(self.meta_group) + self.metadata_layout.addWidget(self.fetch_unreal_check) + self.exclude_non_asset_check = QtWidgets.QCheckBox(self.metadata_group) self.exclude_non_asset_check.setObjectName("exclude_non_asset_check") - self.verticalLayout_2.addWidget(self.exclude_non_asset_check) - self.exclude_entitlements_check = QtWidgets.QCheckBox(self.meta_group) + self.metadata_layout.addWidget(self.exclude_non_asset_check) + self.exclude_entitlements_check = QtWidgets.QCheckBox(self.metadata_group) self.exclude_entitlements_check.setObjectName("exclude_entitlements_check") - self.verticalLayout_2.addWidget(self.exclude_entitlements_check) + self.metadata_layout.addWidget(self.exclude_entitlements_check) self.metadata_info = QtWidgets.QLabel(self.metadata_group) font = QtGui.QFont() font.setItalic(True) self.metadata_info.setFont(font) self.metadata_info.setObjectName("metadata_info") - self.verticalLayout_2.addWidget(self.metadata_info) + self.metadata_layout.addWidget(self.metadata_info) self.refresh_metadata_button = QtWidgets.QPushButton(self.metadata_group) self.refresh_metadata_button.setObjectName("refresh_metadata_button") - self.verticalLayout_2.addWidget(self.refresh_metadata_button) + self.metadata_layout.addWidget(self.refresh_metadata_button) self.right_layout.addWidget(self.metadata_group) spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.right_layout.addItem(spacerItem1) @@ -182,12 +182,12 @@ class Ui_LegendarySettings(object): self.fetch_macos_check.setText(_translate("LegendarySettings", "Include macOS games")) self.fetch_unreal_check.setText(_translate("LegendarySettings", "Include Unreal engine")) self.exclude_non_asset_check.setToolTip(_translate("LegendarySettings", "Do not load metadata for non-asset games (i.e. Origin games) on start-up.\n" - "\n" - "Disabling this greatly improves start-up time, but some games might not be visible in your library.")) +"\n" +"Disabling this greatly improves start-up time, but some games might not be visible in your library.")) self.exclude_non_asset_check.setText(_translate("LegendarySettings", "Exclude non-asset games")) self.exclude_entitlements_check.setToolTip(_translate("LegendarySettings", "Do not load entitlement data (i.e game\'s date of purchase) on start-up.\n" - "\n" - "Disabling this greatly improves start-up time, but some library filters may not work.")) +"\n" +"Disabling this greatly improves start-up time, but some library filters may not work.")) self.exclude_entitlements_check.setText(_translate("LegendarySettings", "Exclude entitlements")) self.metadata_info.setText(_translate("LegendarySettings", "Restart Rare to apply")) self.refresh_metadata_button.setText(_translate("LegendarySettings", "Refresh metadata")) diff --git a/rare/ui/components/tabs/settings/legendary.ui b/rare/ui/components/tabs/settings/legendary.ui index 9f1a9c4c..2aab38d6 100644 --- a/rare/ui/components/tabs/settings/legendary.ui +++ b/rare/ui/components/tabs/settings/legendary.ui @@ -6,8 +6,8 @@ 0 0 - 681 - 456 + 608 + 420 From 99d0bca5fcd478517a6c8b73628408eb3932ac52 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:12:33 +0200 Subject: [PATCH 18/29] ConfigHelper: Extend with specialized methods for environment variables and wine/proton prefixes --- rare/utils/config_helper.py | 79 ++++++++++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/rare/utils/config_helper.py b/rare/utils/config_helper.py index 31ef5973..66b7ac5e 100644 --- a/rare/utils/config_helper.py +++ b/rare/utils/config_helper.py @@ -20,26 +20,39 @@ def save_config(): _save_config() -def add_option(app_name: str, option: str, value: str): +def add_option(app_name: str, option: str, value: str) -> None: value = value.replace("%%", "%").replace("%", "%%") if not _config.has_section(app_name): _config[app_name] = {} - _config.set(app_name, option, value) save_config() -def remove_option(app_name, option): +def add_envvar(app_name: str, envvar: str, value: str) -> None: + add_option(f"{app_name}.env", envvar, value) + + +def remove_option(app_name: str, option: str) -> None: if _config.has_option(app_name, option): _config.remove_option(app_name, option) - # if _config.has_section(app_name) and not _config[app_name]: # _config.remove_section(app_name) - save_config() -def remove_section(app_name): +def remove_envvar(app_name: str, option: str) -> None: + remove_option(f"{app_name}.env", option) + + +def get_option(app_name: str, option: str, fallback: Any = None) -> str: + return _config.get(app_name, option, fallback=fallback) + + +def get_envvar(app_name: str, option: str, fallback: Any = None) -> str: + return get_option(f"{app_name}.env", option, fallback=fallback) + + +def remove_section(app_name: str) -> None: return # Disabled due to env variables implementation if _config.has_section(app_name): @@ -61,13 +74,14 @@ def get_game_envvar(option: str, app_name: Optional[str] = None, fallback: Any = return _option +def get_proton_compat_data(app_name: Optional[str] = None, fallback: Any = None) -> str: + _compat = get_game_envvar("STEAM_COMPAT_DATA_PATH", app_name, fallback=fallback) + # return os.path.join(_compat, "pfx") if _compat else fallback + return _compat + + def get_wine_prefix(app_name: Optional[str] = None, fallback: Any = None) -> str: - _prefix = os.path.join( - _config.get("default.env", "STEAM_COMPAT_DATA_PATH", fallback=fallback), "pfx") - if app_name is not None: - _prefix = os.path.join( - _config.get(f'{app_name}.env', "STEAM_COMPAT_DATA_PATH", fallback=_prefix), "pfx") - _prefix = _config.get("default.env", "WINEPREFIX", fallback=_prefix) + _prefix = _config.get("default.env", "WINEPREFIX", fallback=fallback) _prefix = _config.get("default", "wine_prefix", fallback=_prefix) if app_name is not None: _prefix = _config.get(f'{app_name}.env', 'WINEPREFIX', fallback=_prefix) @@ -79,10 +93,47 @@ def get_wine_prefixes() -> Set[str]: _prefixes = [] for name, section in _config.items(): pfx = section.get("WINEPREFIX") or section.get("wine_prefix") - if not pfx: - pfx = os.path.join(compatdata, "pfx") if (compatdata := section.get("STEAM_COMPAT_DATA_PATH")) else "" if pfx: _prefixes.append(pfx) _prefixes = [os.path.expanduser(prefix) for prefix in _prefixes] return {p for p in _prefixes if os.path.isdir(p)} + +def get_proton_prefixes() -> Set[str]: + _prefixes = [] + for name, section in _config.items(): + pfx = os.path.join(compatdata, "pfx") if (compatdata := section.get("STEAM_COMPAT_DATA_PATH")) else "" + if pfx: + _prefixes.append(pfx) + _prefixes = [os.path.expanduser(prefix) for prefix in _prefixes] + return {p for p in _prefixes if os.path.isdir(p)} + + +def get_prefixes() -> Set[str]: + return get_wine_prefixes().union(get_proton_prefixes()) + + +def prefix_exists(pfx: str) -> bool: + return os.path.isdir(pfx) and os.path.isfile(os.path.join(pfx, "user.reg")) + + +def get_prefix(app_name: str = "default") -> Optional[str]: + _compat_path = _config.get(f"{app_name}.env", "STEAM_COMPAT_DATA_PATH", fallback=None) + if _compat_path and prefix_exists(_compat_prefix := os.path.join(_compat_path, "pfx")): + return _compat_prefix + + _wine_prefix = _config.get(f"{app_name}.env", "WINEPREFIX", fallback=None) + _wine_prefix = _config.get(app_name, "wine_prefix", fallback=_wine_prefix) + if _wine_prefix and prefix_exists(_wine_prefix): + return _wine_prefix + + _compat_path = _config.get(f"default.env", "STEAM_COMPAT_DATA_PATH", fallback=None) + if _compat_path and prefix_exists(_compat_prefix := os.path.join(_compat_path, "pfx")): + return _compat_prefix + + _wine_prefix = _config.get(f"default.env", "WINEPREFIX", fallback=None) + _wine_prefix = _config.get("default", "wine_prefix", fallback=_wine_prefix) + if _wine_prefix and prefix_exists(_wine_prefix): + return _wine_prefix + + return None From 3c01cfc0a8f417155d64ffea3d81bd03577c4113 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:54:09 +0200 Subject: [PATCH 19/29] EosOverlay: Fix a few remaining issues * Don't create path when preparing overlay download, it fails on updates. * Concatenate the overlay install path in InstallDialog instead of passing it as `base_path` * Respect RareGame state in in the EOS overlay form --- .../tabs/games/integrations/__init__.py | 4 +- .../tabs/games/integrations/eos_group.py | 60 ++++++++++++++----- rare/models/game.py | 12 ++-- rare/shared/workers/install_info.py | 3 - 4 files changed, 52 insertions(+), 27 deletions(-) diff --git a/rare/components/tabs/games/integrations/__init__.py b/rare/components/tabs/games/integrations/__init__.py index 0ecbc088..0e9eb6bb 100644 --- a/rare/components/tabs/games/integrations/__init__.py +++ b/rare/components/tabs/games/integrations/__init__.py @@ -1,7 +1,7 @@ from typing import Optional from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, QSpacerItem, QSizePolicy +from PyQt5.QtWidgets import QVBoxLayout, QWidget, QLabel, QSizePolicy from rare.widgets.side_tab import SideTabWidget from .egl_sync_group import EGLSyncGroup @@ -34,8 +34,8 @@ class IntegrationsTabs(SideTabWidget): self.tr(""), self, ) - self.ubisoft_group = UbisoftGroup(self.eos_ubisoft) self.eos_group = EosGroup(self.eos_ubisoft) + self.ubisoft_group = UbisoftGroup(self.eos_ubisoft) self.eos_ubisoft.addWidget(self.eos_group) self.eos_ubisoft.addWidget(self.ubisoft_group) self.eos_ubisoft_index = self.addTab(self.eos_ubisoft, self.tr("Epic Overlay and Ubisoft")) diff --git a/rare/components/tabs/games/integrations/eos_group.py b/rare/components/tabs/games/integrations/eos_group.py index 7cd8c163..2ed3bb0f 100644 --- a/rare/components/tabs/games/integrations/eos_group.py +++ b/rare/components/tabs/games/integrations/eos_group.py @@ -4,6 +4,7 @@ from logging import getLogger from typing import Optional from PyQt5.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool, Qt, pyqtSlot, QSize +from PyQt5.QtGui import QShowEvent from PyQt5.QtWidgets import ( QGroupBox, QMessageBox, @@ -20,7 +21,7 @@ from rare.lgndr.core import LegendaryCore from rare.models.game import RareEosOverlay from rare.shared import RareCore from rare.ui.components.tabs.games.integrations.eos_widget import Ui_EosWidget -from rare.utils import config_helper +from rare.utils import config_helper as config from rare.utils.misc import icon from rare.widgets.elide_label import ElideLabel @@ -51,7 +52,10 @@ class EosPrefixWidget(QFrame): self.indicator = QLabel(parent=self) self.indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred) - self.prefix_label = ElideLabel(prefix if prefix is not None else overlay.app_title, parent=self) + self.prefix_label = ElideLabel( + prefix.replace(os.path.expanduser("~"), "~") if prefix is not None else overlay.app_title, + parent=self, + ) self.overlay_label = ElideLabel(parent=self) self.overlay_label.setDisabled(True) @@ -93,7 +97,7 @@ class EosPrefixWidget(QFrame): def update_state(self) -> None: active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" - self.overlay_label.setText(f"{active_path}<\i>") + self.overlay_label.setText(f"{active_path}") self.path_select.clear() if not self.overlay.is_installed and not self.overlay.available_paths(self.prefix): @@ -103,8 +107,6 @@ class EosPrefixWidget(QFrame): self.button.setText(self.tr("Unavailable")) return - self.setDisabled(False) - if self.overlay.is_enabled(self.prefix): self.indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20))) else: @@ -120,6 +122,8 @@ class EosPrefixWidget(QFrame): self.path_select.setItemData(self.path_select.findData(path), path, Qt.ToolTipRole) self.path_select.setCurrentIndex(self.path_select.findData(active_path)) + self.setEnabled(self.overlay.state == RareEosOverlay.State.IDLE) + @pyqtSlot() def action(self) -> None: path = self.path_select.currentData(Qt.UserRole) @@ -128,10 +132,14 @@ class EosPrefixWidget(QFrame): if self.overlay.is_enabled(self.prefix) and (path == active_path): if not self.overlay.disable(prefix=self.prefix): QMessageBox.warning( - self, "Warning", + self, + "Warning", self.tr("Failed to completely disable the active EOS Overlay.{}").format( - self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.") - if active_path != install_path else "" + self.tr( + " Since the previous overlay was managed by EGL you can safely ignore this is." + ) + if active_path != install_path + else "" ), ) else: @@ -141,7 +149,9 @@ class EosPrefixWidget(QFrame): self, "Warning", self.tr("Failed to completely enable EOS overlay.{}").format( - self.tr(" Since the previous overlay was managed by EGL you can safely ignore this is.") + self.tr( + " Since the previous overlay was managed by EGL you can safely ignore this is." + ) if active_path != install_path else "" ), @@ -175,6 +185,7 @@ class EosGroup(QGroupBox): self.signals = self.rcore.signals() self.overlay = self.rcore.get_overlay() + self.overlay.signals.widget.update.connect(self.update_state) self.overlay.signals.game.installed.connect(self.install_finished) self.overlay.signals.game.uninstalled.connect(self.uninstall_finished) @@ -191,18 +202,29 @@ class EosGroup(QGroupBox): self.ui.update_button.setEnabled(False) self.threadpool = QThreadPool.globalInstance() + self.worker: Optional[CheckForUpdateWorker] = None - def showEvent(self, a0) -> None: + def showEvent(self, a0: QShowEvent) -> None: + if a0.spontaneous(): + return super().showEvent(a0) self.check_for_update() self.update_prefixes() + self.update_state() super().showEvent(a0) + @pyqtSlot() + def update_state(self): + self.ui.install_button.setEnabled(self.overlay.state == RareEosOverlay.State.IDLE) + self.ui.update_button.setEnabled(self.overlay.state == RareEosOverlay.State.IDLE and self.overlay.has_update) + self.ui.uninstall_button.setEnabled(self.overlay.state == RareEosOverlay.State.IDLE) + def update_prefixes(self): for widget in self.findChildren(EosPrefixWidget, options=Qt.FindDirectChildrenOnly): widget.deleteLater() if platform.system() != "Windows": - prefixes = config_helper.get_wine_prefixes() + prefixes = config.get_prefixes() + prefixes = {prefix for prefix in prefixes if config.prefix_exists(prefix)} if platform.system() == "Darwin": # TODO: add crossover support pass @@ -214,16 +236,22 @@ class EosGroup(QGroupBox): widget = EosPrefixWidget(self.overlay, None) self.ui.eos_layout.addWidget(widget) + @pyqtSlot(bool) + def check_for_update_finished(self, update_available: bool): + self.worker = None + self.ui.update_button.setEnabled(update_available) + def check_for_update(self): + self.ui.update_button.setEnabled(False) if not self.overlay.is_installed: return - def worker_finished(update_available): - self.ui.update_button.setEnabled(update_available) + if self.worker is not None: + return - worker = CheckForUpdateWorker(self.core) - worker.signals.update_available.connect(worker_finished) - QThreadPool.globalInstance().start(worker) + self.worker = CheckForUpdateWorker(self.core) + self.worker.signals.update_available.connect(self.check_for_update_finished) + QThreadPool.globalInstance().start(self.worker) @pyqtSlot() def install_finished(self): diff --git a/rare/models/game.py b/rare/models/game.py index 6c255a24..70f6300f 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -575,7 +575,9 @@ class RareEosOverlay(RareGameBase): @property def has_update(self) -> bool: - self.core.check_for_overlay_updates() + # lk: Don't check for updates here to ensure fast return + # There is already a thread in the EosGroup form to update it for us asynchronously + # and legendary does it too during login return self.core.overlay_update_available def is_enabled(self, prefix: Optional[str] = None) -> bool: @@ -644,13 +646,11 @@ class RareEosOverlay(RareGameBase): def install(self) -> bool: if not self.is_idle: return False - if self.is_installed: - base_path = self.igame.install_path - else: - base_path = self.core.get_default_install_dir() self.signals.game.install.emit( InstallOptionsModel( - app_name=self.app_name, base_path=base_path, platform="Windows", overlay=True + app_name=self.app_name, + base_path=self.core.get_default_install_dir(), + platform="Windows", update=self.is_installed, overlay=True ) ) return True diff --git a/rare/shared/workers/install_info.py b/rare/shared/workers/install_info.py index 1b5a921e..d4ff1877 100644 --- a/rare/shared/workers/install_info.py +++ b/rare/shared/workers/install_info.py @@ -41,9 +41,6 @@ class InstallInfoWorker(Worker): else: raise LgndrException(status.message) else: - if not os.path.exists(path := self.options.base_path): - os.makedirs(path) - dlm, analysis, igame = self.core.prepare_overlay_install( path=self.options.base_path ) From 7aa64b385ef290fc58cc1414d133082d13921219 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Wed, 20 Sep 2023 01:32:12 +0300 Subject: [PATCH 20/29] Downloads: Move auto-update change to Rare's settings instead of the game's metadata The setting doesn't have a switch in the GUI yet, but the settings feels like a better place for it. --- rare/components/tabs/downloads/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index 85df8598..c9932f13 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -105,7 +105,9 @@ class DownloadsTab(QWidget): 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): + if QSettings().value( + f"{update.app_name}/auto_update", False, bool + ) or QSettings().value("auto_update", False, bool): self.__get_install_options( InstallOptionsModel(app_name=update.app_name, update=True, silent=True) ) From 4e6008a8f7bc03d0d4f23a446a31f87252b04c10 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:17:31 +0200 Subject: [PATCH 21/29] TrayIcon: Generic notifications * Add a notification when starting a download too --- rare/components/tabs/downloads/__init__.py | 18 +++++++++++++----- rare/components/tabs/downloads/thread.py | 11 +++++------ rare/components/tray_icon.py | 13 +++---------- rare/models/signals.py | 4 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index c9932f13..c7bb2216 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -194,7 +194,7 @@ class DownloadsTab(QWidget): if item.expired: self.__refresh_download(item) return - dl_thread = DlThread(item, self.rcore.get_game(item.options.app_name), self.core, self.args.debug) + dl_thread = DlThread(item, rgame, self.core, self.args.debug) dl_thread.result.connect(self.__on_download_result) dl_thread.progress.connect(self.__on_download_progress) dl_thread.finished.connect(dl_thread.deleteLater) @@ -206,6 +206,11 @@ class DownloadsTab(QWidget): RareCore.instance().image_manager().get_pixmap(rgame.app_name, True) ) + self.signals.application.notify.emit( + self.tr("Downloads"), + self.tr("Starting: \"{}\" is now downloading.").format(rgame.app_title) + ) + @pyqtSlot(UIUpdate, object) def __on_download_progress(self, ui_update: UIUpdate, dl_size: int): self.download_widget.ui.progress_bar.setValue(int(ui_update.progress)) @@ -231,16 +236,19 @@ class DownloadsTab(QWidget): if result.shortcut and desktop_links_supported(): if not create_desktop_link( app_name=result.options.app_name, - app_title=result.shortcut_title, - link_name=result.shortcut_name, + app_title=result.app_title, + link_name=result.folder_name, link_type="desktop", ): # maybe add it to download summary, to show in finished downloads logger.error(f"Failed to create desktop link on {platform.system()}") else: - logger.info(f"Created desktop link {result.shortcut_name} for {result.options.app_name}") + logger.info(f"Created desktop link {result.folder_name} for {result.app_title}") - self.signals.application.notify.emit(result.options.app_name) + self.signals.application.notify.emit( + self.tr("Downloads"), + self.tr("Finished: \"{}\" is now playable.").format(result.app_title), + ) if self.updates_group.contains(result.options.app_name): self.updates_group.set_widget_enabled(result.options.app_name, True) diff --git a/rare/components/tabs/downloads/thread.py b/rare/components/tabs/downloads/thread.py index 9309ef25..7aaa7486 100644 --- a/rare/components/tabs/downloads/thread.py +++ b/rare/components/tabs/downloads/thread.py @@ -34,8 +34,8 @@ class DlResultModel: sync_saves: bool = False tip_url: str = "" shortcut: bool = False - shortcut_name: str = "" - shortcut_title: str = "" + folder_name: str = "" + app_title: str = "" class DlThread(QThread): @@ -151,10 +151,9 @@ class DlThread(QThread): self.item.download.repair_file, ) - if not self.item.options.update and self.item.options.create_shortcut: - result.shortcut = True - result.shortcut_name = self.rgame.folder_name - result.shortcut_title = self.rgame.app_title + result.shortcut = not self.item.options.update and self.item.options.create_shortcut + result.folder_name = self.rgame.folder_name + result.app_title = self.rgame.app_title self.__finish(result) diff --git a/rare/components/tray_icon.py b/rare/components/tray_icon.py index a0c49aa9..8b40cca1 100644 --- a/rare/components/tray_icon.py +++ b/rare/components/tray_icon.py @@ -59,17 +59,10 @@ class TrayIcon(QSystemTrayIcon): last_played.sort(key=lambda g: g.metadata.last_played, reverse=True) return last_played[0:5] - @pyqtSlot(str) - def notify(self, app_name: str): + @pyqtSlot(str, str) + def notify(self, title: str, body: str): if self.settings.value("notification", True, bool): - self.showMessage( - self.tr("Download finished"), - self.tr("Download finished. {} is playable now").format( - self.rcore.get_game(app_name).app_title - ), - self.Information, - 4000, - ) + self.showMessage(f"{QApplication.applicationName()} - {title}", body, QSystemTrayIcon.Information, 4000) @pyqtSlot() def update_actions(self): diff --git a/rare/models/signals.py b/rare/models/signals.py index 3c614237..522ad7f9 100644 --- a/rare/models/signals.py +++ b/rare/models/signals.py @@ -11,8 +11,8 @@ class GlobalSignals: class ApplicationSignals(QObject): # int: exit code quit = pyqtSignal(int) - # str: app_title - notify = pyqtSignal(str) + # str: title, str: body + notify = pyqtSignal(str, str) # none prefix_updated = pyqtSignal() # none From 06e2c9b714cf2c53c407237b29256bb76299d438 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:17:48 +0200 Subject: [PATCH 22/29] DebugSettings: Add button to test notifications --- rare/components/tabs/settings/debug.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rare/components/tabs/settings/debug.py b/rare/components/tabs/settings/debug.py index 8c6b04b2..14aafd12 100644 --- a/rare/components/tabs/settings/debug.py +++ b/rare/components/tabs/settings/debug.py @@ -8,17 +8,23 @@ class DebugSettings(QWidget): def __init__(self, parent=None): super(DebugSettings, self).__init__(parent=parent) - self.raise_runtime_exception_button = QPushButton("Raise Exception") + self.raise_runtime_exception_button = QPushButton("Raise Exception", self) self.raise_runtime_exception_button.clicked.connect(self.raise_exception) - self.restart_button = QPushButton("Restart") + self.restart_button = QPushButton("Restart", self) self.restart_button.clicked.connect( lambda: GlobalSignalsSingleton().application.quit.emit(ExitCodes.LOGOUT) ) + self.send_notification_button = QPushButton("Notify", self) + self.send_notification_button.clicked.connect(self.send_notification) layout = QVBoxLayout(self) layout.addWidget(self.raise_runtime_exception_button) layout.addWidget(self.restart_button) + layout.addWidget(self.send_notification_button) layout.addStretch(1) def raise_exception(self): raise RuntimeError("Debug Crash") + + def send_notification(self): + GlobalSignalsSingleton().application.notify.emit("Debug", "Test notification") From ce0b9788ee132737ae8228cb44175a483f855e62 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:42:10 +0200 Subject: [PATCH 23/29] RareGame: Clear pixmap cache before loading a new pixmap --- rare/models/game.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rare/models/game.py b/rare/models/game.py index 70f6300f..cfc295e6 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -8,7 +8,7 @@ from threading import Lock from typing import List, Optional, Dict, Set from PyQt5.QtCore import QRunnable, pyqtSlot, QProcess, QThreadPool -from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QPixmap, QPixmapCache from legendary.lfs import eos from legendary.models.game import Game, InstalledGame @@ -481,6 +481,7 @@ class RareGame(RareGameSlim): def set_pixmap(self): self.pixmap = self.image_manager.get_pixmap(self.app_name, self.is_installed) + QPixmapCache.clear() if not self.pixmap.isNull(): self.signals.widget.update.emit() From 7b52dc204ceb074d038ed3d4ed16d29f049abcb8 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:23:20 +0200 Subject: [PATCH 24/29] Downloads: Update queue count when adding a download --- rare/components/tabs/downloads/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rare/components/tabs/downloads/__init__.py b/rare/components/tabs/downloads/__init__.py index c7bb2216..0145066f 100644 --- a/rare/components/tabs/downloads/__init__.py +++ b/rare/components/tabs/downloads/__init__.py @@ -326,6 +326,7 @@ class DownloadsTab(QWidget): if self.updates_group.contains(item.options.app_name): self.updates_group.set_widget_enabled(item.options.app_name, True) rgame.state = RareGame.State.IDLE + self.update_queues_count() @pyqtSlot(UninstallOptionsModel) def __get_uninstall_options(self, options: UninstallOptionsModel): From 5b367270766b93bff2fd38ed90246eec71f23671 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:45:44 +0200 Subject: [PATCH 25/29] Dialogs: Add subtitle label in ButtonDialog --- rare/components/dialogs/install_dialog.py | 3 +- rare/components/dialogs/move_dialog.py | 4 +- rare/components/dialogs/selective_dialog.py | 10 +---- rare/components/dialogs/uninstall_dialog.py | 5 +-- rare/ui/components/dialogs/install_dialog.py | 36 ++++++++---------- rare/ui/components/dialogs/install_dialog.ui | 39 ++++++++------------ rare/widgets/dialogs.py | 20 ++++++++-- 7 files changed, 54 insertions(+), 63 deletions(-) diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index a8f23c25..cbe99018 100644 --- a/rare/components/dialogs/install_dialog.py +++ b/rare/components/dialogs/install_dialog.py @@ -75,13 +75,12 @@ class InstallDialog(ActionDialog): header = self.tr("Modify") bicon = icon("fa.gear") self.setWindowTitle(dialog_title_game(header, rgame.app_title)) + self.setSubtitle(dialog_title_game(header, rgame.app_title)) install_widget = QWidget(self) self.ui = Ui_InstallDialog() self.ui.setupUi(install_widget) - self.ui.title_label.setText(f"

{dialog_title_game(header, rgame.app_title)}

") - self.core = rgame.core self.rgame = rgame self.__options: InstallOptionsModel = options diff --git a/rare/components/dialogs/move_dialog.py b/rare/components/dialogs/move_dialog.py index cab68049..b81af267 100644 --- a/rare/components/dialogs/move_dialog.py +++ b/rare/components/dialogs/move_dialog.py @@ -34,8 +34,7 @@ class MoveDialog(ActionDialog): super(MoveDialog, self).__init__(parent=parent) header = self.tr("Move") self.setWindowTitle(dialog_title_game(header, rgame.app_title)) - - title_label = QLabel(f"

{dialog_title_game(header, rgame.app_title)}

", self) + self.setSubtitle(dialog_title_game(header, rgame.app_title)) self.rcore = RareCore.instance() self.core = RareCore.instance().core() @@ -70,7 +69,6 @@ class MoveDialog(ActionDialog): layout = QVBoxLayout() layout.setSizeConstraint(QLayout.SetFixedSize) - layout.addWidget(title_label) layout.addWidget(self.path_edit) layout.addWidget(self.warn_label) layout.addLayout(bottom_layout) diff --git a/rare/components/dialogs/selective_dialog.py b/rare/components/dialogs/selective_dialog.py index 1c3b5031..ac7409e2 100644 --- a/rare/components/dialogs/selective_dialog.py +++ b/rare/components/dialogs/selective_dialog.py @@ -15,8 +15,7 @@ class SelectiveDialog(ButtonDialog): super(SelectiveDialog, self).__init__(parent=parent) header = self.tr("Optional downloads for") self.setWindowTitle(dialog_title_game(header, rgame.app_title)) - - title_label = QLabel(f"

{dialog_title_game(header, rgame.app_title)}

", self) + self.setSubtitle(dialog_title_game(header, rgame.app_title)) self.rgame = rgame self.selective_widget = SelectiveWidget(rgame, rgame.igame.platform, self) @@ -26,12 +25,7 @@ class SelectiveDialog(ButtonDialog): container_layout.setContentsMargins(0, 0, 0, 0) container_layout.addWidget(self.selective_widget) - layout = QVBoxLayout() - layout.setSizeConstraint(QLayout.SetFixedSize) - layout.addWidget(title_label) - layout.addWidget(container) - - self.setCentralLayout(layout) + self.setCentralWidget(container) self.accept_button.setText(self.tr("Verify")) self.accept_button.setIcon(icon("fa.check")) diff --git a/rare/components/dialogs/uninstall_dialog.py b/rare/components/dialogs/uninstall_dialog.py index dd6be379..07befe14 100644 --- a/rare/components/dialogs/uninstall_dialog.py +++ b/rare/components/dialogs/uninstall_dialog.py @@ -1,6 +1,5 @@ from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import ( - QLabel, QVBoxLayout, QCheckBox, ) @@ -18,8 +17,7 @@ class UninstallDialog(ButtonDialog): super(UninstallDialog, self).__init__(parent=parent) header = self.tr("Uninstall") self.setWindowTitle(dialog_title_game(header, rgame.app_title)) - - title_label = QLabel(f"

{dialog_title_game(header, rgame.app_title)}

", self) + self.setSubtitle(dialog_title_game(header, rgame.app_title)) self.keep_files = QCheckBox(self.tr("Keep files")) self.keep_files.setChecked(bool(options.keep_files)) @@ -34,7 +32,6 @@ class UninstallDialog(ButtonDialog): self.keep_overlay_keys.setEnabled(rgame.is_overlay) layout = QVBoxLayout() - layout.addWidget(title_label) layout.addWidget(self.keep_files) layout.addWidget(self.keep_config) layout.addWidget(self.keep_overlay_keys) diff --git a/rare/ui/components/dialogs/install_dialog.py b/rare/ui/components/dialogs/install_dialog.py index 8dc5c664..9e680570 100644 --- a/rare/ui/components/dialogs/install_dialog.py +++ b/rare/ui/components/dialogs/install_dialog.py @@ -14,20 +14,17 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_InstallDialog(object): def setupUi(self, InstallDialog): InstallDialog.setObjectName("InstallDialog") - InstallDialog.resize(197, 216) + InstallDialog.resize(197, 195) InstallDialog.setWindowTitle("InstallDialog") self.main_layout = QtWidgets.QFormLayout(InstallDialog) self.main_layout.setLabelAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.main_layout.setObjectName("main_layout") - self.title_label = QtWidgets.QLabel(InstallDialog) - self.title_label.setObjectName("title_label") - self.main_layout.setWidget(0, QtWidgets.QFormLayout.SpanningRole, self.title_label) self.install_dir_label = QtWidgets.QLabel(InstallDialog) self.install_dir_label.setObjectName("install_dir_label") - self.main_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.install_dir_label) + self.main_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.install_dir_label) self.platform_label = QtWidgets.QLabel(InstallDialog) self.platform_label.setObjectName("platform_label") - self.main_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.platform_label) + self.main_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.platform_label) self.platform_combo = QtWidgets.QComboBox(InstallDialog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -35,43 +32,43 @@ class Ui_InstallDialog(object): sizePolicy.setHeightForWidth(self.platform_combo.sizePolicy().hasHeightForWidth()) self.platform_combo.setSizePolicy(sizePolicy) self.platform_combo.setObjectName("platform_combo") - self.main_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.platform_combo) + self.main_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.platform_combo) self.shortcut_label = QtWidgets.QLabel(InstallDialog) self.shortcut_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) self.shortcut_label.setObjectName("shortcut_label") - self.main_layout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.shortcut_label) + self.main_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.shortcut_label) self.shortcut_check = QtWidgets.QCheckBox(InstallDialog) self.shortcut_check.setText("") self.shortcut_check.setObjectName("shortcut_check") - self.main_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.shortcut_check) + self.main_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.shortcut_check) self.selectable_layout = QtWidgets.QVBoxLayout() self.selectable_layout.setObjectName("selectable_layout") - self.main_layout.setLayout(4, QtWidgets.QFormLayout.SpanningRole, self.selectable_layout) + self.main_layout.setLayout(3, QtWidgets.QFormLayout.SpanningRole, self.selectable_layout) self.advanced_layout = QtWidgets.QVBoxLayout() self.advanced_layout.setObjectName("advanced_layout") - self.main_layout.setLayout(5, QtWidgets.QFormLayout.SpanningRole, self.advanced_layout) + self.main_layout.setLayout(4, QtWidgets.QFormLayout.SpanningRole, self.advanced_layout) self.download_size_label = QtWidgets.QLabel(InstallDialog) self.download_size_label.setObjectName("download_size_label") - self.main_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.download_size_label) + self.main_layout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.download_size_label) self.download_size_text = QtWidgets.QLabel(InstallDialog) font = QtGui.QFont() font.setItalic(True) self.download_size_text.setFont(font) self.download_size_text.setObjectName("download_size_text") - self.main_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.download_size_text) + self.main_layout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.download_size_text) self.install_size_label = QtWidgets.QLabel(InstallDialog) self.install_size_label.setObjectName("install_size_label") - self.main_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.install_size_label) + self.main_layout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.install_size_label) self.install_size_text = QtWidgets.QLabel(InstallDialog) font = QtGui.QFont() font.setItalic(True) self.install_size_text.setFont(font) self.install_size_text.setWordWrap(True) self.install_size_text.setObjectName("install_size_text") - self.main_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.install_size_text) + self.main_layout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.install_size_text) self.avail_space_label = QtWidgets.QLabel(InstallDialog) self.avail_space_label.setObjectName("avail_space_label") - self.main_layout.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.avail_space_label) + self.main_layout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.avail_space_label) self.avail_space = QtWidgets.QLabel(InstallDialog) font = QtGui.QFont() font.setBold(True) @@ -79,10 +76,10 @@ class Ui_InstallDialog(object): self.avail_space.setFont(font) self.avail_space.setText("") self.avail_space.setObjectName("avail_space") - self.main_layout.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.avail_space) + self.main_layout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.avail_space) self.warning_label = QtWidgets.QLabel(InstallDialog) self.warning_label.setObjectName("warning_label") - self.main_layout.setWidget(9, QtWidgets.QFormLayout.LabelRole, self.warning_label) + self.main_layout.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.warning_label) self.warning_text = QtWidgets.QLabel(InstallDialog) font = QtGui.QFont() font.setItalic(True) @@ -92,13 +89,12 @@ class Ui_InstallDialog(object): self.warning_text.setWordWrap(True) self.warning_text.setTextInteractionFlags(QtCore.Qt.LinksAccessibleByMouse|QtCore.Qt.TextSelectableByMouse) self.warning_text.setObjectName("warning_text") - self.main_layout.setWidget(9, QtWidgets.QFormLayout.FieldRole, self.warning_text) + self.main_layout.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.warning_text) self.retranslateUi(InstallDialog) def retranslateUi(self, InstallDialog): _translate = QtCore.QCoreApplication.translate - self.title_label.setText(_translate("InstallDialog", "error")) self.install_dir_label.setText(_translate("InstallDialog", "Install folder")) self.platform_label.setText(_translate("InstallDialog", "Platform")) self.shortcut_label.setText(_translate("InstallDialog", "Create shortcut")) diff --git a/rare/ui/components/dialogs/install_dialog.ui b/rare/ui/components/dialogs/install_dialog.ui index d16cf4d4..6b00a183 100644 --- a/rare/ui/components/dialogs/install_dialog.ui +++ b/rare/ui/components/dialogs/install_dialog.ui @@ -7,7 +7,7 @@ 0 0 197 - 216 + 195
@@ -17,28 +17,21 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - error - - - - + Install folder - + Platform - + @@ -48,7 +41,7 @@ - + Create shortcut @@ -58,27 +51,27 @@ - + - + - + - + Download size - + @@ -90,14 +83,14 @@ - + Total install size - + @@ -112,14 +105,14 @@ - + Available space - + @@ -132,14 +125,14 @@ - + Warning - + diff --git a/rare/widgets/dialogs.py b/rare/widgets/dialogs.py index a5a97db8..2c6839b4 100644 --- a/rare/widgets/dialogs.py +++ b/rare/widgets/dialogs.py @@ -11,7 +11,7 @@ from PyQt5.QtWidgets import ( QVBoxLayout, QHBoxLayout, QWidget, - QLayout, QSpacerItem, QSizePolicy, + QLayout, QSpacerItem, QSizePolicy, QLabel, ) from rare.utils.misc import icon @@ -74,6 +74,9 @@ class ButtonDialog(BaseDialog): def __init__(self, parent=None): super(ButtonDialog, self).__init__(parent=parent) + self.subtitle_label = QLabel(self) + self.subtitle_label.setVisible(False) + self.reject_button = QPushButton(self) self.reject_button.setText(self.tr("Cancel")) self.reject_button.setIcon(icon("fa.remove")) @@ -91,6 +94,7 @@ class ButtonDialog(BaseDialog): self.button_layout.addWidget(self.accept_button) self.main_layout = QVBoxLayout(self) + self.main_layout.addWidget(self.subtitle_label) # lk: dirty way to set a minimum width with fixed size constraint spacer = QSpacerItem( 480, self.main_layout.spacing(), @@ -103,13 +107,23 @@ class ButtonDialog(BaseDialog): def close(self): raise RuntimeError(f"Don't use `close()` with {type(self).__name__}") + def setSubtitle(self, text: str): + self.subtitle_label.setText(f"{text}") + self.subtitle_label.setVisible(True) + def setCentralWidget(self, widget: QWidget): widget.layout().setContentsMargins(0, 0, 0, 0) - self.main_layout.insertWidget(0, widget) + self.main_layout.insertWidget( + self.main_layout.indexOf(self.subtitle_label) + 1, + widget + ) def setCentralLayout(self, layout: QLayout): layout.setContentsMargins(0, 0, 0, 0) - self.main_layout.insertLayout(0, layout) + self.main_layout.insertLayout( + self.main_layout.indexOf(self.subtitle_label) + 1, + layout + ) @abstractmethod def accept_handler(self): From fa9b49c0194afa575e6c07345099214cb3f9ac86 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:57:28 +0200 Subject: [PATCH 26/29] SelectiveDialog: Keep the layout of the central widget --- rare/components/dialogs/selective_dialog.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rare/components/dialogs/selective_dialog.py b/rare/components/dialogs/selective_dialog.py index ac7409e2..26b50968 100644 --- a/rare/components/dialogs/selective_dialog.py +++ b/rare/components/dialogs/selective_dialog.py @@ -25,7 +25,10 @@ class SelectiveDialog(ButtonDialog): container_layout.setContentsMargins(0, 0, 0, 0) container_layout.addWidget(self.selective_widget) - self.setCentralWidget(container) + layout = QVBoxLayout() + layout.addWidget(container) + + self.setCentralLayout(layout) self.accept_button.setText(self.tr("Verify")) self.accept_button.setIcon(icon("fa.check")) From 69aca3851fb5e2040dd91a4b2ac96a4bbdc73e2a Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Sun, 21 Jan 2024 23:42:38 +0200 Subject: [PATCH 27/29] InstallDialog: Update widget attribute name --- rare/components/dialogs/install_dialog.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rare/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index cbe99018..6fafcae7 100644 --- a/rare/components/dialogs/install_dialog.py +++ b/rare/components/dialogs/install_dialog.py @@ -27,10 +27,10 @@ class InstallDialogAdvanced(CollapsibleFrame): title = self.tr("Advanced options") self.setTitle(title) - widget = QWidget(parent=self) + self.widget = QWidget(parent=self) self.ui = Ui_InstallDialogAdvanced() - self.ui.setupUi(widget) - self.setWidget(widget) + self.ui.setupUi(self.widget) + self.setWidget(self.widget) class InstallDialogSelective(CollapsibleFrame): @@ -42,18 +42,18 @@ class InstallDialogSelective(CollapsibleFrame): self.setTitle(title) self.setEnabled(bool(rgame.sdl_name)) - self.selective_widget: SelectiveWidget = None + self.widget: SelectiveWidget = None self.rgame = rgame def update_list(self, platform: str): - if self.selective_widget is not None: - self.selective_widget.deleteLater() - self.selective_widget = SelectiveWidget(self.rgame, platform, parent=self) - self.selective_widget.stateChanged.connect(self.stateChanged) - self.setWidget(self.selective_widget) + if self.widget is not None: + self.widget.deleteLater() + self.widget = SelectiveWidget(self.rgame, platform, parent=self) + self.widget.stateChanged.connect(self.stateChanged) + self.setWidget(self.widget) def install_tags(self): - return self.selective_widget.install_tags() + return self.widget.install_tags() class InstallDialog(ActionDialog): From d9bfdb91ce26892699b1d409dbdac6f0995810ec Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:16:40 +0200 Subject: [PATCH 28/29] Runners: Import vdf only on Linux and FreeBSD --- rare/utils/proton.py | 4 +++- requirements.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rare/utils/proton.py b/rare/utils/proton.py index c0e3fb60..c54e6d42 100644 --- a/rare/utils/proton.py +++ b/rare/utils/proton.py @@ -1,9 +1,11 @@ +import platform as pf import os from dataclasses import dataclass from logging import getLogger from typing import Optional, Union, List, Dict -import vdf +if pf.system() in {"Linux", "FreeBSD"}: + import vdf logger = getLogger("Proton") diff --git a/requirements.txt b/requirements.txt index 6d4303ef..4a9abb8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ setuptools legendary-gl>=0.20.34; platform_system != "Windows" or platform_system != "Darwin" legendary-gl @ git+https://github.com/derrod/legendary@96e07ff ; platform_system == "Windows" or platform_system == "Darwin" orjson -vdf; platform_system != "Windows" +vdf; platform_system == "Linux" or platform_system == "FreeBSD" pywin32; platform_system == "Windows" From 17246f1201aea5df79455aa18e35413cd4ca3523 Mon Sep 17 00:00:00 2001 From: loathingKernel <142770+loathingKernel@users.noreply.github.com> Date: Mon, 22 Jan 2024 00:21:40 +0200 Subject: [PATCH 29/29] Workflows: Run pylint on a matrix of OSes and versions --- .github/workflows/checks.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 3fa9536f..763e7be9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -22,13 +22,17 @@ on: jobs: pylint: - runs-on: ubuntu-latest + strategy: + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] + version: [3.9, 3.10, 3.11, 3.12] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.9' + python-version: ${{ matris.version }} - name: Install dependencies run: | python3 -m pip install --upgrade pip