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 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/components/dialogs/install_dialog.py b/rare/components/dialogs/install_dialog.py index 49c5f10a..6fafcae7 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) + + self.widget = QWidget(parent=self) self.ui = Ui_InstallDialogAdvanced() - self.ui.setupUi(widget) - super(InstallDialogAdvanced, self).__init__(widget=widget, title=title, parent=parent) + self.ui.setupUi(self.widget) + self.setWidget(self.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.widget: SelectiveWidget = None + self.rgame = rgame + + def update_list(self, platform: str): + 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.widget.install_tags() class InstallDialog(ActionDialog): @@ -48,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 @@ -64,7 +90,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 +109,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 +130,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 +159,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 +205,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 +222,31 @@ 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() + 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() 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/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 864eb657..26b50968 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): @@ -22,23 +15,18 @@ class SelectiveDialog(ButtonDialog): super(SelectiveDialog, self).__init__(parent=parent) header = self.tr("Optional downloads for") self.setWindowTitle(dialog_title_game(header, rgame.app_title)) + self.setSubtitle(dialog_title_game(header, rgame.app_title)) - 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 +35,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/components/dialogs/uninstall_dialog.py b/rare/components/dialogs/uninstall_dialog.py index c6f24cae..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,19 +17,24 @@ 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)) + 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 +55,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..0145066f 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) ) @@ -192,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) @@ -204,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)) @@ -229,19 +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}") - 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( + 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) @@ -319,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): 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/tabs/games/integrations/__init__.py b/rare/components/tabs/games/integrations/__init__.py index 0a3bf0fb..0e9eb6bb 100644 --- a/rare/components/tabs/games/integrations/__init__.py +++ b/rare/components/tabs/games/integrations/__init__.py @@ -1,11 +1,11 @@ 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 -from .eos_group import EOSGroup +from .eos_group import EosGroup from .import_group import ImportGroup from .ubisoft_group import UbisoftGroup @@ -34,8 +34,8 @@ class IntegrationsTabs(SideTabWidget): self.tr(""), self, ) + self.eos_group = EosGroup(self.eos_ubisoft) self.ubisoft_group = UbisoftGroup(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..2ed3bb0f 100644 --- a/rare/components/tabs/games/integrations/eos_group.py +++ b/rare/components/tabs/games/integrations/eos_group.py @@ -1,259 +1,280 @@ 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 legendary.lfs import eos +from PyQt5.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool, Qt, pyqtSlot, QSize +from PyQt5.QtGui import QShowEvent +from PyQt5.QtWidgets import ( + QGroupBox, + QMessageBox, + QFrame, + QHBoxLayout, + QSizePolicy, + QLabel, + QPushButton, + QFormLayout, + QComboBox, +) -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 as config 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.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) + + self.path_select = QComboBox(self) + self.path_select.setMaximumWidth(150) + self.path_select.setMinimumWidth(150) + + self.button = QPushButton(parent=self) + self.button.setMinimumWidth(150) + + layout = QHBoxLayout(self) + layout.setContentsMargins(-1, 0, 0, 0) + layout.addWidget(self.indicator) + 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) -> None: + active_path = os.path.normpath(p) if (p := self.overlay.active_path(self.prefix)) else "" + + 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): + self.setDisabled(True) + 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 + + if self.overlay.is_enabled(self.prefix): + self.indicator.setPixmap(icon("fa.check-circle-o", color="green").pixmap(QSize(20, 20))) + else: + 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)) + + self.setEnabled(self.overlay.state == RareEosOverlay.State.IDLE) + + @pyqtSlot() + 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.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() + + +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.widget.update.connect(self.update_state) + 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(os.path.normpath(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() + self.worker: Optional[CheckForUpdateWorker] = 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): - 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 platform.system() != "Windows": + prefixes = config.get_prefixes() + prefixes = {prefix for prefix in prefixes if config.prefix_exists(prefix)} + 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) - 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("~/"), "~/"))) + @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): - 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")) - - self.ui.update_check_button.setDisabled(True) - worker = CheckForUpdateWorker() - 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")) + self.ui.update_button.setEnabled(False) + if not self.overlay.is_installed: return - 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")): + if self.worker is not None: 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) + self.worker = CheckForUpdateWorker(self.core) + self.worker.signals.update_available.connect(self.check_for_update_finished) + QThreadPool.globalInstance().start(self.worker) - 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: - 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 + @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) - 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 + @pyqtSlot() + def uninstall_finished(self): + self.ui.overlay_stack.setCurrentWidget(self.ui.install_page) - 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}') - - 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 Legendary-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/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") 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/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/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() 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() diff --git a/rare/models/base_game.py b/rare/models/base_game.py index 0cd1611c..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: """! @@ -268,7 +273,7 @@ class RareGameSlim(RareGameBase): return if thread: - worker = QRunnable.create(lambda: _upload()) + worker = QRunnable.create(_upload) QThreadPool.globalInstance().start(worker) else: _upload() @@ -293,7 +298,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 ea701015..cfc295e6 100644 --- a/rare/models/game.py +++ b/rare/models/game.py @@ -8,13 +8,13 @@ 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 -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 @@ -412,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 @@ -433,9 +429,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" @@ -446,9 +445,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( @@ -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() @@ -572,3 +573,93 @@ class RareEosOverlay(RareGameBase): else: self.igame = None self.signals.game.uninstalled.emit(self.app_name) + + @property + def has_update(self) -> bool: + # 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: + 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: + 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]: + 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 + ) -> bool: + if self.is_enabled(prefix): + return False + if not 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: + 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) + 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) -> bool: + if not self.is_enabled(prefix): + return False + logger.info(f"Disabling overlay (removing registry keys) for prefix: {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: + if not self.is_idle: + return False + self.signals.game.install.emit( + InstallOptionsModel( + app_name=self.app_name, + base_path=self.core.get_default_install_dir(), + platform="Windows", update=self.is_installed, 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 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) 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..522ad7f9 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) - # str: app_title - notify = pyqtSignal(str) + quit = pyqtSignal(int) + # str: title, str: body + notify = pyqtSignal(str, 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/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) 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) diff --git a/rare/shared/rare_core.py b/rare/shared/rare_core.py index 6000a3b9..9afcaac2 100644 --- a/rare/shared/rare_core.py +++ b/rare/shared/rare_core.py @@ -9,18 +9,22 @@ 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 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 @@ -50,7 +54,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) @@ -66,6 +70,12 @@ 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) + + self.__fetch_progress: int = 0 + self.__fetched_games_dlcs: bool = False + self.__fetched_entitlements: bool = False RareCore.__instance = self @@ -224,12 +234,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 +264,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) @@ -297,31 +310,52 @@ 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.time() - 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.time() - 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) + self.__start_time = time.perf_counter() + + 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: - 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] @@ -332,31 +366,14 @@ 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() - 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}") - return - logger.debug(f"Entitlements: {len(list(entitlements))}") - logger.debug(f"Request Entitlements: {time.time() - start_time} seconds") - - 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) @@ -364,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 b18ea83f..1d4fe761 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") @@ -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,15 +30,39 @@ class FetchWorker(Worker): self.args = args self.settings = QSettings() - def run_real(self): - # Fetch regular EGL games with assets - start_time = time.time() +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) 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 +71,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 +83,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 +94,39 @@ 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)) # 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") + 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(f"Network error while fetching non asset games") + logger.error(e) + na_games, na_dlc_dict = ([], {}) + # 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) + 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)}") - 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) 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 ) diff --git a/rare/shared/workers/uninstall.py b/rare/shared/workers/uninstall.py index 76207fe7..98fe21b7 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() + + 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/dialogs/install_dialog.py b/rare/ui/components/dialogs/install_dialog.py index 10c5d871..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(179, 204) + 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 e47e3eaf..6b00a183 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 + 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/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 - - - - - - - - - - diff --git a/rare/ui/components/tabs/settings/legendary.py b/rare/ui/components/tabs/settings/legendary.py index 829a60ab..8bdcc871 100644 --- a/rare/ui/components/tabs/settings/legendary.py +++ b/rare/ui/components/tabs/settings/legendary.py @@ -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,26 +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.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.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.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) @@ -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..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 @@ -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 + + + 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 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/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) 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/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): diff --git a/rare/widgets/selective_widget.py b/rare/widgets/selective_widget.py new file mode 100644 index 00000000..527697cc --- /dev/null +++ b/rare/widgets/selective_widget.py @@ -0,0 +1,58 @@ +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) + self.parent().setDisabled(False) + else: + self.parent().setDisabled(True) + + 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..4a9abb8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -requests +requests<3.0 PyQt5 QtAwesome 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"