Implement LgndrIndirectLogger
to return the last logged message from LegendaryAPI
The indirect return is stored in a `LgndrIndirectStatus` object that provides checking and unpacking features Lgndr: `LgndrInstallGameArgs.install_tag` default to None Lgndr: add default `move` method. Lgndr: monkeypatch `dlm.status_queue` after preparing a download to reduce `InstallQueueItemModel` InstallOptionsModel: implement `as_install_kwargs` to pass only relevant arguments to `LgndrInstallGameArgs` in InstallDialog InstallOptionsModel: rename `sdl_list` to `install_tag` as they were the same thing LegendaryUtils: Update to use `LgndrIndirectStatus` UninstallDialog: Add option to keep configuration decoupled from keeping game data GameUtils: Add messagebox to show error messages from legendary after uninstalling a game InstallDialog: Update to use `LgndrIndirectStatus` InstallDialog: Update selectable download handling to match legendary's DownloadThread: Remove multiple instance variables, instead reference them directly from `InstallQueueItemModel` instance ImportGroup: Replace `info_label` with an `ElideLabel` for displaying long messages ImportGroup: Don't translate message strings in the `ImportWorker` GameInfo: Call `repair()` if needed after verification instead of handling it locally GamesTab: Fix string matching for capitalized strings and scroll to top on when searching
This commit is contained in:
parent
28531eec38
commit
ee5adce18b
|
@ -1,20 +1,19 @@
|
|||
import os
|
||||
import platform
|
||||
import platform as pf
|
||||
import sys
|
||||
from multiprocessing import Queue as MPQueue
|
||||
from typing import Tuple
|
||||
from typing import Tuple, List, Union, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, QObject, QRunnable, QThreadPool, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtGui import QCloseEvent, QKeyEvent
|
||||
from PyQt5.QtWidgets import QDialog, QFileDialog, QCheckBox
|
||||
from legendary.models.downloading import ConditionCheckResult
|
||||
from legendary.models.game import Game
|
||||
from legendary.utils.selective_dl import get_sdl_appname
|
||||
|
||||
from rare.lgndr.api_arguments import LgndrInstallGameArgs
|
||||
from rare.lgndr.api_exception import LgndrException
|
||||
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||
from rare.lgndr.core import LegendaryCore
|
||||
from legendary.models.downloading import ConditionCheckResult
|
||||
from legendary.models.game import Game
|
||||
from legendary.utils.selective_dl import games
|
||||
|
||||
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, ApiResultsSingleton, ArgumentsSingleton
|
||||
from rare.ui.components.dialogs.install_dialog import Ui_InstallDialog
|
||||
from rare.utils.extra_widgets import PathEdit
|
||||
|
@ -34,7 +33,6 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.core = LegendaryCoreSingleton()
|
||||
self.api_results = ApiResultsSingleton()
|
||||
self.dl_item = dl_item
|
||||
self.dl_item.status_q = MPQueue()
|
||||
self.app_name = self.dl_item.options.app_name
|
||||
self.game = (
|
||||
self.core.get_game(self.app_name)
|
||||
|
@ -87,18 +85,20 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
platforms.append("Mac")
|
||||
self.platform_combo_box.addItems(platforms)
|
||||
self.platform_combo_box.currentIndexChanged.connect(lambda: self.option_changed(None))
|
||||
self.platform_combo_box.currentIndexChanged.connect(lambda: self.error_box())
|
||||
self.platform_combo_box.currentIndexChanged.connect(
|
||||
lambda i: self.error_box(
|
||||
self.tr("Warning"),
|
||||
self.tr("You will not be able to run the Game if you choose {}").format(
|
||||
self.tr("You will not be able to run the game if you select <b>{}</b> as platform").format(
|
||||
self.platform_combo_box.itemText(i)
|
||||
),
|
||||
)
|
||||
if (self.platform_combo_box.currentText() == "Mac" and platform.system() != "Darwin")
|
||||
if (self.platform_combo_box.currentText() == "Mac" and pf.system() != "Darwin")
|
||||
else None
|
||||
)
|
||||
self.platform_combo_box.currentTextChanged.connect(self.setup_sdl_list)
|
||||
|
||||
if platform.system() == "Darwin" and "Mac" in platforms:
|
||||
if pf.system() == "Darwin" and "Mac" in platforms:
|
||||
self.platform_combo_box.setCurrentIndex(platforms.index("Mac"))
|
||||
|
||||
self.max_workers_spin.setValue(self.core.lgd.config.getint("Legendary", "max_workers", fallback=0))
|
||||
|
@ -112,22 +112,10 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.ignore_space_check.stateChanged.connect(self.option_changed)
|
||||
self.download_only_check.stateChanged.connect(lambda: self.non_reload_option_changed("download_only"))
|
||||
self.shortcut_cb.stateChanged.connect(lambda: self.non_reload_option_changed("shortcut"))
|
||||
self.sdl_list_checks = list()
|
||||
try:
|
||||
for key, info in games[self.app_name].items():
|
||||
cb = QDataCheckBox(info["name"], info["tags"])
|
||||
if key == "__required":
|
||||
self.dl_item.options.sdl_list.extend(info["tags"])
|
||||
cb.setChecked(True)
|
||||
cb.setDisabled(True)
|
||||
self.sdl_list_layout.addWidget(cb)
|
||||
self.sdl_list_checks.append(cb)
|
||||
self.sdl_list_frame.resize(self.sdl_list_frame.minimumSize())
|
||||
for cb in self.sdl_list_checks:
|
||||
cb.stateChanged.connect(self.option_changed)
|
||||
except (KeyError, AttributeError):
|
||||
self.sdl_list_frame.setVisible(False)
|
||||
self.sdl_list_label.setVisible(False)
|
||||
|
||||
self.sdl_list_cbs: List[TagCheckBox] = []
|
||||
self.config_tags: Optional[List[str]] = None
|
||||
self.setup_sdl_list("Mac" if pf.system() == "Darwin" and "Mac" in platforms else "Windows")
|
||||
|
||||
self.install_button.setEnabled(False)
|
||||
|
||||
|
@ -141,7 +129,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.shortcut_cb.setVisible(False)
|
||||
self.shortcut_lbl.setVisible(False)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
if pf.system() == "Darwin":
|
||||
self.shortcut_cb.setDisabled(True)
|
||||
self.shortcut_cb.setChecked(False)
|
||||
self.shortcut_cb.setToolTip(self.tr("Creating a shortcut is not supported on MacOS"))
|
||||
|
@ -156,7 +144,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.verify_button.clicked.connect(self.verify_clicked)
|
||||
self.install_button.clicked.connect(self.install_clicked)
|
||||
|
||||
self.resize(self.minimumSize())
|
||||
self.install_dialog_layout.setSizeConstraint(self.install_dialog_layout.SetFixedSize)
|
||||
|
||||
def execute(self):
|
||||
if self.silent:
|
||||
|
@ -166,6 +154,37 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.verify_clicked()
|
||||
self.show()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setup_sdl_list(self, platform="Windows"):
|
||||
for cb in self.sdl_list_cbs:
|
||||
cb.disconnect()
|
||||
cb.deleteLater()
|
||||
self.sdl_list_cbs.clear()
|
||||
|
||||
if config_tags := self.core.lgd.config.get(self.game.app_name, 'install_tags', fallback=None):
|
||||
self.config_tags = config_tags.split(",")
|
||||
config_disable_sdl = self.core.lgd.config.getboolean(self.game.app_name, 'disable_sdl', fallback=False)
|
||||
sdl_name = get_sdl_appname(self.game.app_name)
|
||||
if not config_disable_sdl and sdl_name is not None:
|
||||
# FIXME: this should be updated whenever platform changes
|
||||
sdl_data = self.core.get_sdl_data(sdl_name, platform=platform)
|
||||
if sdl_data:
|
||||
for tag, info in sdl_data.items():
|
||||
cb = TagCheckBox(info["name"], info["tags"])
|
||||
if tag == "__required":
|
||||
cb.setChecked(True)
|
||||
cb.setDisabled(True)
|
||||
if all(elem in self.config_tags for elem in info["tags"]):
|
||||
cb.setChecked(True)
|
||||
self.sdl_list_layout.addWidget(cb)
|
||||
self.sdl_list_cbs.append(cb)
|
||||
self.sdl_list_frame.resize(self.sdl_list_frame.minimumSize())
|
||||
for cb in self.sdl_list_cbs:
|
||||
cb.stateChanged.connect(self.option_changed)
|
||||
else:
|
||||
self.sdl_list_frame.setVisible(False)
|
||||
self.sdl_list_label.setVisible(False)
|
||||
|
||||
def get_options(self):
|
||||
self.dl_item.options.base_path = self.install_dir_edit.text() if not self.update else None
|
||||
|
||||
|
@ -176,11 +195,12 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.dl_item.options.ignore_space = self.ignore_space_check.isChecked()
|
||||
self.dl_item.options.no_install = self.download_only_check.isChecked()
|
||||
self.dl_item.options.platform = self.platform_combo_box.currentText()
|
||||
self.dl_item.options.sdl_list = [""]
|
||||
for cb in self.sdl_list_checks:
|
||||
if data := cb.isChecked():
|
||||
# noinspection PyTypeChecker
|
||||
self.dl_item.options.sdl_list.extend(data)
|
||||
if self.sdl_list_cbs:
|
||||
self.dl_item.options.install_tag = [""]
|
||||
for cb in self.sdl_list_cbs:
|
||||
if data := cb.isChecked():
|
||||
# noinspection PyTypeChecker
|
||||
self.dl_item.options.install_tag.extend(data)
|
||||
|
||||
def get_download_info(self):
|
||||
self.dl_item.download = None
|
||||
|
@ -221,6 +241,12 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.dl_item.options.install_preqs = self.install_preqs_check.isChecked()
|
||||
|
||||
def cancel_clicked(self):
|
||||
if self.config_tags:
|
||||
self.core.lgd.config.set(self.game.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
|
||||
self.core.lgd.config.remove_option(self.game.app_name, 'install_tags')
|
||||
|
||||
self.dl_item.download = None
|
||||
self.reject_close = False
|
||||
self.close()
|
||||
|
@ -246,7 +272,7 @@ class InstallDialog(QDialog, Ui_InstallDialog):
|
|||
self.cancel_button.setEnabled(True)
|
||||
if self.silent:
|
||||
self.close()
|
||||
if platform.system() == "Windows" or ArgumentsSingleton().debug:
|
||||
if pf.system() == "Windows" or ArgumentsSingleton().debug:
|
||||
if dl_item.igame.prereq_info and not dl_item.igame.prereq_info.get("installed", False):
|
||||
self.install_preqs_check.setVisible(True)
|
||||
self.install_preqs_lbl.setVisible(True)
|
||||
|
@ -302,52 +328,27 @@ class InstallInfoWorker(QRunnable):
|
|||
self.signals = InstallInfoWorker.Signals()
|
||||
self.core = core
|
||||
self.dl_item = dl_item
|
||||
self.is_overlay_install = self.dl_item.options.overlay
|
||||
self.game = game
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self):
|
||||
try:
|
||||
if not self.is_overlay_install:
|
||||
if not self.dl_item.options.overlay:
|
||||
cli = LegendaryCLISingleton()
|
||||
download = InstallDownloadModel(
|
||||
# *self.core.prepare_download(
|
||||
*cli.install_game(LgndrInstallGameArgs(
|
||||
app_name=self.dl_item.options.app_name,
|
||||
base_path=self.dl_item.options.base_path,
|
||||
force=self.dl_item.options.force,
|
||||
no_install=self.dl_item.options.no_install,
|
||||
status_q=self.dl_item.status_q,
|
||||
shared_memory=self.dl_item.options.shared_memory,
|
||||
max_workers=self.dl_item.options.max_workers,
|
||||
# game_folder=,
|
||||
# disable_patching=,
|
||||
# override_manifest=,
|
||||
# override_old_manifest=,
|
||||
# override_base_url=,
|
||||
platform=self.dl_item.options.platform,
|
||||
# file_prefix_filter=,
|
||||
# file_exclude_filter=,
|
||||
# file_install_tag=,
|
||||
order_opt=self.dl_item.options.order_opt,
|
||||
# dl_timeout=,
|
||||
repair_mode=self.dl_item.options.repair_mode,
|
||||
repair_and_update=self.dl_item.options.repair_and_update,
|
||||
ignore_space=self.dl_item.options.ignore_space,
|
||||
# disable_delta=,
|
||||
# override_delta_manifest=,
|
||||
# reset_sdl=,
|
||||
sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list,)
|
||||
)
|
||||
status = LgndrIndirectStatus()
|
||||
result = cli.install_game(
|
||||
LgndrInstallGameArgs(**self.dl_item.options.as_install_kwargs(), indirect_status=status)
|
||||
)
|
||||
|
||||
if result:
|
||||
download = InstallDownloadModel(*result)
|
||||
else:
|
||||
raise LgndrException(status.message)
|
||||
else:
|
||||
if not os.path.exists(path := self.dl_item.options.base_path):
|
||||
os.makedirs(path)
|
||||
|
||||
dlm, analysis, igame = self.core.prepare_overlay_install(
|
||||
path=self.dl_item.options.base_path,
|
||||
status_q=self.dl_item.status_q,
|
||||
path=self.dl_item.options.base_path
|
||||
)
|
||||
|
||||
download = InstallDownloadModel(
|
||||
|
@ -371,14 +372,14 @@ class InstallInfoWorker(QRunnable):
|
|||
self.signals.finished.emit()
|
||||
|
||||
|
||||
class QDataCheckBox(QCheckBox):
|
||||
def __init__(self, text, data=None, parent=None):
|
||||
super(QDataCheckBox, self).__init__(parent)
|
||||
class TagCheckBox(QCheckBox):
|
||||
def __init__(self, text, tags: List[str], parent=None):
|
||||
super(TagCheckBox, self).__init__(parent)
|
||||
self.setText(text)
|
||||
self.data = data
|
||||
self.tags = tags
|
||||
|
||||
def isChecked(self):
|
||||
if super(QDataCheckBox, self).isChecked():
|
||||
return self.data
|
||||
def isChecked(self) -> Union[bool, List[str]]:
|
||||
if super(TagCheckBox, self).isChecked():
|
||||
return self.tags
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
from enum import Enum, IntEnum
|
||||
from typing import Tuple
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
@ -7,7 +6,6 @@ from PyQt5.QtWidgets import (
|
|||
QLabel,
|
||||
QVBoxLayout,
|
||||
QCheckBox,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QPushButton,
|
||||
)
|
||||
|
@ -23,13 +21,15 @@ class UninstallDialog(QDialog):
|
|||
self.setWindowTitle("Uninstall Game")
|
||||
layout = QVBoxLayout()
|
||||
self.info_text = QLabel(
|
||||
self.tr("Do you really want to uninstall {}").format(game.app_title)
|
||||
self.tr("Do you really want to uninstall <b>{}</b> ?").format(game.app_title)
|
||||
)
|
||||
layout.addWidget(self.info_text)
|
||||
self.keep_files = QCheckBox(self.tr("Keep Files"))
|
||||
form_layout = QFormLayout()
|
||||
form_layout.setContentsMargins(0, 10, 0, 10)
|
||||
form_layout.addRow(QLabel(self.tr("Do you want to keep files?")), self.keep_files)
|
||||
self.keep_files = QCheckBox(self.tr("Keep game files?"))
|
||||
self.keep_config = QCheckBox(self.tr("Keep game configuation?"))
|
||||
form_layout = QVBoxLayout()
|
||||
form_layout.setContentsMargins(6, 6, 0, 6)
|
||||
form_layout.addWidget(self.keep_files)
|
||||
form_layout.addWidget(self.keep_config)
|
||||
layout.addLayout(form_layout)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
|
@ -41,22 +41,22 @@ class UninstallDialog(QDialog):
|
|||
self.cancel_button = QPushButton(self.tr("Cancel"))
|
||||
self.cancel_button.clicked.connect(self.cancel)
|
||||
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self.ok_button)
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self.cancel_button)
|
||||
layout.addLayout(button_layout)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.options: Tuple[bool, bool] = (False, False)
|
||||
self.options: Tuple[bool, bool, bool] = (False, False, False)
|
||||
|
||||
def get_options(self) -> Tuple[bool, bool]:
|
||||
def get_options(self) -> Tuple[bool, bool, bool]:
|
||||
self.exec_()
|
||||
return self.options
|
||||
|
||||
def ok(self):
|
||||
self.options = (True, self.keep_files.isChecked())
|
||||
self.options = (True, self.keep_files.isChecked(), self.keep_config.isChecked())
|
||||
self.close()
|
||||
|
||||
def cancel(self):
|
||||
self.options = (False, False)
|
||||
self.options = (False, False, False)
|
||||
self.close()
|
||||
|
|
|
@ -23,17 +23,11 @@ class DownloadThread(QThread):
|
|||
status = pyqtSignal(str)
|
||||
statistics = pyqtSignal(UIUpdate)
|
||||
|
||||
def __init__(self, core: LegendaryCore, queue_item: InstallQueueItemModel):
|
||||
def __init__(self, core: LegendaryCore, item: InstallQueueItemModel):
|
||||
super(DownloadThread, self).__init__()
|
||||
self.core = core
|
||||
self.signals = GlobalSignalsSingleton()
|
||||
self.dlm = queue_item.download.dlmanager
|
||||
self.no_install = queue_item.options.no_install
|
||||
self.status_q = queue_item.status_q
|
||||
self.igame = queue_item.download.igame
|
||||
self.repair = queue_item.download.repair
|
||||
self.repair_file = queue_item.download.repair_file
|
||||
self.queue_item = queue_item
|
||||
self.core: LegendaryCore = core
|
||||
self.item: InstallQueueItemModel = item
|
||||
|
||||
self._kill = False
|
||||
|
||||
|
@ -42,9 +36,9 @@ class DownloadThread(QThread):
|
|||
dl_stopped = False
|
||||
try:
|
||||
|
||||
self.dlm.start()
|
||||
self.item.download.dlmanager.start()
|
||||
time.sleep(1)
|
||||
while self.dlm.is_alive():
|
||||
while self.item.download.dlmanager.is_alive():
|
||||
if self._kill:
|
||||
self.status.emit("stop")
|
||||
logger.info("Download stopping...")
|
||||
|
@ -52,15 +46,15 @@ class DownloadThread(QThread):
|
|||
# The code below is a temporary solution.
|
||||
# It should be removed once legendary supports stopping downloads more gracefully.
|
||||
|
||||
self.dlm.running = False
|
||||
self.item.download.dlmanager.running = False
|
||||
|
||||
# send conditions to unlock threads if they aren't already
|
||||
for cond in self.dlm.conditions:
|
||||
for cond in self.item.download.dlmanager.conditions:
|
||||
with cond:
|
||||
cond.notify()
|
||||
|
||||
# make sure threads are dead.
|
||||
for t in self.dlm.threads:
|
||||
for t in self.item.download.dlmanager.threads:
|
||||
t.join(timeout=5.0)
|
||||
if t.is_alive():
|
||||
logger.warning(f"Thread did not terminate! {repr(t)}")
|
||||
|
@ -74,10 +68,10 @@ class DownloadThread(QThread):
|
|||
"Writer results",
|
||||
),
|
||||
(
|
||||
self.dlm.dl_worker_queue,
|
||||
self.dlm.writer_queue,
|
||||
self.dlm.dl_result_q,
|
||||
self.dlm.writer_result_q,
|
||||
self.item.download.dlmanager.dl_worker_queue,
|
||||
self.item.download.dlmanager.writer_queue,
|
||||
self.item.download.dlmanager.dl_result_q,
|
||||
self.item.download.dlmanager.writer_result_q,
|
||||
),
|
||||
):
|
||||
logger.debug(f'Cleaning up queue "{name}"')
|
||||
|
@ -90,22 +84,22 @@ class DownloadThread(QThread):
|
|||
except AttributeError:
|
||||
logger.warning(f"Queue {name} did not close")
|
||||
|
||||
if self.dlm.writer_queue:
|
||||
if self.item.download.dlmanager.writer_queue:
|
||||
# cancel installation
|
||||
self.dlm.writer_queue.put_nowait(WriterTask("", kill=True))
|
||||
self.item.download.dlmanager.writer_queue.put_nowait(WriterTask("", kill=True))
|
||||
|
||||
# forcibly kill DL workers that are not actually dead yet
|
||||
for child in self.dlm.children:
|
||||
for child in self.item.download.dlmanager.children:
|
||||
if child.exitcode is None:
|
||||
child.terminate()
|
||||
|
||||
if self.dlm.shared_memory:
|
||||
if self.item.download.dlmanager.shared_memory:
|
||||
# close up shared memory
|
||||
self.dlm.shared_memory.close()
|
||||
self.dlm.shared_memory.unlink()
|
||||
self.dlm.shared_memory = None
|
||||
self.item.download.dlmanager.shared_memory.close()
|
||||
self.item.download.dlmanager.shared_memory.unlink()
|
||||
self.item.download.dlmanager.shared_memory = None
|
||||
|
||||
self.dlm.kill()
|
||||
self.item.download.dlmanager.kill()
|
||||
|
||||
# force kill any threads that are somehow still alive
|
||||
for proc in psutil.process_iter():
|
||||
|
@ -126,11 +120,11 @@ class DownloadThread(QThread):
|
|||
dl_stopped = True
|
||||
try:
|
||||
if not dl_stopped:
|
||||
self.statistics.emit(self.status_q.get(timeout=1))
|
||||
self.statistics.emit(self.item.download.dlmanager.status_queue.get(timeout=1))
|
||||
except queue.Empty:
|
||||
pass
|
||||
|
||||
self.dlm.join()
|
||||
self.item.download.dlmanager.join()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
@ -145,20 +139,20 @@ class DownloadThread(QThread):
|
|||
self.status.emit("dl_finished")
|
||||
end_t = time.time()
|
||||
logger.info(f"Download finished in {end_t - start_time}s")
|
||||
game = self.core.get_game(self.igame.app_name)
|
||||
game = self.core.get_game(self.item.download.igame.app_name)
|
||||
|
||||
if self.queue_item.options.overlay:
|
||||
if self.item.options.overlay:
|
||||
self.signals.overlay_installation_finished.emit()
|
||||
self.core.finish_overlay_install(self.igame)
|
||||
self.core.finish_overlay_install(self.item.download.igame)
|
||||
self.status.emit("finish")
|
||||
return
|
||||
|
||||
if not self.no_install:
|
||||
postinstall = self.core.install_game(self.igame)
|
||||
if not self.item.options.no_install:
|
||||
postinstall = self.core.install_game(self.item.download.igame)
|
||||
if postinstall:
|
||||
self._handle_postinstall(postinstall, self.igame)
|
||||
self._handle_postinstall(postinstall, self.item.download.igame)
|
||||
|
||||
dlcs = self.core.get_dlc_for_game(self.igame.app_name)
|
||||
dlcs = self.core.get_dlc_for_game(self.item.download.igame.app_name)
|
||||
if dlcs:
|
||||
print("The following DLCs are available for this game:")
|
||||
for dlc in dlcs:
|
||||
|
@ -179,21 +173,21 @@ class DownloadThread(QThread):
|
|||
f'To download saves for this game run "legendary sync-saves {game.app_name}"'
|
||||
)
|
||||
old_igame = self.core.get_installed_game(game.app_name)
|
||||
if old_igame and self.repair and os.path.exists(self.repair_file):
|
||||
if old_igame and self.item.download.repair and os.path.exists(self.item.download.repair_file):
|
||||
if old_igame.needs_verification:
|
||||
old_igame.needs_verification = False
|
||||
self.core.install_game(old_igame)
|
||||
|
||||
logger.debug("Removing repair file.")
|
||||
os.remove(self.repair_file)
|
||||
if old_igame and old_igame.install_tags != self.igame.install_tags:
|
||||
old_igame.install_tags = self.igame.install_tags
|
||||
os.remove(self.item.download.repair_file)
|
||||
if old_igame and old_igame.install_tags != self.item.download.igame.install_tags:
|
||||
old_igame.install_tags = self.item.download.igame.install_tags
|
||||
logger.info("Deleting now untagged files.")
|
||||
self.core.uninstall_tag(old_igame)
|
||||
self.core.install_game(old_igame)
|
||||
|
||||
if not self.queue_item.options.update and self.queue_item.options.create_shortcut:
|
||||
if not create_desktop_link(self.queue_item.options.app_name, self.core, "desktop"):
|
||||
if not self.item.options.update and self.item.options.create_shortcut:
|
||||
if not create_desktop_link(self.item.options.app_name, self.core, "desktop"):
|
||||
# maybe add it to download summary, to show in finished downloads
|
||||
pass
|
||||
else:
|
||||
|
@ -204,7 +198,7 @@ class DownloadThread(QThread):
|
|||
def _handle_postinstall(self, postinstall, igame):
|
||||
logger.info(f"Postinstall info: {postinstall}")
|
||||
if platform.system() == "Windows":
|
||||
if self.queue_item.options.install_preqs:
|
||||
if self.item.options.install_preqs:
|
||||
self.core.prereq_installed(igame.app_name)
|
||||
req_path, req_exec = os.path.split(postinstall["path"])
|
||||
work_dir = os.path.join(igame.install_path, req_path)
|
||||
|
@ -218,7 +212,7 @@ class DownloadThread(QThread):
|
|||
proc.start(fullpath, postinstall.get("args", []))
|
||||
proc.waitForFinished() # wait, because it is inside the thread
|
||||
else:
|
||||
self.core.prereq_installed(self.igame.app_name)
|
||||
self.core.prereq_installed(self.item.download.igame.app_name)
|
||||
else:
|
||||
logger.info("Automatic installation not available on Linux.")
|
||||
|
||||
|
|
|
@ -139,6 +139,16 @@ class GamesTab(QStackedWidget):
|
|||
self.view_stack.setCurrentWidget(self.icon_view_scroll)
|
||||
|
||||
self.head_bar.search_bar.textChanged.connect(lambda x: self.filter_games("", x))
|
||||
self.head_bar.search_bar.textChanged.connect(
|
||||
lambda x: self.icon_view_scroll.verticalScrollBar().setSliderPosition(
|
||||
self.icon_view_scroll.verticalScrollBar().minimum()
|
||||
)
|
||||
)
|
||||
self.head_bar.search_bar.textChanged.connect(
|
||||
lambda x: self.list_view_scroll.verticalScrollBar().setSliderPosition(
|
||||
self.list_view_scroll.verticalScrollBar().minimum()
|
||||
)
|
||||
)
|
||||
self.head_bar.filterChanged.connect(self.filter_games)
|
||||
self.head_bar.refresh_list.clicked.connect(self.update_list)
|
||||
self.head_bar.view.toggled.connect(self.toggle_view)
|
||||
|
@ -320,8 +330,8 @@ class GamesTab(QStackedWidget):
|
|||
visible = True
|
||||
|
||||
if (
|
||||
search_text not in widget.game.app_name.lower()
|
||||
and search_text not in widget.game.app_title.lower()
|
||||
search_text.lower() not in widget.game.app_name.lower()
|
||||
and search_text.lower() not in widget.game.app_title.lower()
|
||||
):
|
||||
opacity = 0.25
|
||||
else:
|
||||
|
@ -345,7 +355,7 @@ class GamesTab(QStackedWidget):
|
|||
# lk: it sorts by installed then by title
|
||||
installing_widget = self.icon_view.layout().remove(type(self.installing_widget).__name__)
|
||||
if sort_by:
|
||||
self.icon_view.layout().sort(lambda x: (sort_by not in x.widget().game.app_title.lower(),))
|
||||
self.icon_view.layout().sort(lambda x: (sort_by.lower() not in x.widget().game.app_title.lower(),))
|
||||
else:
|
||||
self.icon_view.layout().sort(
|
||||
lambda x: (
|
||||
|
|
|
@ -129,7 +129,7 @@ class GameInfo(QWidget, Ui_GameInfo):
|
|||
)
|
||||
return
|
||||
self.signals.install_game.emit(
|
||||
InstallOptionsModel(app_name=self.game.app_name, repair_mode=True, update=True)
|
||||
InstallOptionsModel(app_name=self.game.app_name, repair_mode=True, repair_and_update=True, update=True)
|
||||
)
|
||||
|
||||
def verify(self):
|
||||
|
@ -163,24 +163,24 @@ class GameInfo(QWidget, Ui_GameInfo):
|
|||
|
||||
@pyqtSlot(str, bool, int, int)
|
||||
def verify_result(self, app_name, success, failed, missing):
|
||||
igame = self.core.get_installed_game(app_name)
|
||||
if success:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Summary",
|
||||
"Game was verified successfully. No missing or corrupt files found",
|
||||
self.tr("Summary - {}").format(igame.title),
|
||||
self.tr("Game has been verified successfully. No missing or corrupt files found").format(igame.title),
|
||||
)
|
||||
igame = self.core.get_installed_game(app_name)
|
||||
if igame.needs_verification:
|
||||
igame.needs_verification = False
|
||||
self.core.lgd.set_installed_game(igame.app_name, igame)
|
||||
self.verification_finished.emit(igame)
|
||||
elif failed == missing == -1:
|
||||
QMessageBox.warning(self, "Warning", self.tr("Something went wrong"))
|
||||
QMessageBox.warning(self, self.tr("Warning - {}").format(igame.title), self.tr("Something went wrong"))
|
||||
|
||||
else:
|
||||
ans = QMessageBox.question(
|
||||
self,
|
||||
"Summary",
|
||||
self.tr("Summary - {}").format(igame.title),
|
||||
self.tr(
|
||||
"Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them?"
|
||||
).format(failed, missing),
|
||||
|
@ -188,9 +188,7 @@ class GameInfo(QWidget, Ui_GameInfo):
|
|||
QMessageBox.Yes,
|
||||
)
|
||||
if ans == QMessageBox.Yes:
|
||||
self.signals.install_game.emit(
|
||||
InstallOptionsModel(app_name=app_name, repair_mode=True, update=True)
|
||||
)
|
||||
self.repair()
|
||||
self.verify_widget.setCurrentIndex(0)
|
||||
self.verify_threads.pop(app_name)
|
||||
self.move_button.setEnabled(True)
|
||||
|
|
|
@ -152,7 +152,7 @@ class GameUtils(QObject):
|
|||
if not os.path.exists(igame.install_path):
|
||||
if QMessageBox.Yes == QMessageBox.question(
|
||||
None,
|
||||
"Uninstall",
|
||||
self.tr("Uninstall - {}").format(igame.title),
|
||||
self.tr(
|
||||
"Game files of {} do not exist. Remove it from installed games?"
|
||||
).format(igame.title),
|
||||
|
@ -164,10 +164,12 @@ class GameUtils(QObject):
|
|||
else:
|
||||
return False
|
||||
|
||||
proceed, keep_files = UninstallDialog(game).get_options()
|
||||
proceed, keep_files, keep_config = UninstallDialog(game).get_options()
|
||||
if not proceed:
|
||||
return False
|
||||
legendary_utils.uninstall_game(self.core, game.app_name, keep_files)
|
||||
success, message = legendary_utils.uninstall_game(self.core, game.app_name, keep_files, keep_config)
|
||||
if not success:
|
||||
QMessageBox.warning(None, self.tr("Uninstall - {}").format(igame.title), message, QMessageBox.Close)
|
||||
self.signals.game_uninstalled.emit(app_name)
|
||||
return True
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
import os
|
||||
from argparse import Namespace
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from logging import getLogger
|
||||
|
@ -13,9 +12,11 @@ from PyQt5.QtWidgets import QFileDialog, QGroupBox, QCompleter, QTreeView, QHead
|
|||
|
||||
from rare.lgndr.api_arguments import LgndrImportGameArgs
|
||||
from rare.lgndr.api_exception import LgndrException
|
||||
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton, GlobalSignalsSingleton, ApiResultsSingleton
|
||||
from rare.ui.components.tabs.games.import_sync.import_group import Ui_ImportGroup
|
||||
from rare.utils.extra_widgets import IndicatorLineEdit, PathEdit
|
||||
from rare.widgets.elide_label import ElideLabel
|
||||
|
||||
logger = getLogger("Import")
|
||||
|
||||
|
@ -46,7 +47,9 @@ class ImportResult(IntEnum):
|
|||
@dataclass
|
||||
class ImportedGame:
|
||||
result: ImportResult
|
||||
path: Optional[str] = None
|
||||
app_name: Optional[str] = None
|
||||
app_title: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
|
@ -59,7 +62,6 @@ class ImportWorker(QRunnable):
|
|||
super(ImportWorker, self).__init__()
|
||||
self.signals = self.Signals()
|
||||
self.core = LegendaryCoreSingleton()
|
||||
self.tr = lambda message: qApp.translate("ImportThread", message)
|
||||
|
||||
self.path = Path(path)
|
||||
self.app_name = app_name
|
||||
|
@ -83,29 +85,30 @@ class ImportWorker(QRunnable):
|
|||
self.signals.finished.emit(result_list)
|
||||
|
||||
def __try_import(self, path: Path, app_name: str = None) -> ImportedGame:
|
||||
result = ImportedGame(ImportResult.ERROR, None, None)
|
||||
result = ImportedGame(ImportResult.ERROR)
|
||||
result.path = str(path)
|
||||
if app_name or (app_name := find_app_name(str(path), self.core)):
|
||||
result.app_name = app_name
|
||||
app_title = self.core.get_game(app_name).app_title
|
||||
result.app_title = app_title = self.core.get_game(app_name).app_title
|
||||
success, message = self.__import_game(path, app_name, app_title)
|
||||
if not success:
|
||||
result.result = ImportResult.FAILED
|
||||
result.message = f"{app_title} - {message}"
|
||||
result.message = message
|
||||
else:
|
||||
result.result = ImportResult.SUCCESS
|
||||
result.message = self.tr("{} - Imported successfully").format(app_title)
|
||||
else:
|
||||
result.message = self.tr("Could not find AppName for {}").format(str(path))
|
||||
return result
|
||||
|
||||
def __import_game(self, path: Path, app_name: str, app_title: str):
|
||||
cli = LegendaryCLISingleton()
|
||||
status = LgndrIndirectStatus()
|
||||
args = LgndrImportGameArgs(
|
||||
app_path=str(path),
|
||||
app_name=app_name,
|
||||
indirect_status=status,
|
||||
get_boolean_choice=lambda prompt, default=True: self.import_dlcs
|
||||
)
|
||||
return cli.import_game(args)
|
||||
cli.import_game(args)
|
||||
return status.success, status.message
|
||||
|
||||
|
||||
class AppNameCompleter(QCompleter):
|
||||
|
@ -194,6 +197,10 @@ class ImportGroup(QGroupBox):
|
|||
self.ui.import_button.clicked.connect(
|
||||
lambda: self.import_pressed(self.path_edit.text())
|
||||
)
|
||||
|
||||
self.info_label = ElideLabel(text="", parent=self)
|
||||
self.ui.button_info_layout.addWidget(self.info_label)
|
||||
|
||||
self.threadpool = QThreadPool.globalInstance()
|
||||
|
||||
def path_edit_cb(self, path) -> Tuple[bool, str, str]:
|
||||
|
@ -207,7 +214,7 @@ class ImportGroup(QGroupBox):
|
|||
return False, path, ""
|
||||
|
||||
def path_changed(self, path):
|
||||
self.ui.info_label.setText("")
|
||||
self.info_label.setText("")
|
||||
self.ui.import_folder_check.setCheckState(Qt.Unchecked)
|
||||
if self.path_edit.is_valid:
|
||||
self.app_name_edit.setText(find_app_name(path, self.core))
|
||||
|
@ -223,7 +230,7 @@ class ImportGroup(QGroupBox):
|
|||
return False, text, IndicatorLineEdit.reasons.game_not_installed
|
||||
|
||||
def app_name_changed(self, app_name: str):
|
||||
self.ui.info_label.setText("")
|
||||
self.info_label.setText("")
|
||||
self.ui.import_dlcs_check.setCheckState(Qt.Unchecked)
|
||||
if self.app_name_edit.is_valid:
|
||||
self.ui.import_dlcs_check.setEnabled(
|
||||
|
@ -272,16 +279,16 @@ class ImportGroup(QGroupBox):
|
|||
if len(result) == 1:
|
||||
res = result[0]
|
||||
if res.result == ImportResult.SUCCESS:
|
||||
self.ui.info_label.setText(
|
||||
self.tr("Success: {}").format(res.message)
|
||||
self.info_label.setText(
|
||||
self.tr("Success: <b>{}</b> imported").format(res.app_title)
|
||||
)
|
||||
elif res.result == ImportResult.FAILED:
|
||||
self.ui.info_label.setText(
|
||||
self.tr("Failed: {}").format(res.message)
|
||||
self.info_label.setText(
|
||||
self.tr("Failed: <b>{}</b> - {}").format(res.app_title, res.message)
|
||||
)
|
||||
else:
|
||||
self.ui.info_label.setText(
|
||||
self.tr("Error: {}").format(res.message)
|
||||
self.info_label.setText(
|
||||
self.tr("Error: Could not find AppName for <b>{}</b>").format(res.path)
|
||||
)
|
||||
else:
|
||||
success = [r for r in result if r.result == ImportResult.SUCCESS]
|
||||
|
@ -301,15 +308,15 @@ class ImportGroup(QGroupBox):
|
|||
details: List = []
|
||||
for res in success:
|
||||
details.append(
|
||||
self.tr("Success: {}").format(res.message)
|
||||
self.tr("Success: {} imported").format(res.app_title)
|
||||
)
|
||||
for res in failure:
|
||||
details.append(
|
||||
self.tr("Failed: {}").format(res.message)
|
||||
self.tr("Failed: {} - {}").format(res.app_title, res.message)
|
||||
)
|
||||
for res in errored:
|
||||
details.append(
|
||||
self.tr("Error: {}").format(res.message)
|
||||
self.tr("Error: Could not find AppName for {}").format(res.path)
|
||||
)
|
||||
messagebox.setDetailedText("\n".join(details))
|
||||
messagebox.show()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from dataclasses import dataclass
|
||||
from multiprocessing import Queue
|
||||
from typing import Callable, List
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
from .api_monkeys import get_boolean_choice
|
||||
from .api_monkeys import get_boolean_choice, LgndrIndirectStatus
|
||||
|
||||
"""
|
||||
@dataclass(kw_only=True)
|
||||
|
@ -25,6 +25,7 @@ class LgndrImportGameArgs:
|
|||
with_dlcs: bool = False
|
||||
yes: bool = False
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
get_boolean_choice: Callable[[str, bool], bool] = lambda prompt, default=True: default
|
||||
|
||||
|
||||
|
@ -34,6 +35,7 @@ class LgndrUninstallGameArgs:
|
|||
keep_files: bool = False
|
||||
yes: bool = False
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
get_boolean_choice: Callable[[str, bool], bool] = lambda prompt, default=True: default
|
||||
|
||||
|
||||
|
@ -41,6 +43,7 @@ class LgndrUninstallGameArgs:
|
|||
class LgndrVerifyGameArgs:
|
||||
app_name: str
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print(
|
||||
f"Verification progress: {a0}/{a1} ({a2:.01f}%) [{a3:.1f} MiB/s]\t\r"
|
||||
)
|
||||
|
@ -50,7 +53,6 @@ class LgndrVerifyGameArgs:
|
|||
class LgndrInstallGameArgs:
|
||||
app_name: str
|
||||
base_path: str = ""
|
||||
status_q: Queue = None
|
||||
shared_memory: int = 0
|
||||
max_workers: int = 0
|
||||
force: bool = False
|
||||
|
@ -62,7 +64,7 @@ class LgndrInstallGameArgs:
|
|||
platform: str = "Windows"
|
||||
file_prefix: List = None
|
||||
file_exclude_prefix: List = None
|
||||
install_tag: List = None
|
||||
install_tag: Optional[List[str]] = None
|
||||
order_opt: bool = False
|
||||
dl_timeout: int = 10
|
||||
repair_mode: bool = False
|
||||
|
@ -79,6 +81,7 @@ class LgndrInstallGameArgs:
|
|||
disable_https: bool = False
|
||||
yes: bool = True
|
||||
# Rare: Extra arguments
|
||||
indirect_status: LgndrIndirectStatus = LgndrIndirectStatus()
|
||||
get_boolean_choice: Callable[[str, bool], bool] = lambda prompt, default=True: default
|
||||
sdl_prompt: Callable[[str, str], List[str]] = lambda sdl_data, title: [""]
|
||||
verify_stdout: Callable[[int, int, float, float], None] = lambda a0, a1, a2, a3: print(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox, QLabel
|
||||
|
||||
|
@ -24,17 +25,52 @@ class UILogHandler(logging.Handler):
|
|||
self.widget.setText(record.getMessage())
|
||||
|
||||
|
||||
class LgndrReturnLogger:
|
||||
def __init__(self, logger: logging.Logger, level: int = logging.ERROR):
|
||||
@dataclass
|
||||
class LgndrIndirectStatus:
|
||||
success: bool = False
|
||||
message: str = ""
|
||||
|
||||
def __len__(self):
|
||||
if self.message:
|
||||
return 2
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
return self.success
|
||||
|
||||
def __getitem__(self, item):
|
||||
if item == 0:
|
||||
return self.success
|
||||
elif item == 1:
|
||||
return self.message
|
||||
else:
|
||||
raise IndexError
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.success, self.message))
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
|
||||
class LgndrIndirectLogger:
|
||||
def __init__(self, status: LgndrIndirectStatus, logger: logging.Logger = None, level: int = logging.ERROR):
|
||||
self.logger = logger
|
||||
self.level = level
|
||||
self.value = False
|
||||
self.message = ""
|
||||
self.status = status
|
||||
|
||||
def set_logger(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
|
||||
def set_level(self, level: int):
|
||||
self.level = level
|
||||
|
||||
def _log(self, level: int, msg: str):
|
||||
self.value = True if level < self.level else False
|
||||
self.message = msg
|
||||
self.logger.log(level, msg)
|
||||
self.status.success = True if level < self.level else False
|
||||
self.status.message = msg
|
||||
if self.logger:
|
||||
self.logger.log(level, msg)
|
||||
|
||||
def debug(self, msg: str):
|
||||
self._log(logging.DEBUG, msg)
|
||||
|
@ -53,26 +89,3 @@ class LgndrReturnLogger:
|
|||
|
||||
def fatal(self, msg: str):
|
||||
self.critical(msg)
|
||||
|
||||
def __len__(self):
|
||||
if self.message:
|
||||
return 2
|
||||
else:
|
||||
return 0
|
||||
|
||||
def __bool__(self):
|
||||
return self.value
|
||||
|
||||
def __getitem__(self, item):
|
||||
if item == 0:
|
||||
return self.value
|
||||
elif item == 1:
|
||||
return self.message
|
||||
else:
|
||||
raise IndexError
|
||||
|
||||
def __iter__(self):
|
||||
return iter((self.value, self.message))
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import functools
|
||||
import os
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Optional, Union
|
||||
from typing import Optional, Union, Tuple
|
||||
|
||||
from legendary.cli import LegendaryCLI as LegendaryCLIReal
|
||||
from legendary.models.downloading import AnalysisResult, ConditionCheckResult
|
||||
|
@ -10,51 +9,27 @@ from legendary.models.game import Game, InstalledGame, VerifyResult
|
|||
from legendary.utils.lfs import validate_files
|
||||
from legendary.utils.selective_dl import get_sdl_appname
|
||||
|
||||
from .api_arguments import LgndrInstallGameArgs, LgndrImportGameArgs, LgndrVerifyGameArgs, LgndrUninstallGameArgs
|
||||
from .api_monkeys import LgndrIndirectStatus, LgndrIndirectLogger
|
||||
from .core import LegendaryCore
|
||||
from .manager import DLManager
|
||||
from .api_arguments import LgndrInstallGameArgs, LgndrImportGameArgs, LgndrVerifyGameArgs, LgndrUninstallGameArgs
|
||||
from .api_monkeys import return_exit, get_boolean_choice, LgndrReturnLogger
|
||||
|
||||
|
||||
class LegendaryCLI(LegendaryCLIReal):
|
||||
|
||||
"""
|
||||
@staticmethod
|
||||
def apply_wrap(func):
|
||||
|
||||
@functools.wraps(func)
|
||||
def inner(self, args, *oargs, **kwargs):
|
||||
old_exit = legendary.cli.exit
|
||||
legendary.cli.exit = exit
|
||||
|
||||
# old_choice = legendary.cli.get_boolean_choice
|
||||
# if hasattr(args, 'get_boolean_choice') and args.get_boolean_choice is not None:
|
||||
# legendary.cli.get_boolean_choice = args.get_boolean_choice
|
||||
|
||||
try:
|
||||
return func(self, args, *oargs, **kwargs)
|
||||
except LgndrException as ret:
|
||||
raise ret
|
||||
finally:
|
||||
# legendary.cli.get_boolean_choice = old_choice
|
||||
legendary.cli.exit = old_exit
|
||||
|
||||
return inner
|
||||
"""
|
||||
|
||||
def __init__(self, override_config=None, api_timeout=None):
|
||||
self.core = LegendaryCore(override_config, timeout=api_timeout)
|
||||
self.logger = logging.getLogger('cli')
|
||||
self.logging_queue = None
|
||||
# self.handler = LgndrCLILogHandler()
|
||||
# self.logger.addHandler(self.handler)
|
||||
|
||||
def resolve_aliases(self, name):
|
||||
return super(LegendaryCLI, self)._resolve_aliases(name)
|
||||
|
||||
def install_game(self, args: LgndrInstallGameArgs) -> (DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult):
|
||||
# Override logger for the local context to use message as part of the return value
|
||||
logger = LgndrReturnLogger(self.logger)
|
||||
def install_game(self, args: LgndrInstallGameArgs) -> Optional[Tuple[DLManager, AnalysisResult, InstalledGame, Game, bool, Optional[str], ConditionCheckResult]]:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
sdl_prompt = args.sdl_prompt
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
if self.core.is_installed(args.app_name):
|
||||
|
@ -71,15 +46,21 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
args.no_install = args.repair_and_update is False
|
||||
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{args.app_name}.repair')
|
||||
|
||||
# Rare: Rare is already logged in
|
||||
|
||||
if args.file_prefix or args.file_exclude_prefix:
|
||||
args.no_install = True
|
||||
|
||||
# Rare: Rare runs updates on already installed games only
|
||||
|
||||
game = self.core.get_game(args.app_name, update_meta=True, platform=args.platform)
|
||||
|
||||
if not game:
|
||||
logger.error(f'Could not find "{args.app_name}" in list of available games, '
|
||||
f'did you type the name correctly?')
|
||||
return logger
|
||||
return
|
||||
|
||||
# Rare: Rare checks this before calling 'install_game'
|
||||
|
||||
if args.platform not in game.asset_infos:
|
||||
if not args.no_install:
|
||||
|
@ -90,7 +71,7 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
logger.error(f'No app asset found for platform "{args.platform}", run '
|
||||
f'"legendary info --platform {args.platform}" and make '
|
||||
f'sure the app is available for the specified platform.')
|
||||
return logger
|
||||
return
|
||||
else:
|
||||
logger.warning(f'No asset found for platform "{args.platform}", '
|
||||
f'trying anyway since --no-install is set.')
|
||||
|
@ -104,25 +85,30 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
# download mode doesn't care about whether something's installed
|
||||
if not args.no_install:
|
||||
logger.fatal(f'Base game "{app_name}" is not installed!')
|
||||
return
|
||||
else:
|
||||
base_game = None
|
||||
|
||||
if args.repair_mode:
|
||||
if not self.core.is_installed(game.app_name):
|
||||
logger.error(f'Game "{game.app_title}" ({game.app_name}) is not installed!')
|
||||
return
|
||||
|
||||
if not os.path.exists(repair_file):
|
||||
logger.info('Game has not been verified yet.')
|
||||
# Rare: Dodge the path below for now
|
||||
logger.error('Game has not been verified yet.')
|
||||
return
|
||||
if not args.yes:
|
||||
if not args.get_boolean_choice(f'Verify "{game.app_name}" now ("no" will abort repair)?'):
|
||||
return logger
|
||||
if not get_boolean_choice(f'Verify "{game.app_name}" now ("no" will abort repair)?'):
|
||||
return
|
||||
try:
|
||||
self.verify_game(args, print_command=False, repair_mode=True, repair_online=args.repair_and_update)
|
||||
except ValueError:
|
||||
logger.error('To repair a game with a missing manifest you must run the command with '
|
||||
'"--repair-and-update". However this will redownload any file that does '
|
||||
'not match the current hash in its entirety.')
|
||||
return logger
|
||||
return
|
||||
else:
|
||||
logger.info(f'Using existing repair file: {repair_file}')
|
||||
|
||||
|
@ -154,7 +140,7 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
if '__required' in sdl_data:
|
||||
args.install_tag.extend(sdl_data['__required']['tags'])
|
||||
else:
|
||||
args.install_tag = args.sdl_prompt(sdl_data, game.app_title)
|
||||
args.install_tag = sdl_prompt(sdl_data, game.app_title)
|
||||
self.core.lgd.config.set(game.app_name, 'install_tags', ','.join(args.install_tag))
|
||||
else:
|
||||
logger.error(f'Unable to get SDL data for {sdl_name}')
|
||||
|
@ -176,7 +162,6 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
# todo use status queue to print progress from CLI
|
||||
# This has become a little ridiculous hasn't it?
|
||||
dlm, analysis, igame = self.core.prepare_download(game=game, base_game=base_game, base_path=args.base_path,
|
||||
status_q=args.status_q,
|
||||
force=args.force, max_shm=args.shared_memory,
|
||||
max_workers=args.max_workers, game_folder=args.game_folder,
|
||||
disable_patching=args.disable_patching,
|
||||
|
@ -200,8 +185,7 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
if not analysis.dl_size:
|
||||
logger.info('Download size is 0, the game is either already up to date or has not changed. Exiting...')
|
||||
self.clean_post_install(game, igame, args.repair_mode, repair_file)
|
||||
|
||||
logger.error('Nothing to do.')
|
||||
return
|
||||
|
||||
res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game,
|
||||
updating=self.core.is_installed(args.app_name),
|
||||
|
@ -209,9 +193,9 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
|
||||
return dlm, analysis, igame, game, args.repair_mode, repair_file, res
|
||||
|
||||
def clean_post_install(self, game: Game, igame: InstalledGame, repair: bool = False, repair_file: str = ''):
|
||||
# Override logger for the local context to use message as part of the return value
|
||||
logger = LgndrReturnLogger(self.logger)
|
||||
def clean_post_install(self, game: Game, igame: InstalledGame, repair: bool = False, repair_file: str = '') -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(LgndrIndirectStatus(), self.logger)
|
||||
|
||||
old_igame = self.core.get_installed_game(game.app_name)
|
||||
if old_igame and repair and os.path.exists(repair_file):
|
||||
|
@ -229,24 +213,23 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
self.core.uninstall_tag(old_igame)
|
||||
self.core.install_game(old_igame)
|
||||
|
||||
return logger
|
||||
|
||||
def handle_postinstall(self, postinstall, igame, yes=False):
|
||||
super(LegendaryCLI, self)._handle_postinstall(postinstall, igame, yes)
|
||||
|
||||
def uninstall_game(self, args: LgndrUninstallGameArgs):
|
||||
# Override logger for the local context to use message as part of the return value
|
||||
logger = LgndrReturnLogger(self.logger, level=logging.WARNING)
|
||||
def uninstall_game(self, args: LgndrUninstallGameArgs) -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger, logging.WARNING)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
igame = self.core.get_installed_game(args.app_name)
|
||||
if not igame:
|
||||
logger.error(f'Game {args.app_name} not installed, cannot uninstall!')
|
||||
return logger
|
||||
return
|
||||
|
||||
if not args.yes:
|
||||
if not args.get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
|
||||
return logger
|
||||
if not get_boolean_choice(f'Do you wish to uninstall "{igame.title}"?', default=False):
|
||||
return
|
||||
|
||||
try:
|
||||
if not igame.is_dlc:
|
||||
|
@ -261,19 +244,19 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
self.core.uninstall_game(igame, delete_files=not args.keep_files,
|
||||
delete_root_directory=not igame.is_dlc)
|
||||
logger.info('Game has been uninstalled.')
|
||||
return logger
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
|
||||
return logger
|
||||
return
|
||||
|
||||
def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False):
|
||||
# Override logger for the local context to use message as part of the return value
|
||||
logger = LgndrReturnLogger(self.logger)
|
||||
def verify_game(self, args: Union[LgndrVerifyGameArgs, LgndrInstallGameArgs], print_command=True, repair_mode=False, repair_online=False) -> Optional[Tuple[int, int]]:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
|
||||
args.app_name = self._resolve_aliases(args.app_name)
|
||||
if not self.core.is_installed(args.app_name):
|
||||
logger.error(f'Game "{args.app_name}" is not installed')
|
||||
return logger
|
||||
return
|
||||
|
||||
logger.info(f'Loading installed manifest for "{args.app_name}"')
|
||||
igame = self.core.get_installed_game(args.app_name)
|
||||
|
@ -281,7 +264,7 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
logger.error(f'Install path "{igame.install_path}" does not exist, make sure all necessary mounts '
|
||||
f'are available. If you previously deleted the game folder without uninstalling, run '
|
||||
f'"legendary uninstall -y {igame.app_name}" and reinstall from scratch.')
|
||||
return logger
|
||||
return
|
||||
|
||||
manifest_data, _ = self.core.get_installed_manifest(args.app_name)
|
||||
if manifest_data is None:
|
||||
|
@ -301,7 +284,7 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
logger.critical(f'Manifest appears to be missing! To repair, run "legendary repair '
|
||||
f'{args.app_name} --repair-and-update", this will however redownload all files '
|
||||
f'that do not match the latest manifest in their entirety.')
|
||||
return logger
|
||||
return
|
||||
|
||||
manifest = self.core.load_manifest(manifest_data)
|
||||
|
||||
|
@ -371,16 +354,17 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
|
||||
if not missing and not failed:
|
||||
logger.info('Verification finished successfully.')
|
||||
return logger, 0, 0
|
||||
return 0, 0
|
||||
else:
|
||||
logger.warning(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
|
||||
logger.error(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
|
||||
if print_command:
|
||||
logger.info(f'Run "legendary repair {args.app_name}" to repair your game installation.')
|
||||
return logger, len(failed), len(missing)
|
||||
return len(failed), len(missing)
|
||||
|
||||
def import_game(self, args: LgndrImportGameArgs):
|
||||
# Override logger for the local context to use message as part of the return value
|
||||
logger = LgndrReturnLogger(self.logger)
|
||||
def import_game(self, args: LgndrImportGameArgs) -> None:
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
get_boolean_choice = args.get_boolean_choice
|
||||
|
||||
# make sure path is absolute
|
||||
args.app_path = os.path.abspath(args.app_path)
|
||||
|
@ -388,21 +372,21 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
|
||||
if not os.path.exists(args.app_path):
|
||||
logger.error(f'Specified path "{args.app_path}" does not exist!')
|
||||
return logger
|
||||
return
|
||||
|
||||
if self.core.is_installed(args.app_name):
|
||||
logger.error('Game is already installed!')
|
||||
return logger
|
||||
return
|
||||
|
||||
if not self.core.login():
|
||||
logger.error('Log in failed!')
|
||||
return logger
|
||||
return
|
||||
|
||||
# do some basic checks
|
||||
game = self.core.get_game(args.app_name, update_meta=True, platform=args.platform)
|
||||
if not game:
|
||||
logger.fatal(f'Did not find game "{args.app_name}" on account.')
|
||||
return logger
|
||||
return
|
||||
|
||||
if game.is_dlc:
|
||||
release_info = game.metadata.get('mainGameItem', {}).get('releaseInfo')
|
||||
|
@ -412,10 +396,10 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
if not self.core.is_installed(main_game_appname):
|
||||
logger.error(f'Import candidate is DLC but base game "{main_game_title}" '
|
||||
f'(App name: "{main_game_appname}") is not installed!')
|
||||
return logger
|
||||
return
|
||||
else:
|
||||
logger.fatal(f'Unable to get base game information for DLC, cannot continue.')
|
||||
return logger
|
||||
return
|
||||
|
||||
# get everything needed for import from core, then run additional checks.
|
||||
manifest, igame = self.core.import_game(game, args.app_path, platform=args.platform)
|
||||
|
@ -435,12 +419,12 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
if folder and folder not in args.app_path:
|
||||
new_path = os.path.join(args.app_path, folder)
|
||||
logger.info(f'Did you mean "{new_path}"?')
|
||||
return logger
|
||||
return
|
||||
|
||||
if not game.is_dlc and not os.path.exists(exe_path) and not args.disable_check:
|
||||
logger.error(f'Game executable could not be found at "{exe_path}", '
|
||||
f'please verify that the specified path is correct.')
|
||||
return logger
|
||||
return
|
||||
|
||||
if ratio < 0.95:
|
||||
logger.warning('Some files are missing from the game installation, install may not '
|
||||
|
@ -464,7 +448,7 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
logger.info(f'Found {len(dlcs)} items of DLC that could be imported.')
|
||||
import_dlc = True
|
||||
if not args.yes and not args.with_dlcs:
|
||||
if not args.get_boolean_choice(f'Do you wish to automatically attempt to import all DLCs?'):
|
||||
if not get_boolean_choice(f'Do you wish to automatically attempt to import all DLCs?'):
|
||||
import_dlc = False
|
||||
|
||||
if import_dlc:
|
||||
|
@ -473,4 +457,43 @@ class LegendaryCLI(LegendaryCLIReal):
|
|||
self.import_game(args)
|
||||
|
||||
logger.info(f'{"DLC" if game.is_dlc else "Game"} "{game.app_title}" has been imported.')
|
||||
return logger
|
||||
return
|
||||
|
||||
def move(self, args):
|
||||
# Override logger for the local context to use message as part of the indirect return value
|
||||
logger = LgndrIndirectLogger(args.indirect_status, self.logger)
|
||||
|
||||
app_name = self._resolve_aliases(args.app_name)
|
||||
igame = self.core.get_installed_game(app_name, skip_sync=True)
|
||||
if not igame:
|
||||
logger.error(f'No installed game found for "{app_name}"')
|
||||
return
|
||||
|
||||
old_base, game_folder = os.path.split(igame.install_path.replace('\\', '/'))
|
||||
new_path = os.path.join(args.new_path, game_folder)
|
||||
logger.info(f'Moving "{game_folder}" from "{old_base}" to "{args.new_path}"')
|
||||
|
||||
if not args.skip_move:
|
||||
try:
|
||||
if not os.path.exists(args.new_path):
|
||||
os.makedirs(args.new_path)
|
||||
|
||||
os.rename(igame.install_path, new_path)
|
||||
except Exception as e:
|
||||
if isinstance(e, OSError) and e.errno == 18:
|
||||
logger.error(f'Moving to a different drive is not supported. Move the folder manually to '
|
||||
f'"{new_path}" and run "legendary move {app_name} "{args.new_path}" --skip-move"')
|
||||
elif isinstance(e, FileExistsError):
|
||||
logger.error(f'The target path already contains a folder called "{game_folder}", '
|
||||
f'please remove or rename it first.')
|
||||
else:
|
||||
logger.error(f'Moving failed with unknown error {e!r}.')
|
||||
logger.info(f'Try moving the folder manually to "{new_path}" and running '
|
||||
f'"legendary move {app_name} "{args.new_path}" --skip-move"')
|
||||
return
|
||||
else:
|
||||
logger.info(f'Not moving, just rewriting legendary metadata...')
|
||||
|
||||
igame.install_path = new_path
|
||||
self.core.install_game(igame)
|
||||
logger.info('Finished.')
|
||||
|
|
|
@ -44,6 +44,7 @@ class LegendaryCore(LegendaryCoreReal):
|
|||
)
|
||||
# lk: monkeypatch run_real (the method that emits the stats) into DLManager
|
||||
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
|
||||
dlm.status_queue = Queue()
|
||||
return dlm, analysis, igame
|
||||
|
||||
def uninstall_game(self, installed_game: InstalledGame, delete_files=True, delete_root_directory=False):
|
||||
|
@ -70,10 +71,10 @@ class LegendaryCore(LegendaryCoreReal):
|
|||
finally:
|
||||
pass
|
||||
|
||||
def prepare_overlay_install(self, path=None, status_q: Queue = None):
|
||||
def prepare_overlay_install(self, path=None):
|
||||
dlm, analysis_result, igame = super(LegendaryCore, self).prepare_overlay_install(path)
|
||||
# lk: monkeypatch status_q (the queue for download stats)
|
||||
dlm.run_real = DLManager.run_real.__get__(dlm, DLManager)
|
||||
dlm.status_queue = status_q
|
||||
dlm.status_queue = Queue()
|
||||
return dlm, analysis_result, igame
|
||||
|
||||
|
|
|
@ -60,11 +60,6 @@ class Ui_ImportGroup(object):
|
|||
self.import_button.setSizePolicy(sizePolicy)
|
||||
self.import_button.setObjectName("import_button")
|
||||
self.button_info_layout.addWidget(self.import_button)
|
||||
self.info_label = QtWidgets.QLabel(ImportGroup)
|
||||
self.info_label.setText("")
|
||||
self.info_label.setWordWrap(True)
|
||||
self.info_label.setObjectName("info_label")
|
||||
self.button_info_layout.addWidget(self.info_label)
|
||||
self.formLayout.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.button_info_layout)
|
||||
|
||||
self.retranslateUi(ImportGroup)
|
||||
|
|
|
@ -96,16 +96,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="info_label">
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
|
|
@ -2,9 +2,10 @@ import os
|
|||
import platform
|
||||
from logging import getLogger
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QCoreApplication, QObject, QRunnable, QStandardPaths
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QRunnable, QStandardPaths
|
||||
from legendary.core import LegendaryCore
|
||||
|
||||
from rare.lgndr.api_monkeys import LgndrIndirectStatus
|
||||
from rare.lgndr.api_arguments import LgndrVerifyGameArgs, LgndrUninstallGameArgs
|
||||
from rare.lgndr.api_exception import LgndrException
|
||||
from rare.shared import LegendaryCLISingleton, LegendaryCoreSingleton
|
||||
|
@ -13,7 +14,7 @@ from rare.utils import config_helper
|
|||
logger = getLogger("Legendary Utils")
|
||||
|
||||
|
||||
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False):
|
||||
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
|
||||
igame = core.get_installed_game(app_name)
|
||||
|
||||
# remove shortcuts link
|
||||
|
@ -38,21 +39,23 @@ def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False):
|
|||
if os.path.exists(start_menu_shortcut):
|
||||
os.remove(start_menu_shortcut)
|
||||
|
||||
result = LegendaryCLISingleton().uninstall_game(
|
||||
status = LgndrIndirectStatus()
|
||||
LegendaryCLISingleton().uninstall_game(
|
||||
LgndrUninstallGameArgs(
|
||||
app_name=app_name,
|
||||
keep_files=keep_files,
|
||||
indirect_status=status,
|
||||
yes=True,
|
||||
)
|
||||
)
|
||||
if not keep_files:
|
||||
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.save_config()
|
||||
|
||||
return result
|
||||
return status.success, status.message
|
||||
|
||||
|
||||
def update_manifest(app_name: str, core: LegendaryCore):
|
||||
|
@ -93,16 +96,18 @@ class VerifyWorker(QRunnable):
|
|||
self.signals.status.emit(self.app_name, num, total, percentage, speed)
|
||||
|
||||
def run(self):
|
||||
status = LgndrIndirectStatus()
|
||||
args = LgndrVerifyGameArgs(app_name=self.app_name,
|
||||
indirect_status=status,
|
||||
verify_stdout=self.status_callback)
|
||||
# TODO: offer this as an alternative when manifest doesn't exist
|
||||
# TODO: requires the client to be online. To do it this way, we need to
|
||||
# TODO: somehow detect the error and offer a dialog in which case `verify_games` is
|
||||
# TODO: re-run with `repair_mode` and `repair_online`
|
||||
result, failed, missing = self.cli.verify_game(
|
||||
result = self.cli.verify_game(
|
||||
args, print_command=False, repair_mode=True, repair_online=True)
|
||||
# success, failed, missing = self.cli.verify_game(args, print_command=False)
|
||||
if result:
|
||||
self.signals.result.emit(self.app_name, not failed and not missing, failed, missing)
|
||||
self.signals.result.emit(self.app_name, not any(result), *result)
|
||||
else:
|
||||
self.signals.error.emit(self.app_name, result.message)
|
||||
self.signals.error.emit(self.app_name, status.message)
|
||||
|
|
|
@ -2,7 +2,7 @@ import os
|
|||
import platform as pf
|
||||
from dataclasses import field, dataclass
|
||||
from multiprocessing import Queue
|
||||
from typing import Union, List, Optional
|
||||
from typing import Union, List, Optional, Callable, Dict
|
||||
|
||||
from legendary.core import LegendaryCore
|
||||
from legendary.downloader.mp.manager import DLManager
|
||||
|
@ -16,22 +16,31 @@ class InstallOptionsModel:
|
|||
base_path: str = ""
|
||||
shared_memory: int = 1024
|
||||
max_workers: int = os.cpu_count() * 2
|
||||
force: bool = False
|
||||
platform: str = "Windows"
|
||||
install_tag: Optional[List[str]] = None
|
||||
order_opt: bool = False
|
||||
repair_mode: bool = False
|
||||
repair_and_update: bool = False
|
||||
no_install: bool = False
|
||||
ignore_space: bool = False
|
||||
force: bool = False
|
||||
sdl_list: list = field(default_factory=lambda: [""])
|
||||
# Rare's internal arguments
|
||||
# FIXME: Do we really need all of these?
|
||||
create_shortcut: bool = True
|
||||
overlay: bool = False
|
||||
update: bool = False
|
||||
silent: bool = False
|
||||
platform: str = ""
|
||||
order_opt: bool = False
|
||||
overlay: bool = False
|
||||
create_shortcut: bool = True
|
||||
install_preqs: bool = pf.system() == "Windows"
|
||||
|
||||
def set_no_install(self, enabled: bool) -> None:
|
||||
self.no_install = enabled
|
||||
def __post_init__(self):
|
||||
self.sdl_prompt: Callable[[str, str], list] = lambda app_name, title: self.install_tag
|
||||
|
||||
def as_install_kwargs(self) -> Dict:
|
||||
return {
|
||||
k: getattr(self, k)
|
||||
for k in self.__dict__
|
||||
if k not in ["update", "silent", "create_shortcut", "overlay", "install_preqs"]
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -47,16 +56,11 @@ class InstallDownloadModel:
|
|||
|
||||
@dataclass
|
||||
class InstallQueueItemModel:
|
||||
status_q: Optional[Queue] = None
|
||||
download: Optional[InstallDownloadModel] = None
|
||||
options: Optional[InstallOptionsModel] = None
|
||||
|
||||
def __bool__(self):
|
||||
return (
|
||||
(self.status_q is not None)
|
||||
and (self.download is not None)
|
||||
and (self.options is not None)
|
||||
)
|
||||
return (self.download is not None) and (self.options is not None)
|
||||
|
||||
|
||||
class PathSpec:
|
||||
|
@ -81,9 +85,7 @@ class PathSpec:
|
|||
|
||||
@property
|
||||
def wine_egl_programdata(self):
|
||||
return self.egl_programdata.replace("\\", "/").replace(
|
||||
"%PROGRAMDATA%", self.wine_programdata
|
||||
)
|
||||
return self.egl_programdata.replace("\\", "/").replace("%PROGRAMDATA%", self.wine_programdata)
|
||||
|
||||
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
|
||||
possible_prefixes = [
|
||||
|
|
Loading…
Reference in a new issue