1
0
Fork 0
mirror of synced 2024-06-29 11:40:37 +12:00

UninstallDialog: Implement it to work similarly to InstallDialog

Similarly to the installation procedure, when an uninstall is
requested, an `UninstallOptionsModel` is emitted by the `RareGame`.
`DownloadsTab` handles the signal and spawns the `UninstallDialog`.
After the `UninstallDialog` is closed, a worker thread handles
uninstalling the application to avoid UI lock-ups when a large
number of files is deleted.

Allows for uninstall actions to be spawned from anything having
access to the `RareGame` instance.

LaunchDialog: Don't check health on DLCs, they always will require
verification if they don't specify an executable.

Signed-off-by: loathingKernel <142770+loathingKernel@users.noreply.github.com>
This commit is contained in:
loathingKernel 2023-01-25 12:59:00 +02:00
parent b02483eb80
commit fc7e45a43a
12 changed files with 260 additions and 179 deletions

View file

@ -10,7 +10,7 @@ from requests.exceptions import ConnectionError, HTTPError
from rare.components.dialogs.login import LoginDialog from rare.components.dialogs.login import LoginDialog
from rare.models.apiresults import ApiResults from rare.models.apiresults import ApiResults
from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton, ImageManagerSingleton from rare.shared import LegendaryCoreSingleton, ArgumentsSingleton, ApiResultsSingleton, ImageManagerSingleton
from rare.shared.game_utils import uninstall_game from rare.shared.workers.uninstall import uninstall_game
from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog from rare.ui.components.dialogs.launch_dialog import Ui_LaunchDialog
from rare.utils.misc import CloudWorker from rare.utils.misc import CloudWorker
from rare.widgets.elide_label import ElideLabel from rare.widgets.elide_label import ElideLabel
@ -92,6 +92,10 @@ class ImageWorker(LaunchWorker):
# FIXME: incorporate installed game status checking here for now, still slow # FIXME: incorporate installed game status checking here for now, still slow
for i, igame in enumerate(igame_list): for i, igame in enumerate(igame_list):
# lk: do not check dlcs, they do not specify an executable
if igame.is_dlc:
continue
self.signals.progress.emit( self.signals.progress.emit(
int(i / len(igame_list) * 25) + 75, int(i / len(igame_list) * 25) + 75,
self.tr("Validating install for <b>{}</b>").format(igame.title) self.tr("Validating install for <b>{}</b>").format(igame.title)
@ -102,6 +106,7 @@ class ImageWorker(LaunchWorker):
uninstall_game(self.core, igame.app_name, keep_files=True) uninstall_game(self.core, igame.app_name, keep_files=True)
logger.info(f"Uninstalled {igame.title}, because no game files exist") logger.info(f"Uninstalled {igame.title}, because no game files exist")
continue continue
# lk: games that don't have an override and can't find their executable due to case sensitivity # lk: games that don't have an override and can't find their executable due to case sensitivity
# lk: will still erroneously require verification. This might need to be removed completely # lk: will still erroneously require verification. This might need to be removed completely
# lk: or be decoupled from the verification requirement # lk: or be decoupled from the verification requirement

View file

@ -1,6 +1,7 @@
from typing import Tuple from typing import Tuple
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QCloseEvent
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QDialog,
QLabel, QLabel,
@ -8,62 +9,70 @@ from PyQt5.QtWidgets import (
QCheckBox, QCheckBox,
QHBoxLayout, QHBoxLayout,
QPushButton, QPushButton,
QApplication,
) )
from legendary.models.game import Game
from legendary.utils.selective_dl import get_sdl_appname from legendary.utils.selective_dl import get_sdl_appname
from rare.models.game import RareGame
from rare.models.install import UninstallOptionsModel
from rare.utils.misc import icon from rare.utils.misc import icon
class UninstallDialog(QDialog): class UninstallDialog(QDialog):
def __init__(self, game: Game): result_ready = pyqtSignal(UninstallOptionsModel)
super(UninstallDialog, self).__init__()
self.setAttribute(Qt.WA_DeleteOnClose, True) def __init__(self, rgame: RareGame, options: UninstallOptionsModel, parent=None):
self.setWindowTitle("Uninstall Game") super(UninstallDialog, self).__init__(parent=parent)
layout = QVBoxLayout() self.setAttribute(Qt.WA_DeleteOnClose, True)
self.info_text = QLabel( self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint)
self.tr("Do you really want to uninstall <b>{}</b>?").format(game.app_title) self.setWindowTitle(f'{QApplication.instance().applicationName()} - Uninstall "{rgame.app_title}"')
) self.info_text = QLabel(
layout.addWidget(self.info_text) self.tr("Do you really want to uninstall <b>{}</b>?").format(rgame.app_title)
self.keep_files = QCheckBox(self.tr("Keep game files.")) )
self.keep_config = QCheckBox(self.tr("Keep game configuation."))
form_layout = QVBoxLayout() self.keep_files = QCheckBox(self.tr("Keep game files."))
form_layout.setContentsMargins(6, 6, 0, 6) self.keep_files.setChecked(bool(options.keep_files))
form_layout.addWidget(self.keep_files) self.keep_config = QCheckBox(self.tr("Keep game configuation."))
form_layout.addWidget(self.keep_config) self.keep_config.setChecked(bool(options.keep_config))
layout.addLayout(form_layout)
button_layout = QHBoxLayout()
self.ok_button = QPushButton( self.ok_button = QPushButton(
icon("ei.remove-circle", color="red"), self.tr("Uninstall") icon("ei.remove-circle", color="red"), self.tr("Uninstall")
) )
self.ok_button.clicked.connect(self.ok) self.ok_button.clicked.connect(self.__on_uninstall)
self.cancel_button = QPushButton(self.tr("Cancel")) self.cancel_button = QPushButton(self.tr("Cancel"))
self.cancel_button.clicked.connect(self.cancel) self.cancel_button.clicked.connect(self.__on_cancel)
form_layout = QVBoxLayout()
form_layout.setContentsMargins(-1, -1, 0, -1)
form_layout.addWidget(self.keep_files)
form_layout.addWidget(self.keep_config)
button_layout = QHBoxLayout()
button_layout.addWidget(self.ok_button) button_layout.addWidget(self.ok_button)
button_layout.addStretch(1) button_layout.addStretch(1)
button_layout.addWidget(self.cancel_button) button_layout.addWidget(self.cancel_button)
layout = QVBoxLayout()
layout.addWidget(self.info_text)
layout.addLayout(form_layout)
layout.addLayout(button_layout) layout.addLayout(button_layout)
self.setLayout(layout) self.setLayout(layout)
if get_sdl_appname(game.app_name) is not None: if get_sdl_appname(rgame.app_name) is not None:
self.keep_config.setChecked(True) self.keep_config.setChecked(True)
self.options: Tuple[bool, bool, bool] = ( self.options: UninstallOptionsModel = options
False, self.keep_files.isChecked(), self.keep_config.isChecked()
)
def get_options(self) -> Tuple[bool, bool, bool]: def closeEvent(self, a0: QCloseEvent) -> None:
self.exec_() self.result_ready.emit(self.options)
return self.options super(UninstallDialog, self).closeEvent(a0)
def ok(self): def __on_uninstall(self):
self.options = (True, self.keep_files.isChecked(), self.keep_config.isChecked()) self.options.values = (True, self.keep_files.isChecked(), self.keep_config.isChecked())
self.close() self.close()
def cancel(self): def __on_cancel(self):
self.options = (False, False, False) self.options.values = (None, None, None)
self.close() self.close()

View file

@ -164,7 +164,7 @@ class MainWindow(QMainWindow):
defaultButton=QMessageBox.No, defaultButton=QMessageBox.No,
) )
if reply == QMessageBox.Yes: if reply == QMessageBox.Yes:
self.tab_widget.downloads_tab.stop_download(on_exit=True) self.tab_widget.downloads_tab.stop_download(omit_queue=True)
else: else:
e.ignore() e.ignore()
return return

View file

@ -10,10 +10,12 @@ from PyQt5.QtWidgets import (
) )
from rare.components.dialogs.install_dialog import InstallDialog, InstallInfoWorker from rare.components.dialogs.install_dialog import InstallDialog, InstallInfoWorker
from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.lgndr.models.downloading import UIUpdate from rare.lgndr.models.downloading import UIUpdate
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.models.install import InstallOptionsModel, InstallQueueItemModel from rare.models.install import InstallOptionsModel, InstallQueueItemModel, UninstallOptionsModel
from rare.shared import RareCore, LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton from rare.shared import RareCore
from rare.shared.workers.uninstall import UninstallWorker
from rare.ui.components.tabs.downloads.downloads_tab import Ui_DownloadsTab from rare.ui.components.tabs.downloads.downloads_tab import Ui_DownloadsTab
from rare.utils.misc import get_size, create_desktop_link from rare.utils.misc import get_size, create_desktop_link
from .groups import UpdateGroup, QueueGroup from .groups import UpdateGroup, QueueGroup
@ -36,13 +38,12 @@ class DownloadsTab(QWidget):
self.ui = Ui_DownloadsTab() self.ui = Ui_DownloadsTab()
self.ui.setupUi(self) self.ui.setupUi(self)
self.rcore = RareCore.instance() self.rcore = RareCore.instance()
self.core = LegendaryCoreSingleton() self.core = self.rcore.core()
self.signals = GlobalSignalsSingleton() self.signals = self.rcore.signals()
self.args = ArgumentsSingleton() self.args = self.rcore.args()
self.thread: Optional[DlThread] = None self.thread: Optional[DlThread] = None
self.threadpool = QThreadPool(self) self.threadpool = QThreadPool.globalInstance()
self.threadpool.setMaxThreadCount(1)
self.ui.kill_button.clicked.connect(self.stop_download) self.ui.kill_button.clicked.connect(self.stop_download)
@ -59,18 +60,21 @@ class DownloadsTab(QWidget):
self.__check_updates() self.__check_updates()
self.signals.game.install.connect(self.__get_install_options) self.signals.game.install.connect(self.__get_install_options)
self.signals.game.uninstall.connect(self.__get_uninstall_options)
self.signals.download.enqueue.connect(self.__add_update) self.signals.download.enqueue.connect(self.__add_update)
self.signals.download.dequeue.connect(self.__on_game_uninstalled) self.signals.download.dequeue.connect(self.__remove_update)
self.__reset_download() self.__reset_download()
self.forced_item: Optional[InstallQueueItemModel] = None self.__forced_item: Optional[InstallQueueItemModel] = None
self.on_exit = False self.__omit_queue = False
def __check_updates(self): def __check_updates(self):
for rgame in self.rcore.updates: for rgame in self.rcore.updates:
self.__add_update(rgame) self.__add_update(rgame)
@pyqtSlot(str)
@pyqtSlot(RareGame)
def __add_update(self, update: Union[str,RareGame]): def __add_update(self, update: Union[str,RareGame]):
if isinstance(update, str): if isinstance(update, str):
update = self.rcore.get_game(update) update = self.rcore.get_game(update)
@ -80,6 +84,17 @@ class DownloadsTab(QWidget):
) )
else: else:
self.updates_group.append(update.game, update.igame) self.updates_group.append(update.game, update.igame)
self.update_title.emit(self.queues_count())
@pyqtSlot(str)
def __remove_update(self, app_name):
if self.thread and self.thread.item.options.app_name == app_name:
self.stop_download(omit_queue=True)
if self.queue_group.contains(app_name):
self.queue_group.remove(app_name)
if self.updates_group.contains(app_name):
self.updates_group.remove(app_name)
self.update_title.emit(self.queues_count())
@pyqtSlot(str) @pyqtSlot(str)
def __on_queue_removed(self, app_name: str): def __on_queue_removed(self, app_name: str):
@ -100,32 +115,27 @@ class DownloadsTab(QWidget):
def __on_queue_force(self, item: InstallQueueItemModel): def __on_queue_force(self, item: InstallQueueItemModel):
if self.thread: if self.thread:
self.stop_download() self.stop_download()
self.forced_item = item self.__forced_item = item
else: else:
self.__start_installation(item) self.__start_installation(item)
@pyqtSlot(str) def stop_download(self, omit_queue=False):
def __on_game_uninstalled(self, app_name): """
if self.thread and self.thread.item.options.app_name == app_name: Stops the active download, by optionally skipping the queue
self.stop_download()
if self.queue_group.contains(app_name): :param omit_queue: bool
self.queue_group.remove(app_name) If `True`, the stopped download won't be added back to the queue.
Defaults to `False`
if self.updates_group.contains(app_name): :return:
self.updates_group.remove(app_name) """
self.update_title.emit(self.queues_count())
def stop_download(self, on_exit=False):
self.thread.kill() self.thread.kill()
self.ui.kill_button.setEnabled(False) self.ui.kill_button.setEnabled(False)
# lk: if we are exitin Rare, waif for thread to finish # lk: if we are exitin Rare, waif for thread to finish
# `self.on_exit` control whether we try to add the download # `self.on_exit` control whether we try to add the download
# back in the queue. If we are on exit we wait for the thread # back in the queue. If we are on exit we wait for the thread
# to finish, we do not care about handling the result really # to finish, we do not care about handling the result really
if on_exit: self.__omit_queue = omit_queue
self.on_exit = on_exit if omit_queue:
self.thread.wait() self.thread.wait()
def __start_installation(self, item: InstallQueueItemModel): def __start_installation(self, item: InstallQueueItemModel):
@ -141,6 +151,22 @@ class DownloadsTab(QWidget):
def queues_count(self) -> int: def queues_count(self) -> int:
return self.updates_group.count() + self.queue_group.count() return self.updates_group.count() + self.queue_group.count()
@property
def is_download_active(self):
return self.thread is not None
@pyqtSlot(UIUpdate, c_ulonglong)
def __on_download_progress(self, ui_update: UIUpdate, dl_size: c_ulonglong):
self.ui.progress_bar.setValue(int(ui_update.progress))
self.ui.dl_speed.setText(f"{get_size(ui_update.download_compressed_speed)}/s")
self.ui.cache_used.setText(
f"{get_size(ui_update.cache_usage) if ui_update.cache_usage > 1023 else '0KB'}"
)
self.ui.downloaded.setText(
f"{get_size(ui_update.total_downloaded)} / {get_size(dl_size.value)}"
)
self.ui.time_left.setText(get_time(ui_update.estimated_time_left))
@pyqtSlot(InstallQueueItemModel) @pyqtSlot(InstallQueueItemModel)
def __on_info_worker_result(self, item: InstallQueueItemModel): def __on_info_worker_result(self, item: InstallQueueItemModel):
rgame = self.rcore.get_game(item.options.app_name) rgame = self.rcore.get_game(item.options.app_name)
@ -171,7 +197,7 @@ class DownloadsTab(QWidget):
if result.item.options.overlay: if result.item.options.overlay:
self.signals.application.overlay_installed.emit() self.signals.application.overlay_installed.emit()
else: else:
self.signals.application.notify.emit(result.item.download.game.app_name) self.signals.application.notify.emit(result.item.download.game.app_title)
if self.updates_group.contains(result.item.options.app_name): if self.updates_group.contains(result.item.options.app_name):
self.updates_group.set_widget_enabled(result.item.options.app_name, True) self.updates_group.set_widget_enabled(result.item.options.app_name, True)
@ -182,12 +208,12 @@ class DownloadsTab(QWidget):
elif result.code == DlResultCode.STOPPED: elif result.code == DlResultCode.STOPPED:
logger.info(f"Download stopped: {result.item.download.game.app_title}") logger.info(f"Download stopped: {result.item.download.game.app_title}")
if not self.on_exit: if not self.__omit_queue:
info_worker = InstallInfoWorker(self.core, result.item.options) worker = InstallInfoWorker(self.core, result.item.options)
info_worker.signals.result.connect(self.__on_info_worker_result) worker.signals.result.connect(self.__on_info_worker_result)
info_worker.signals.failed.connect(self.__on_info_worker_failed) worker.signals.failed.connect(self.__on_info_worker_failed)
info_worker.signals.finished.connect(self.__on_info_worker_finished) worker.signals.finished.connect(self.__on_info_worker_finished)
self.threadpool.start(info_worker) self.threadpool.start(worker)
else: else:
return return
@ -199,9 +225,9 @@ class DownloadsTab(QWidget):
if result.code == DlResultCode.FINISHED and self.queue_group.count(): if result.code == DlResultCode.FINISHED and self.queue_group.count():
self.__start_installation(self.queue_group.pop_front()) self.__start_installation(self.queue_group.pop_front())
elif result.code == DlResultCode.STOPPED and self.forced_item: elif result.code == DlResultCode.STOPPED and self.__forced_item:
self.__start_installation(self.forced_item) self.__start_installation(self.__forced_item)
self.forced_item = None self.__forced_item = None
else: else:
self.__reset_download() self.__reset_download()
@ -215,17 +241,15 @@ class DownloadsTab(QWidget):
self.ui.downloaded.setText("n/a") self.ui.downloaded.setText("n/a")
self.thread = None self.thread = None
@pyqtSlot(UIUpdate, c_ulonglong) @pyqtSlot(InstallOptionsModel)
def __on_download_progress(self, ui_update: UIUpdate, dl_size: c_ulonglong): def __get_install_options(self, options: InstallOptionsModel):
self.ui.progress_bar.setValue(int(ui_update.progress)) install_dialog = InstallDialog(
self.ui.dl_speed.setText(f"{get_size(ui_update.download_compressed_speed)}/s") self.rcore.get_game(options.app_name),
self.ui.cache_used.setText( options=options,
f"{get_size(ui_update.cache_usage) if ui_update.cache_usage > 1023 else '0KB'}" parent=self,
) )
self.ui.downloaded.setText( install_dialog.result_ready.connect(self.__on_install_dialog_closed)
f"{get_size(ui_update.total_downloaded)} / {get_size(dl_size.value)}" install_dialog.execute()
)
self.ui.time_left.setText(get_time(ui_update.estimated_time_left))
@pyqtSlot(InstallQueueItemModel) @pyqtSlot(InstallQueueItemModel)
def __on_install_dialog_closed(self, item: InstallQueueItemModel): def __on_install_dialog_closed(self, item: InstallQueueItemModel):
@ -249,17 +273,26 @@ class DownloadsTab(QWidget):
if self.updates_group.contains(item.options.app_name): if self.updates_group.contains(item.options.app_name):
self.updates_group.set_widget_enabled(item.options.app_name, True) self.updates_group.set_widget_enabled(item.options.app_name, True)
@pyqtSlot(UninstallOptionsModel)
@pyqtSlot(InstallOptionsModel) def __get_uninstall_options(self, options: UninstallOptionsModel):
def __get_install_options(self, options: InstallOptionsModel): uninstall_dialog = UninstallDialog(
install_dialog = InstallDialog(
self.rcore.get_game(options.app_name), self.rcore.get_game(options.app_name),
options=options, options=options,
parent=self, parent=self,
) )
install_dialog.result_ready.connect(self.__on_install_dialog_closed) uninstall_dialog.result_ready.connect(self.__on_uninstall_dialog_closed)
install_dialog.execute() uninstall_dialog.exec()
@property @pyqtSlot(UninstallOptionsModel)
def is_download_active(self): def __on_uninstall_dialog_closed(self, options: UninstallOptionsModel):
return self.thread is not None if options and options.uninstall:
self.__remove_update(options.app_name)
worker = UninstallWorker(self.core, self.rcore.get_game(options.app_name), options)
worker.signals.result.connect(self.__on_uninstall_worker_result)
self.threadpool.start(worker)
@pyqtSlot(RareGame, bool, str)
def __on_uninstall_worker_result(self, rgame: RareGame, success: bool, message: str):
if not success:
QMessageBox.warning(None, self.tr("Uninstall - {}").format(rgame.title), message, QMessageBox.Close)
rgame.set_installed(False)

View file

@ -117,9 +117,9 @@ class GamesTab(QStackedWidget):
# signals # signals
self.signals.game.installed.connect(self.update_count_games_label) self.signals.game.installed.connect(self.update_count_games_label)
# self.signals.game.installed.connect(self.library_controller.update_list) self.signals.game.installed.connect(self.library_controller.update_list)
self.signals.game.uninstalled.connect(self.update_count_games_label) self.signals.game.uninstalled.connect(self.update_count_games_label)
# self.signals.game.uninstalled.connect(self.library_controller.update_list) self.signals.game.uninstalled.connect(self.library_controller.update_list)
# self.signals.game.uninstalled.connect(lambda x: self.setCurrentWidget(self.games)) # self.signals.game.uninstalled.connect(lambda x: self.setCurrentWidget(self.games))
start_t = time.time() start_t = time.time()

View file

@ -11,7 +11,6 @@ from PyQt5.QtCore import (
pyqtSlot, pyqtSlot,
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QHBoxLayout,
QMenu, QMenu,
QPushButton, QPushButton,
QWidget, QWidget,
@ -19,7 +18,6 @@ from PyQt5.QtWidgets import (
QWidgetAction, QWidgetAction,
) )
from rare.components.dialogs.uninstall_dialog import UninstallDialog
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.shared import ( from rare.shared import (
RareCore, RareCore,
@ -28,7 +26,6 @@ from rare.shared import (
ArgumentsSingleton, ArgumentsSingleton,
ImageManagerSingleton, ImageManagerSingleton,
) )
from rare.shared.game_utils import uninstall_game
from rare.shared.image_manager import ImageSize from rare.shared.image_manager import ImageSize
from rare.shared.workers.verify import VerifyWorker from rare.shared.workers.verify import VerifyWorker
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
@ -107,35 +104,7 @@ class GameInfo(QWidget):
@pyqtSlot() @pyqtSlot()
def __on_uninstall(self): def __on_uninstall(self):
""" This function is to be called from the button only """ """ This function is to be called from the button only """
self.uninstall_game(self.rgame) self.rgame.uninstall()
def uninstall_game(self, rgame: RareGame) -> bool:
# returns if uninstalled
if not os.path.exists(rgame.igame.install_path):
if QMessageBox.Yes == QMessageBox.question(
None,
self.tr("Uninstall - {}").format(rgame.igame.title),
self.tr(
"Game files of {} do not exist. Remove it from installed games?"
).format(rgame.igame.title),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
):
self.core.lgd.remove_installed_game(rgame.app_name)
rgame.set_installed(False)
return True
else:
return False
proceed, keep_files, keep_config = UninstallDialog(rgame.game).get_options()
if not proceed:
return False
success, message = uninstall_game(self.core, rgame.app_name, keep_files, keep_config)
if not success:
QMessageBox.warning(None, self.tr("Uninstall - {}").format(rgame.title), message, QMessageBox.Close)
rgame.set_installed(False)
self.signals.download.dequeue.emit(rgame.app_name)
return True
@pyqtSlot() @pyqtSlot()
def __on_repair(self): def __on_repair(self):

View file

@ -11,7 +11,7 @@ from PyQt5.QtGui import QPixmap
from legendary.models.game import Game, InstalledGame, SaveGameFile from legendary.models.game import Game, InstalledGame, SaveGameFile
from rare.lgndr.core import LegendaryCore from rare.lgndr.core import LegendaryCore
from rare.models.install import InstallOptionsModel from rare.models.install import InstallOptionsModel, UninstallOptionsModel
from rare.shared.game_process import GameProcess from rare.shared.game_process import GameProcess
from rare.shared.image_manager import ImageManager from rare.shared.image_manager import ImageManager
from rare.utils.misc import get_rare_executable from rare.utils.misc import get_rare_executable
@ -28,6 +28,7 @@ class RareGame(QObject):
DOWNLOADING = 2 DOWNLOADING = 2
VERIFYING = 3 VERIFYING = 3
MOVING = 4 MOVING = 4
UNINSTALLING = 5
@dataclass @dataclass
class Metadata: class Metadata:
@ -74,6 +75,7 @@ class RareGame(QObject):
class Game(QObject): class Game(QObject):
install = pyqtSignal(InstallOptionsModel) install = pyqtSignal(InstallOptionsModel)
installed = pyqtSignal(str) installed = pyqtSignal(str)
uninstall = pyqtSignal(UninstallOptionsModel)
uninstalled = pyqtSignal(str) uninstalled = pyqtSignal(str)
launched = pyqtSignal(str) launched = pyqtSignal(str)
finished = pyqtSignal(str) finished = pyqtSignal(str)
@ -475,6 +477,11 @@ class RareGame(QObject):
) )
) )
def uninstall(self):
self.signals.game.uninstall.emit(
UninstallOptionsModel(app_name=self.app_name)
)
def launch( def launch(
self, self,
offline: bool = False, offline: bool = False,

View file

@ -1,7 +1,7 @@
import os import os
import platform as pf import platform as pf
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Callable, Dict from typing import List, Optional, Callable, Dict, Tuple
from legendary.models.downloading import AnalysisResult, ConditionCheckResult from legendary.models.downloading import AnalysisResult, ConditionCheckResult
from legendary.models.game import Game, InstalledGame from legendary.models.game import Game, InstalledGame
@ -63,3 +63,29 @@ class InstallQueueItemModel:
def __bool__(self): def __bool__(self):
return (self.download is not None) and (self.options is not None) return (self.download is not None) and (self.options is not None)
@dataclass
class UninstallOptionsModel:
app_name: str
uninstall: bool = None
keep_files: bool = None
keep_config: bool = None
def __bool__(self):
return (
bool(self.app_name)
and (self.uninstall is not None)
and (self.keep_files is not None)
and (self.keep_config is not None)
)
@property
def values(self) -> Tuple[bool, bool, bool]:
return self.uninstall, self.keep_config, self.keep_files
@values.setter
def values(self, values: Tuple[bool, bool, bool]):
self.uninstall = values[0]
self.keep_files = values[1]
self.keep_config = values[2]

View file

@ -1,6 +1,6 @@
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
from .install import InstallOptionsModel from .install import InstallOptionsModel, UninstallOptionsModel
class GlobalSignals: class GlobalSignals:
@ -22,6 +22,7 @@ class GlobalSignals:
class GameSignals(QObject): class GameSignals(QObject):
install = pyqtSignal(InstallOptionsModel) install = pyqtSignal(InstallOptionsModel)
uninstall = pyqtSignal(UninstallOptionsModel)
# str: app_name # str: app_name
installed = pyqtSignal(str) installed = pyqtSignal(str)
# str: app_name # str: app_name

View file

@ -1,68 +1,16 @@
import os
import platform
from logging import getLogger from logging import getLogger
from PyQt5.QtCore import QObject, pyqtSignal, QUrl, pyqtSlot from PyQt5.QtCore import QObject, pyqtSignal, QUrl, pyqtSlot
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMessageBox, QPushButton from PyQt5.QtWidgets import QMessageBox, QPushButton
from legendary.core import LegendaryCore
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.glue.arguments import LgndrUninstallGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame from rare.models.game import RareGame
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton, ArgumentsSingleton
from rare.utils import config_helper
from .cloud_save_utils import CloudSaveUtils from .cloud_save_utils import CloudSaveUtils
logger = getLogger("GameUtils") logger = getLogger("GameUtils")
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
igame = core.get_installed_game(app_name)
# remove shortcuts link
desktop = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation)
applications = QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation)
if platform.system() == "Linux":
desktop_shortcut = os.path.join(desktop, f"{igame.title}.desktop")
if os.path.exists(desktop_shortcut):
os.remove(desktop_shortcut)
applications_shortcut = os.path.join(applications, f"{igame.title}.desktop")
if os.path.exists(applications_shortcut):
os.remove(applications_shortcut)
elif platform.system() == "Windows":
game_title = igame.title.split(":")[0]
desktop_shortcut = os.path.join(desktop, f"{game_title}.lnk")
if os.path.exists(desktop_shortcut):
os.remove(desktop_shortcut)
start_menu_shortcut = os.path.join(applications, "..", f"{game_title}.lnk")
if os.path.exists(start_menu_shortcut):
os.remove(start_menu_shortcut)
status = LgndrIndirectStatus()
LegendaryCLI(core).uninstall_game(
LgndrUninstallGameArgs(
app_name=app_name,
keep_files=keep_files,
indirect_status=status,
yes=True,
)
)
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 status.success, status.message
class GameUtils(QObject): class GameUtils(QObject):
finished = pyqtSignal(str, str) # app_name, error finished = pyqtSignal(str, str) # app_name, error
cloud_save_finished = pyqtSignal(str) cloud_save_finished = pyqtSignal(str)

View file

@ -146,6 +146,7 @@ class RareCore(QObject):
def add_game(self, rgame: RareGame) -> None: def add_game(self, rgame: RareGame) -> None:
rgame.signals.game.install.connect(self.__signals.game.install) rgame.signals.game.install.connect(self.__signals.game.install)
rgame.signals.game.uninstall.connect(self.__signals.game.uninstall)
rgame.signals.game.installed.connect(self.__signals.game.installed) rgame.signals.game.installed.connect(self.__signals.game.installed)
rgame.signals.game.uninstalled.connect(self.__signals.game.uninstalled) rgame.signals.game.uninstalled.connect(self.__signals.game.uninstalled)
rgame.signals.game.finished.connect(self.__signals.application.update_tray) rgame.signals.game.finished.connect(self.__signals.application.update_tray)

View file

@ -0,0 +1,82 @@
import os
import platform
import sys
from logging import getLogger
from PyQt5.QtCore import QStandardPaths, QRunnable, QObject, pyqtSignal
from legendary.core import LegendaryCore
from rare.lgndr.cli import LegendaryCLI
from rare.lgndr.glue.arguments import LgndrUninstallGameArgs
from rare.lgndr.glue.monkeys import LgndrIndirectStatus
from rare.models.game import RareGame
from rare.models.install import UninstallOptionsModel
from rare.utils import config_helper
logger = getLogger("UninstallWorker")
def uninstall_game(core: LegendaryCore, app_name: str, keep_files=False, keep_config=False):
igame = core.get_installed_game(app_name)
# remove shortcuts link
desktop = QStandardPaths.writableLocation(QStandardPaths.DesktopLocation)
applications = QStandardPaths.writableLocation(QStandardPaths.ApplicationsLocation)
if platform.system() == "Linux":
desktop_shortcut = os.path.join(desktop, f"{igame.title}.desktop")
if os.path.exists(desktop_shortcut):
os.remove(desktop_shortcut)
applications_shortcut = os.path.join(applications, f"{igame.title}.desktop")
if os.path.exists(applications_shortcut):
os.remove(applications_shortcut)
elif platform.system() == "Windows":
game_title = igame.title.split(":")[0]
desktop_shortcut = os.path.join(desktop, f"{game_title}.lnk")
if os.path.exists(desktop_shortcut):
os.remove(desktop_shortcut)
start_menu_shortcut = os.path.join(applications, "..", f"{game_title}.lnk")
if os.path.exists(start_menu_shortcut):
os.remove(start_menu_shortcut)
status = LgndrIndirectStatus()
LegendaryCLI(core).uninstall_game(
LgndrUninstallGameArgs(
app_name=app_name,
keep_files=keep_files,
indirect_status=status,
yes=True,
)
)
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 status.success, status.message
class UninstallWorker(QRunnable):
class Signals(QObject):
result = pyqtSignal(RareGame, bool, str)
def __init__(self, core: LegendaryCore, rgame: RareGame, options: UninstallOptionsModel):
sys.excepthook = sys.__excepthook__
super(UninstallWorker, self).__init__()
self.signals = UninstallWorker.Signals()
self.setAutoDelete(True)
self.core = core
self.rgame = rgame
self.options = options
def run(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.rgame.state = RareGame.State.IDLE
self.signals.result.emit(self.rgame, success, message)