import os import platform import shutil from logging import getLogger from typing import Optional from PyQt5.QtCore import ( Qt, pyqtSlot, pyqtSignal, ) from PyQt5.QtWidgets import ( QWidget, QMessageBox, ) from rare.models.install import SelectiveDownloadsModel, MoveGameModel from rare.components.dialogs.selective_dialog import SelectiveDialog from rare.models.game import RareGame from rare.shared import RareCore from rare.shared.workers import VerifyWorker, MoveWorker from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo from rare.utils.misc import format_size, icon, style_hyperlink from rare.widgets.image_widget import ImageWidget, ImageSize from rare.widgets.side_tab import SideTabContents from rare.components.dialogs.move_dialog import MoveDialog, is_game_dir logger = getLogger("GameInfo") class GameInfo(QWidget, SideTabContents): # str: app_name import_clicked = pyqtSignal(str) def __init__(self, parent=None): super(GameInfo, self).__init__(parent=parent) self.ui = Ui_GameInfo() self.ui.setupUi(self) # lk: set object names for CSS properties self.ui.install_button.setObjectName("InstallButton") self.ui.modify_button.setObjectName("InstallButton") self.ui.uninstall_button.setObjectName("UninstallButton") self.ui.install_button.setIcon(icon("ri.install-line")) self.ui.import_button.setIcon(icon("mdi.application-import")) self.ui.modify_button.setIcon(icon("fa.gear")) self.ui.verify_button.setIcon(icon("fa.check")) self.ui.repair_button.setIcon(icon("fa.wrench")) self.ui.move_button.setIcon(icon("mdi.folder-move-outline")) self.ui.uninstall_button.setIcon(icon("ri.uninstall-line")) self.rcore = RareCore.instance() self.core = RareCore.instance().core() self.args = RareCore.instance().args() # self.image_manager = RareCore.instance().image_manager() self.rgame: Optional[RareGame] = None self.image = ImageWidget(self) self.image.setFixedSize(ImageSize.Display) self.ui.left_layout.insertWidget(0, self.image, alignment=Qt.AlignTop) self.ui.install_button.clicked.connect(self.__on_install) self.ui.import_button.clicked.connect(self.__on_import) self.ui.modify_button.clicked.connect(self.__on_modify) self.ui.verify_button.clicked.connect(self.__on_verify) self.ui.repair_button.clicked.connect(self.__on_repair) self.ui.move_button.clicked.connect(self.__on_move) self.ui.uninstall_button.clicked.connect(self.__on_uninstall) self.steam_grade_ratings = { "platinum": self.tr("Platinum"), "gold": self.tr("Gold"), "silver": self.tr("Silver"), "bronze": self.tr("Bronze"), "borked": self.tr("Borked"), "fail": self.tr("Failed to get rating"), "pending": self.tr("Loading..."), "na": self.tr("Not applicable"), } # lk: hide unfinished things self.ui.tags_group.setVisible(False) self.ui.requirements_group.setVisible(False) @pyqtSlot() def __on_install(self): if self.rgame.is_non_asset: self.rgame.launch() else: self.rgame.install() @pyqtSlot() def __on_import(self): self.import_clicked.emit(self.rgame.app_name) @pyqtSlot() def __on_uninstall(self): """ This method is to be called from the button only """ self.rgame.uninstall() @pyqtSlot() def __on_modify(self): """ This method is to be called from the button only """ self.rgame.modify() @pyqtSlot() def __on_repair(self): """ This method is to be called from the button only """ repair_file = os.path.join(self.core.lgd.get_tmp_path(), f"{self.rgame.app_name}.repair") if not os.path.exists(repair_file): QMessageBox.warning( self, self.tr("Error - {}").format(self.rgame.app_title), self.tr( "Repair file does not exist or game does not need a repair. Please verify game first" ), ) return self.repair_game(self.rgame) def repair_game(self, rgame: RareGame): rgame.update_game() ans = False if rgame.has_update: ans = QMessageBox.question( self, self.tr("Repair and update? - {}").format(self.rgame.app_title), self.tr( "There is an update for {} from {} to {}. " "Do you want to update the game while repairing it?" ).format(rgame.app_title, rgame.version, rgame.remote_version), ) == QMessageBox.Yes rgame.repair(repair_and_update=ans) @pyqtSlot(RareGame, str) def __on_worker_error(self, rgame: RareGame, message: str): QMessageBox.warning( self, self.tr("Error - {}").format(rgame.app_title), message ) @pyqtSlot() def __on_verify(self): """ This method is to be called from the button only """ if not os.path.exists(self.rgame.igame.install_path): logger.error(f"Installation path {self.rgame.igame.install_path} for {self.rgame.app_title} does not exist") QMessageBox.warning( self, self.tr("Error - {}").format(self.rgame.app_title), self.tr("Installation path for {} does not exist. Cannot continue.").format(self.rgame.app_title), ) return if self.rgame.sdl_name is not None: selective_dialog = SelectiveDialog( self.rgame, parent=self ) selective_dialog.result_ready.connect(self.verify_game) selective_dialog.open() else: self.verify_game(self.rgame) @pyqtSlot(RareGame, SelectiveDownloadsModel) def verify_game(self, rgame: RareGame, sdl_model: SelectiveDownloadsModel = None): if sdl_model is not None: if not sdl_model.accepted or sdl_model.install_tag is None: return self.core.lgd.config.set(rgame.app_name, "install_tags", ','.join(sdl_model.install_tag)) self.core.lgd.save_config() worker = VerifyWorker(self.core, self.args, rgame) worker.signals.progress.connect(self.__on_verify_progress) worker.signals.result.connect(self.__on_verify_result) worker.signals.error.connect(self.__on_worker_error) self.rcore.enqueue_worker(rgame, worker) @pyqtSlot(RareGame, int, int, float, float) def __on_verify_progress(self, rgame: RareGame, num, total, percentage, speed): # lk: the check is NOT REQUIRED because signals are disconnected but protect against it anyway if rgame is not self.rgame: return self.ui.verify_progress.setValue(num * 100 // total) @pyqtSlot(RareGame, bool, int, int) def __on_verify_result(self, rgame: RareGame, success, failed, missing): self.ui.repair_button.setDisabled(success) if success: QMessageBox.information( self, self.tr("Summary - {}").format(rgame.app_title), self.tr("{} has been verified successfully. " "No missing or corrupt files found").format(rgame.app_title), ) else: ans = QMessageBox.question( self, self.tr("Summary - {}").format(rgame.app_title), self.tr( "{} failed verification, {} file(s) corrupted, {} file(s) are missing. " "Do you want to repair them?" ).format(rgame.app_title, failed, missing), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes, ) if ans == QMessageBox.Yes: self.repair_game(rgame) @pyqtSlot() def __on_move(self): """ This method is to be called from the button only """ move_dialog = MoveDialog(self.rgame, parent=self) move_dialog.result_ready.connect(self.move_game) move_dialog.open() def move_game(self, rgame: RareGame, model: MoveGameModel): if not model.accepted: return new_install_path = os.path.join(model.target_path, os.path.basename(self.rgame.install_path)) dir_exists = False if os.path.isdir(new_install_path): dir_exists = is_game_dir(self.rgame.install_path, new_install_path) if not dir_exists: for item in os.listdir(model.target_path): if os.path.basename(self.rgame.install_path) in os.path.basename(item): ans = QMessageBox.question( self, self.tr("Move game? - {}").format(self.rgame.app_title), self.tr( "Destination {} already exists. " "Are you sure you want to overwrite it?" ).format(new_install_path), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes, ) if ans == QMessageBox.Yes: if os.path.isdir(new_install_path): shutil.rmtree(new_install_path) else: os.remove(new_install_path) else: return worker = MoveWorker( self.core, rgame=rgame, dst_path=model.target_path, dst_exists=dir_exists ) worker.signals.progress.connect(self.__on_move_progress) worker.signals.result.connect(self.__on_move_result) worker.signals.error.connect(self.__on_worker_error) self.rcore.enqueue_worker(self.rgame, worker) @pyqtSlot(RareGame, int, object, object) def __on_move_progress(self, rgame: RareGame, progress: int, total_size: int, copied_size: int): # lk: the check is NOT REQUIRED because signals are disconnected but protect against it anyway if rgame is not self.rgame: return self.ui.move_progress.setValue(progress) @pyqtSlot(RareGame, str) def __on_move_result(self, rgame: RareGame, dst_path: str): QMessageBox.information( self, self.tr("Summary - {}").format(rgame.app_title), self.tr("{} successfully moved to {}.").format(rgame.app_title, dst_path), ) @pyqtSlot() def __update_widget(self): """ React to state updates from RareGame """ self.image.setPixmap(self.rgame.get_pixmap(True)) self.ui.lbl_version.setDisabled(self.rgame.is_non_asset) self.ui.version.setDisabled(self.rgame.is_non_asset) self.ui.version.setText( self.rgame.version if not self.rgame.is_non_asset else "N/A" ) self.ui.lbl_install_size.setEnabled(bool(self.rgame.install_size)) self.ui.install_size.setEnabled(bool(self.rgame.install_size)) self.ui.install_size.setText( format_size(self.rgame.install_size) if self.rgame.install_size else "N/A" ) self.ui.lbl_install_path.setEnabled(bool(self.rgame.install_path)) self.ui.install_path.setEnabled(bool(self.rgame.install_path)) self.ui.install_path.setText( self.rgame.install_path if self.rgame.install_path else "N/A" ) self.ui.platform.setText( self.rgame.igame.platform if self.rgame.is_installed and not self.rgame.is_non_asset else self.rgame.default_platform ) self.ui.lbl_grade.setDisabled( self.rgame.is_unreal or platform.system() == "Windows" ) self.ui.grade.setDisabled( self.rgame.is_unreal or platform.system() == "Windows" ) self.ui.grade.setText( style_hyperlink( f"https://www.protondb.com/app/{self.rgame.steam_appid}", self.steam_grade_ratings[self.rgame.steam_grade()] ) ) self.ui.install_button.setEnabled( (not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle ) self.ui.import_button.setEnabled( (not self.rgame.is_installed or self.rgame.is_non_asset) and self.rgame.is_idle ) self.ui.modify_button.setEnabled( self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle and self.rgame.sdl_name is not None ) self.ui.verify_button.setEnabled( self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle ) self.ui.verify_progress.setValue(self.rgame.progress if self.rgame.state == RareGame.State.VERIFYING else 0) if self.rgame.state == RareGame.State.VERIFYING: self.ui.verify_stack.setCurrentWidget(self.ui.verify_progress_page) else: self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page) self.ui.repair_button.setEnabled( self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle and self.rgame.needs_repair and not self.args.offline ) self.ui.move_button.setEnabled( self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle ) self.ui.move_progress.setValue(self.rgame.progress if self.rgame.state == RareGame.State.MOVING else 0) if self.rgame.state == RareGame.State.MOVING: self.ui.move_stack.setCurrentWidget(self.ui.move_progress_page) else: self.ui.move_stack.setCurrentWidget(self.ui.move_button_page) self.ui.uninstall_button.setEnabled( self.rgame.is_installed and (not self.rgame.is_non_asset) and self.rgame.is_idle ) if self.rgame.is_installed and not self.rgame.is_non_asset: self.ui.game_actions_stack.setCurrentWidget(self.ui.installed_page) else: self.ui.game_actions_stack.setCurrentWidget(self.ui.uninstalled_page) @pyqtSlot(RareGame) def update_game(self, rgame: RareGame): if self.rgame is not None: if (worker := self.rgame.worker()) is not None: if isinstance(worker, VerifyWorker): try: worker.signals.progress.disconnect(self.__on_verify_progress) except TypeError as e: logger.warning(f"{self.rgame.app_name} verify worker: {e}") if isinstance(worker, MoveWorker): try: worker.signals.progress.disconnect(self.__on_move_progress) except TypeError as e: logger.warning(f"{self.rgame.app_name} move worker: {e}") self.rgame.signals.widget.update.disconnect(self.__update_widget) self.rgame = None rgame.signals.widget.update.connect(self.__update_widget) if (worker := rgame.worker()) is not None: if isinstance(worker, VerifyWorker): worker.signals.progress.connect(self.__on_verify_progress) if isinstance(worker, MoveWorker): worker.signals.progress.connect(self.__on_move_progress) self.set_title.emit(rgame.app_title) self.ui.app_name.setText(rgame.app_name) self.ui.dev.setText(rgame.developer) if rgame.is_non_asset: self.ui.install_button.setText(self.tr("Link/Launch")) self.ui.game_actions_stack.setCurrentWidget(self.ui.uninstalled_page) else: self.ui.install_button.setText(self.tr("Install")) self.rgame = rgame self.__update_widget()