From 6c0663771cec9c1f49b648469a15333b63ee054c Mon Sep 17 00:00:00 2001
From: loathingKernel <142770+loathingKernel@users.noreply.github.com>
Date: Wed, 15 Feb 2023 16:59:33 +0200
Subject: [PATCH] GameInfo: Make widgets react to changes from RareGame's
`widget.update` signal
Note: the `__update_widget()` method, while it doesn't have any visible delay
has the potential for improvement. I didn't do it because it felt like
premature optimization.
MoveGamePopUp: update it to use RareGame and it's signals
RareGame: Add `install_path` attribute and change `needs_verification` setter
Now both setters will update the local `igame` attribute and save it too.
MoveWorker: Update it to use RareGame.
Other changes include moving "same-drive" moving into the worker and using
`os.path` methods instead of `PathLib`
SteamGrades: Remove worker, it is implemented in RareGame now.
---
.../tabs/games/game_info/game_info.py | 384 ++++++++----------
.../tabs/games/game_info/move_game.py | 151 +++----
rare/models/game.py | 23 +-
rare/shared/workers/move.py | 162 ++++----
rare/utils/steam_grades.py | 28 --
5 files changed, 350 insertions(+), 398 deletions(-)
diff --git a/rare/components/tabs/games/game_info/game_info.py b/rare/components/tabs/games/game_info/game_info.py
index 2955801c..99fcb5fb 100644
--- a/rare/components/tabs/games/game_info/game_info.py
+++ b/rare/components/tabs/games/game_info/game_info.py
@@ -1,36 +1,27 @@
import os
import platform
import shutil
+from ctypes import c_uint64
from logging import getLogger
-from pathlib import Path
-from typing import Optional, Union
+from typing import Optional
from PyQt5.QtCore import (
Qt,
- QThreadPool,
pyqtSlot,
)
from PyQt5.QtWidgets import (
QMenu,
- QPushButton,
QWidget,
QMessageBox,
QWidgetAction,
)
from rare.models.game import RareGame
-from rare.shared import (
- RareCore,
- LegendaryCoreSingleton,
- GlobalSignalsSingleton,
- ArgumentsSingleton,
- ImageManagerSingleton,
-)
+from rare.shared import RareCore
from rare.shared.image_manager import ImageSize
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 get_size
-from rare.utils.steam_grades import SteamWorker
from rare.widgets.image_widget import ImageWidget
from .move_game import MoveGamePopUp, is_game_dir
@@ -47,10 +38,9 @@ class GameInfo(QWidget):
self.ui.uninstall_button.setObjectName("UninstallButton")
self.rcore = RareCore.instance()
- self.core = LegendaryCoreSingleton()
- self.signals = GlobalSignalsSingleton()
- self.args = ArgumentsSingleton()
- self.image_manager = ImageManagerSingleton()
+ self.core = RareCore.instance().core()
+ self.args = RareCore.instance().args()
+ # self.image_manager = RareCore.instance().image_manager()
self.rgame: Optional[RareGame] = None
@@ -58,42 +48,35 @@ class GameInfo(QWidget):
self.image.setFixedSize(ImageSize.Display)
self.ui.layout_game_info.insertWidget(0, self.image, alignment=Qt.AlignTop)
- if platform.system() == "Windows":
- self.ui.lbl_grade.setVisible(False)
- self.ui.grade.setVisible(False)
-
- self.ui.game_actions_stack.setCurrentWidget(self.ui.installed_page)
-
- self.ui.uninstall_button.clicked.connect(self.__on_uninstall)
- self.ui.verify_button.clicked.connect(self.__on_verify)
-
- self.verify_pool = QThreadPool()
- self.verify_pool.setMaxThreadCount(2)
- if self.args.offline:
- self.ui.repair_button.setDisabled(True)
- else:
- self.ui.repair_button.clicked.connect(self.__on_repair)
-
self.ui.install_button.clicked.connect(self.__on_install)
+ self.ui.verify_button.clicked.connect(self.__on_verify)
+ self.ui.repair_button.clicked.connect(self.__on_repair)
+ self.ui.uninstall_button.clicked.connect(self.__on_uninstall)
- self.move_game_pop_up = MoveGamePopUp()
- self.move_action = QWidgetAction(self)
- self.move_action.setDefaultWidget(self.move_game_pop_up)
- self.ui.move_button.setMenu(QMenu())
- self.ui.move_button.menu().addAction(self.move_action)
+ self.move_game_pop_up = MoveGamePopUp(self)
+ move_action = QWidgetAction(self)
+ move_action.setDefaultWidget(self.move_game_pop_up)
+ self.ui.move_button.setMenu(QMenu(self.ui.move_button))
+ self.ui.move_button.menu().addAction(move_action)
- self.existing_game_dir = False
- self.is_moving = False
- self.game_moving = None
- self.dest_path_with_suffix = None
-
- self.move_game_pop_up.browse_done.connect(self.show_menu_after_browse)
+ self.move_game_pop_up.browse_done.connect(self.ui.move_button.showMenu)
self.move_game_pop_up.move_clicked.connect(self.ui.move_button.menu().close)
- self.move_game_pop_up.move_clicked.connect(self.move_game)
+ self.move_game_pop_up.move_clicked.connect(self.__on_move)
+
+ 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"),
+ }
@pyqtSlot()
def __on_install(self):
- if self.rgame.is_origin:
+ if self.rgame.is_non_asset:
self.rgame.launch()
else:
self.rgame.install()
@@ -101,12 +84,12 @@ class GameInfo(QWidget):
# FIXME: Move to RareGame
@pyqtSlot()
def __on_uninstall(self):
- """ This function is to be called from the button only """
+ """ This method is to be called from the button only """
self.rgame.uninstall()
@pyqtSlot()
def __on_repair(self):
- """ This function is to be called from the button only """
+ """ 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(
@@ -125,7 +108,7 @@ class GameInfo(QWidget):
if rgame.has_update:
ans = QMessageBox.question(
self,
- self.tr("Repair and update?"),
+ self.tr("Repair and update? - {}").format(self.rgame.title),
self.tr(
"There is an update for {} from {} to {}. "
"Do you want to update the game while repairing it?"
@@ -133,9 +116,17 @@ class GameInfo(QWidget):
) == 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.title),
+ message
+ )
+
@pyqtSlot()
def __on_verify(self):
- """ This function is to be called from the button only """
+ """ 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.title} does not exist")
QMessageBox.warning(
@@ -150,35 +141,18 @@ class GameInfo(QWidget):
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_verify_error)
- self.ui.verify_stack.setCurrentWidget(self.ui.verify_progress_page)
- self.ui.verify_progress.setValue(0)
- self.ui.move_button.setEnabled(False)
+ worker.signals.error.connect(self.__on_worker_error)
self.rcore.enqueue_worker(rgame, worker)
- def verify_cleanup(self, rgame: RareGame):
- if rgame is not self.rgame:
- return
- self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page)
- self.ui.move_button.setEnabled(True)
- self.ui.verify_button.setEnabled(True)
-
- @pyqtSlot(RareGame, str)
- def __on_verify_error(self, rgame: RareGame, message):
- self.verify_cleanup(rgame)
- QMessageBox.warning(
- self,
- self.tr("Error - {}").format(rgame.title),
- message
- )
-
@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.verify_cleanup(rgame)
self.ui.repair_button.setDisabled(success)
if success:
QMessageBox.information(
@@ -202,201 +176,173 @@ class GameInfo(QWidget):
self.repair_game(rgame)
@pyqtSlot(str)
- def move_game(self, dest_path):
- dest_path = Path(dest_path)
- install_path = Path(self.rgame.igame.install_path)
- self.dest_path_with_suffix = dest_path.joinpath(install_path.stem)
+ def __on_move(self, dst_path: str):
+ """ This method is to be called from the button only """
+ new_install_path = os.path.join(dst_path, os.path.basename(self.rgame.install_path))
- if self.dest_path_with_suffix.is_dir():
- self.existing_game_dir = is_game_dir(install_path, self.dest_path_with_suffix)
+ dir_exists = False
+ if os.path.isdir(new_install_path):
+ dir_exists = is_game_dir(self.rgame.install_path, new_install_path)
- if not self.existing_game_dir:
- for i in dest_path.iterdir():
- if install_path.stem in i.stem:
- warn_msg = QMessageBox()
- warn_msg.setText(self.tr("Destination file/directory exists."))
- warn_msg.setInformativeText(
- self.tr("Do you really want to overwrite it? This will delete {}").format(
- self.dest_path_with_suffix
- )
+ if not dir_exists:
+ for item in os.listdir(dst_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.title),
+ self.tr(
+ "Destination {} already exists. "
+ "Are you sure you want to overwrite it?"
+ ).format(new_install_path),
+ QMessageBox.Yes | QMessageBox.No,
+ QMessageBox.Yes,
)
- warn_msg.addButton(QPushButton(self.tr("Yes")), QMessageBox.YesRole)
- warn_msg.addButton(QPushButton(self.tr("No")), QMessageBox.NoRole)
- response = warn_msg.exec()
-
- if response == 0:
- # Not using pathlib, since we can't delete not-empty folders. With shutil we can.
- if self.dest_path_with_suffix.is_dir():
- shutil.rmtree(self.dest_path_with_suffix)
+ if ans == QMessageBox.Yes:
+ if os.path.isdir(new_install_path):
+ shutil.rmtree(new_install_path)
else:
- self.dest_path_with_suffix.unlink()
+ os.remove(new_install_path)
else:
return
- self.ui.move_stack.setCurrentWidget(self.ui.move_progress)
+ self.move_game(self.rgame, new_install_path, dir_exists)
- self.game_moving = self.rgame.app_name
- self.is_moving = True
-
- self.ui.verify_button.setEnabled(False)
-
- if self.move_game_pop_up.is_different_drive(str(dest_path), str(install_path)):
- # Destination dir on different drive
- self.start_copy_diff_drive()
- else:
- # Destination dir on same drive
- shutil.move(self.rgame.igame.install_path, dest_path)
- self.set_new_game(self.dest_path_with_suffix)
-
- def __on_move_progress(self, progress_int):
- self.ui.move_progress.setValue(progress_int)
-
- def start_copy_diff_drive(self):
+ def move_game(self, rgame: RareGame, dst_path, dst_exists):
worker = MoveWorker(
- self.core,
- install_path=self.rgame.igame.install_path,
- dest_path=self.dest_path_with_suffix,
- is_existing_dir=self.existing_game_dir,
- igame=self.rgame.igame,
+ self.core, rgame=rgame, dst_path=dst_path, dst_exists=dst_exists
)
-
worker.signals.progress.connect(self.__on_move_progress)
- worker.signals.result.connect(self.set_new_game)
- worker.signals.error.connect(self.warn_no_space_left)
+ worker.signals.result.connect(self.__on_move_result)
+ worker.signals.error.connect(self.__on_worker_error)
self.rcore.enqueue_worker(self.rgame, worker)
- def move_helper_clean_up(self):
- self.ui.move_stack.setCurrentWidget(self.ui.move_button_page)
- self.move_game_pop_up.refresh_indicator()
- self.is_moving = False
- self.game_moving = None
- self.ui.verify_button.setEnabled(True)
- self.ui.move_button.setEnabled(True)
+ @pyqtSlot(RareGame, int, c_uint64, c_uint64)
+ def __on_move_progress(self, rgame: RareGame, progress: int, total_size: c_uint64, copied_size: c_uint64):
+ # 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)
- # This func does the needed UI changes, e.g. changing back to the initial move tool button and other stuff
- def warn_no_space_left(self):
- err_msg = QMessageBox()
- err_msg.setText(self.tr("Out of space or unknown OS error occured."))
- err_msg.exec()
- self.move_helper_clean_up()
-
- # Sets all needed variables to the new path.
- def set_new_game(self, dest_path_with_suffix):
- self.ui.install_path.setText(str(dest_path_with_suffix))
- self.rgame.igame.install_path = str(dest_path_with_suffix)
- self.core.lgd.set_installed_game(self.rgame.app_name, self.rgame.igame)
- self.move_game_pop_up.install_path = self.rgame.igame.install_path
-
- self.move_helper_clean_up()
-
- # We need to re-show the menu, as after clicking on browse, the whole menu gets closed.
- # Otherwise, the user would need to click on the move button again to open it again.
- def show_menu_after_browse(self):
- self.ui.move_button.showMenu()
+ @pyqtSlot(RareGame, str)
+ def __on_move_result(self, rgame: RareGame, dst_path: str):
+ QMessageBox.information(
+ self,
+ self.tr("Summary - {}").format(rgame.title),
+ self.tr("{} successfully moved to {}.").format(rgame.title, dst_path),
+ )
@pyqtSlot()
- def __update_ui(self):
- pass
+ def __update_widget(self):
+ """ React to state updates from RareGame """
+ # self.image.setPixmap(self.image_manager.get_pixmap(self.rgame.app_name, True))
+ self.image.setPixmap(self.rgame.pixmap)
+
+ 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(self.rgame.is_installed and not self.rgame.is_non_asset)
+ self.ui.install_size.setEnabled(self.rgame.is_installed and not self.rgame.is_non_asset)
+ self.ui.install_size.setText(
+ get_size(self.rgame.igame.install_size) if self.rgame.is_installed and not self.rgame.is_non_asset else "N/A"
+ )
+
+ self.ui.lbl_install_path.setEnabled(self.rgame.is_installed and not self.rgame.is_non_asset)
+ self.ui.install_path.setEnabled(self.rgame.is_installed and not self.rgame.is_non_asset)
+ self.ui.install_path.setText(
+ self.rgame.igame.install_path if self.rgame.is_installed and not self.rgame.is_non_asset else "N/A"
+ )
+
+ self.ui.platform.setText(
+ self.rgame.igame.platform if self.rgame.is_installed and not self.rgame.is_non_asset else "Windows"
+ )
+
+ self.ui.lbl_grade.setDisabled(
+ self.rgame.is_unreal and platform.system() == "Windows"
+ )
+ self.ui.grade.setDisabled(
+ self.rgame.is_unreal and platform.system() == "Windows"
+ )
+ self.ui.grade.setText(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(False)
+
+ 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 os.path.exists(os.path.join(self.core.lgd.get_tmp_path(), f"{self.rgame.app_name}.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(str)
@pyqtSlot(RareGame)
- def update_game(self, rgame: Union[RareGame, str]):
- if isinstance(rgame, str):
- rgame = self.rcore.get_game(rgame)
-
+ 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)
- self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page)
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)
- self.ui.move_stack.setCurrentWidget(self.ui.move_button_page)
except TypeError as e:
logger.warning(f"{self.rgame.app_name} move worker: {e}")
- self.rgame.signals.widget.update.disconnect(self.__update_ui)
- self.rgame.signals.game.installed.disconnect(self.update_game)
- self.rgame.signals.game.uninstalled.disconnect(self.update_game)
+ self.rgame.signals.widget.update.disconnect(self.__update_widget)
self.rgame = None
- rgame.signals.widget.update.connect(self.__update_ui)
- rgame.signals.game.installed.connect(self.update_game)
- rgame.signals.game.uninstalled.connect(self.update_game)
+ rgame.signals.widget.update.connect(self.__update_widget)
if (worker := rgame.worker()) is not None:
if isinstance(worker, VerifyWorker):
- self.ui.verify_stack.setCurrentWidget(self.ui.verify_progress_page)
- self.ui.verify_progress.setValue(rgame.progress)
worker.signals.progress.connect(self.__on_verify_progress)
- else:
- self.ui.verify_stack.setCurrentWidget(self.ui.verify_button_page)
if isinstance(worker, MoveWorker):
- self.ui.move_stack.setCurrentWidget(self.ui.move_progress_page)
- self.ui.move_progress.setValue(rgame.progress)
worker.signals.progress.connect(self.__on_move_progress)
- else:
- self.ui.move_stack.setCurrentWidget(self.ui.move_button_page)
self.title.setTitle(rgame.app_title)
- self.image.setPixmap(rgame.pixmap)
self.ui.app_name.setText(rgame.app_name)
- self.ui.version.setText(rgame.version)
self.ui.dev.setText(rgame.developer)
- if rgame.igame:
- self.ui.install_size.setText(get_size(rgame.igame.install_size))
- self.ui.install_path.setText(rgame.igame.install_path)
- self.ui.platform.setText(rgame.igame.platform)
- else:
- self.ui.install_size.setText("N/A")
- self.ui.install_path.setText("N/A")
- self.ui.platform.setText("Windows")
-
- self.ui.install_size.setEnabled(bool(rgame.igame))
- self.ui.lbl_install_size.setEnabled(bool(rgame.igame))
- self.ui.install_path.setEnabled(bool(rgame.igame))
- self.ui.lbl_install_path.setEnabled(bool(rgame.igame))
-
- self.ui.uninstall_button.setEnabled(bool(rgame.igame))
- self.ui.verify_button.setEnabled(bool(rgame.igame))
- self.ui.repair_button.setEnabled(bool(rgame.igame))
-
- if not rgame.is_installed or rgame.is_origin:
+ if rgame.is_non_asset:
+ self.ui.install_button.setText(self.tr("Link to Origin/Launch"))
self.ui.game_actions_stack.setCurrentWidget(self.ui.uninstalled_page)
- if rgame.is_origin:
- self.ui.version.setText("N/A")
- self.ui.version.setEnabled(False)
- self.ui.install_button.setText(self.tr("Link to Origin/Launch"))
- else:
- self.ui.install_button.setText(self.tr("Install Game"))
else:
- if not self.args.offline:
- self.ui.repair_button.setDisabled(
- not os.path.exists(os.path.join(self.core.lgd.get_tmp_path(), f"{rgame.app_name}.repair"))
- )
- self.ui.game_actions_stack.setCurrentWidget(self.ui.installed_page)
+ self.ui.install_button.setText(self.tr("Install Game"))
- grade_visible = not rgame.is_unreal and platform.system() != "Windows"
- self.ui.grade.setVisible(grade_visible)
- self.ui.lbl_grade.setVisible(grade_visible)
-
- if platform.system() != "Windows" and not rgame.is_unreal:
- self.ui.grade.setText(self.tr("Loading"))
- # TODO: Handle result emitted after quickly changing between game information
- steam_worker: SteamWorker = SteamWorker(self.core, rgame.app_name)
- steam_worker.signals.rating.connect(self.ui.grade.setText)
- QThreadPool.globalInstance().start(steam_worker)
-
- self.ui.verify_button.setEnabled(rgame.is_idle)
- self.ui.move_button.setEnabled(rgame.is_idle)
-
- self.move_game_pop_up.update_game(rgame.app_name)
+ self.move_game_pop_up.update_game(rgame)
self.rgame = rgame
-
-
+ self.__update_widget()
diff --git a/rare/components/tabs/games/game_info/move_game.py b/rare/components/tabs/games/game_info/move_game.py
index 25c7d694..04be4e89 100644
--- a/rare/components/tabs/games/game_info/move_game.py
+++ b/rare/components/tabs/games/game_info/move_game.py
@@ -1,12 +1,12 @@
import os
import shutil
from logging import getLogger
-from pathlib import Path
-from typing import Tuple
+from typing import Tuple, Optional
-from PyQt5.QtCore import pyqtSignal, Qt
+from PyQt5.QtCore import pyqtSignal, Qt, pyqtSlot
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog
+from rare.models.game import RareGame
from rare.shared import LegendaryCoreSingleton
from rare.utils.extra_widgets import PathEdit
@@ -20,132 +20,135 @@ class MoveGamePopUp(QWidget):
def __init__(self, parent=None):
super(MoveGamePopUp, self).__init__(parent=parent)
self.core = LegendaryCoreSingleton()
- self.install_path = ""
- self.move_path_edit = PathEdit("", QFileDialog.Directory, edit_func=self.edit_func_move_game)
- self.move_path_edit.path_select.clicked.connect(self.emit_browse_done_signal)
+ self.rgame: Optional[RareGame] = None
- self.move_button = QPushButton(self.tr("Move"))
- self.move_button.setFixedSize(self.move_path_edit.path_select.sizeHint())
- self.move_button.clicked.connect(self.emit_move_game_signal)
+ self.path_edit = PathEdit("", QFileDialog.Directory, edit_func=self.path_edit_cb)
+ self.path_edit.path_select.clicked.connect(self.browse_done)
- self.warn_overwriting = QLabel()
+ self.button = QPushButton(self.tr("Move"))
+ self.button.setMinimumWidth(self.path_edit.path_select.sizeHint().width())
+ self.button.clicked.connect(lambda p: self.move_clicked.emit(self.path_edit.text()))
+
+ self.warn_label = QLabel(self)
middle_layout = QHBoxLayout()
middle_layout.setAlignment(Qt.AlignRight)
- middle_layout.addWidget(self.warn_overwriting, stretch=1)
- middle_layout.addWidget(self.move_button)
+ middle_layout.addWidget(self.warn_label, stretch=1)
+ middle_layout.addWidget(self.button)
bottom_layout = QVBoxLayout()
- self.aval_space_label = QLabel()
- self.req_space_label = QLabel()
+ self.aval_space_label = QLabel(self)
+ self.req_space_label = QLabel(self)
bottom_layout.addWidget(self.aval_space_label)
bottom_layout.addWidget(self.req_space_label)
- layout: QVBoxLayout = QVBoxLayout()
- layout.addWidget(self.move_path_edit)
+ layout: QVBoxLayout = QVBoxLayout(self)
+ layout.addWidget(self.path_edit)
layout.addLayout(middle_layout)
layout.addLayout(bottom_layout)
- self.setLayout(layout)
-
- def emit_move_game_signal(self):
- self.move_clicked.emit(self.move_path_edit.text())
-
- def emit_browse_done_signal(self):
- self.browse_done.emit()
-
def refresh_indicator(self):
# needed so the edit_func gets run again
- text = self.move_path_edit.text()
- self.move_path_edit.setText(str())
- self.move_path_edit.setText(text)
+ text = self.path_edit.text()
+ self.path_edit.setText(str())
+ self.path_edit.setText(text)
- @staticmethod
- def is_different_drive(dir1: str, dir2: str):
- return os.stat(dir1).st_dev != os.stat(dir2).st_dev
-
- def edit_func_move_game(self, dir_selected):
- self.move_button.setEnabled(True)
- self.warn_overwriting.setHidden(True)
+ def path_edit_cb(self, path: str):
+ self.button.setEnabled(True)
+ self.warn_label.setHidden(True)
def helper_func(reason: str) -> Tuple[bool, str, str]:
- self.move_button.setEnabled(False)
- return False, dir_selected, self.tr(reason)
+ self.button.setEnabled(False)
+ return False, path, self.tr(reason)
- if not self.install_path or not dir_selected:
+ if not self.rgame.igame.install_path or not path:
return helper_func("You need to provide a directory.")
- install_path = Path(self.install_path).resolve()
- dest_path = Path(dir_selected).resolve()
- dest_path_with_suffix = dest_path.joinpath(install_path.stem).resolve()
+ src_path = os.path.realpath(self.rgame.igame.install_path)
+ dst_path = os.path.realpath(path)
+ dst_install_path = os.path.realpath(os.path.join(dst_path, os.path.basename(src_path)))
- if not dest_path.is_dir():
+ if not os.path.isdir(dst_path):
return helper_func("Directory doesn't exist or file selected.")
# Get free space on drive and size of game folder
- _, _, free_space = shutil.disk_usage(dest_path)
- source_size = sum(f.stat().st_size for f in install_path.glob("**/*") if f.is_file())
+ _, _, free_space = shutil.disk_usage(dst_path)
+ source_size = sum(
+ os.stat(os.path.join(dp, f)).st_size
+ for dp, dn, filenames in os.walk(src_path)
+ for f in filenames
+ )
# Calculate from bytes to gigabytes
- free_space_dest_drive = round(free_space / 1000 ** 3, 2)
+ free_space = round(free_space / 1000 ** 3, 2)
source_size = round(source_size / 1000 ** 3, 2)
- self.aval_space_label.setText(self.tr("Available space: {}GB".format(free_space_dest_drive)))
+ self.aval_space_label.setText(self.tr("Available space: {}GB".format(free_space)))
self.req_space_label.setText(self.tr("Required space: {}GB").format(source_size))
- if not os.access(dir_selected, os.W_OK) or not os.access(self.install_path, os.W_OK):
+ if not os.access(path, os.W_OK) or not os.access(self.rgame.igame.install_path, os.W_OK):
return helper_func("No write permission on destination path/current install path.")
- if install_path == dest_path or install_path == dest_path_with_suffix:
+ if src_path == dst_path or src_path == dst_install_path:
return helper_func("Same directory or parent directory selected.")
- if str(install_path) in str(dest_path):
+ if str(src_path) in str(dst_path):
return helper_func("You can't select a directory that is inside the current install path.")
- if str(dest_path_with_suffix) in str(install_path):
+ if str(dst_install_path) in str(src_path):
return helper_func("You can't select a directory which contains the game installation.")
for game in self.core.get_installed_list():
- if game.install_path in dir_selected:
+ if game.install_path in path:
return helper_func("Game installations cannot be nested due to unintended sideeffects.")
- is_existing_dir = is_game_dir(install_path, dest_path_with_suffix)
+ is_existing_dir = is_game_dir(src_path, dst_install_path)
- for i in dest_path.iterdir():
- if install_path.stem in i.stem:
- if dest_path_with_suffix.is_dir():
+ for item in os.listdir(dst_path):
+ if os.path.basename(src_path) in os.path.basename(item):
+ if os.path.isdir(dst_install_path):
if not is_existing_dir:
- self.warn_overwriting.setHidden(False)
- elif dest_path_with_suffix.is_file():
- self.warn_overwriting.setHidden(False)
+ self.warn_label.setHidden(False)
+ elif os.path.isfile(dst_install_path):
+ self.warn_label.setHidden(False)
- if free_space_dest_drive <= source_size and not is_existing_dir:
+ if free_space <= source_size and not is_existing_dir:
return helper_func("Not enough space available on drive.")
# Fallback
- self.move_button.setEnabled(True)
- return True, dir_selected, str()
+ self.button.setEnabled(True)
+ return True, path, str()
- def update_game(self, app_name):
- igame = self.core.get_installed_game(app_name, skip_sync=True)
- if igame is None:
+ @pyqtSlot()
+ def __update_widget(self):
+ """ React to state updates from RareGame """
+ if not self.rgame.is_installed and not self.rgame.is_non_asset:
+ self.setDisabled(True)
return
- self.install_path = igame.install_path
# FIXME: Make edit_func lighter instead of blocking signals
- self.move_path_edit.line_edit.blockSignals(True)
- self.move_path_edit.setText(igame.install_path)
+ self.path_edit.line_edit.blockSignals(True)
+ self.path_edit.setText(self.rgame.igame.install_path)
# FIXME: Make edit_func lighter instead of blocking signals
- self.move_path_edit.line_edit.blockSignals(False)
- self.warn_overwriting.setText(
- self.tr("Moving here will overwrite the dir/file {}/").format(Path(self.install_path).stem)
+ self.path_edit.line_edit.blockSignals(False)
+ self.warn_label.setText(
+ self.tr("Moving will overwrite {}").format(os.path.basename(self.rgame.install_path))
)
+ self.refresh_indicator()
+
+ def update_game(self, rgame: RareGame):
+ if self.rgame is not None:
+ self.rgame.signals.widget.update.disconnect(self.__update_widget)
+ self.rgame = None
+ rgame.signals.widget.update.connect(self.__update_widget)
+ self.rgame = rgame
+ self.__update_widget()
-def is_game_dir(install_path: Path, dest_path: Path):
+def is_game_dir(src_path: str, dst_path: str):
# This iterates over the destination dir, then iterates over the current install dir and if the file names
# matches, we have an exisiting dir
- if dest_path.is_dir():
- for file in dest_path.iterdir():
- for install_file in install_path.iterdir():
- if file.name == install_file.name:
+ if os.path.isdir(dst_path):
+ for dst_file in os.listdir(dst_path):
+ for src_file in os.listdir(src_path):
+ if dst_file == src_file:
return True
return False
diff --git a/rare/models/game.py b/rare/models/game.py
index c607bf36..2f5f7cf9 100644
--- a/rare/models/game.py
+++ b/rare/models/game.py
@@ -280,6 +280,9 @@ class RareGame(RareGameSlim):
with open(os.path.join(data_dir(), "game_meta.json"), "w") as metadata_json:
json.dump(metadata, metadata_json, indent=2)
+ def store_igame(self):
+ self.core.lgd.set_installed_game(self.app_name, self.igame)
+
def update_game(self):
self.game = self.core.get_game(
self.app_name, update_meta=False, platform=self.igame.platform if self.igame else "Windows"
@@ -428,6 +431,7 @@ class RareGame(RareGameSlim):
ret = False
return ret
+
@property
def needs_verification(self) -> bool:
"""!
@@ -444,7 +448,7 @@ class RareGame(RareGameSlim):
return False
@needs_verification.setter
- def needs_verification(self, not_update: bool) -> None:
+ def needs_verification(self, needs: bool) -> None:
"""!
@brief Sets the verification status of a game.
@@ -452,11 +456,12 @@ class RareGame(RareGameSlim):
named like this. After the verification, set this to 'False'
to update the InstalledGame in the widget.
- @param not_update If the game requires verification
+ @param needs If the game requires verification
@return None
"""
- if not not_update:
- self.igame = self.core.get_installed_game(self.game.app_name)
+ self.igame.needs_verification = needs
+ self.store_igame()
+ self.update_igame()
@property
def is_dlc(self) -> bool:
@@ -515,6 +520,16 @@ class RareGame(RareGameSlim):
return self.game.metadata.get("customAttributes", {}).get("CloudSaveFolder", {}).get("value")
return ""
+ @property
+ def install_path(self) -> str:
+ return self.igame.install_path
+
+ @install_path.setter
+ def install_path(self, path: str) -> None:
+ self.igame.install_path = path
+ self.store_igame()
+ self.update_igame()
+
def steam_grade(self) -> str:
if platform.system() == "Windows" or self.is_unreal:
return "na"
diff --git a/rare/shared/workers/move.py b/rare/shared/workers/move.py
index d4d80a4d..2571a595 100644
--- a/rare/shared/workers/move.py
+++ b/rare/shared/workers/move.py
@@ -1,121 +1,137 @@
import os
import shutil
+from ctypes import c_uint64
from logging import getLogger
-from pathlib import Path
from PyQt5.QtCore import pyqtSignal, QObject
from legendary.lfs.utils import validate_files
-from legendary.models.game import VerifyResult, InstalledGame
+from legendary.models.game import VerifyResult
+from rare.models.game import RareGame
from rare.lgndr.core import LegendaryCore
from .worker import QueueWorker, QueueWorkerInfo
logger = getLogger("MoveWorker")
-# noinspection PyUnresolvedReferences
class MoveWorker(QueueWorker):
class Signals(QObject):
- progress = pyqtSignal(int)
- result = pyqtSignal(str)
- error = pyqtSignal()
+ # int: percentage, c_uint64: source size, c_uint64: dest size
+ progress = pyqtSignal(RareGame, int, c_uint64, c_uint64)
+ # str: destination path
+ result = pyqtSignal(RareGame, str)
+ # str: error message
+ error = pyqtSignal(RareGame, str)
- def __init__(
- self,
- core: LegendaryCore,
- install_path: str,
- dest_path: Path,
- is_existing_dir: bool,
- igame: InstalledGame,
- ):
+ def __init__(self, core: LegendaryCore, rgame: RareGame, dst_path: str, dst_exists: bool):
super(MoveWorker, self).__init__()
self.signals = MoveWorker.Signals()
self.core = core
- self.install_path = install_path
- self.dest_path = dest_path
- self.source_size = 0
- self.dest_size = 0
- self.is_existing_dir = is_existing_dir
- self.igame = igame
- self.file_list = None
- self.total: int = 0
+ self.rgame = rgame
+ self.dst_path = dst_path
+ self.dst_exists = dst_exists
def worker_info(self) -> QueueWorkerInfo:
return QueueWorkerInfo(
app_name=self.rgame.app_name, app_title=self.rgame.app_title, worker_type="Move", state=self.state
)
- def run_real(self):
- root_directory = Path(self.install_path)
- self.source_size = sum(f.stat().st_size for f in root_directory.glob("**/*") if f.is_file())
+ def progress(self, src_size, dst_size):
+ progress = dst_size * 100 // src_size
+ self.rgame.signals.progress.update.emit(progress)
+ self.signals.progress.emit(self.rgame, progress, c_uint64(src_size), c_uint64(dst_size))
+
+ def run_real(self):
+ self.rgame.signals.progress.start.emit()
+
+ if os.stat(self.rgame.install_path).st_dev == os.stat(os.path.dirname(self.dst_path)).st_dev:
+ shutil.move(self.rgame.install_path, self.dst_path)
+
+ elif not self.dst_exists:
+ src_size = sum(
+ os.stat(os.path.join(dp, f)).st_size
+ for dp, dn, filenames in os.walk(self.rgame.install_path)
+ for f in filenames
+ )
+ dst_size = 0
+
+ def copy_with_progress(src, dst):
+ nonlocal dst_size
+ shutil.copy2(src, dst)
+ dst_size += os.stat(src).st_size
+ self.progress(src_size, dst_size)
- # if game dir is not existing, just copying:
- if not self.is_existing_dir:
shutil.copytree(
- self.install_path,
- self.dest_path,
- copy_function=self.copy_each_file_with_progress,
+ self.rgame.install_path,
+ self.dst_path,
+ copy_function=copy_with_progress,
dirs_exist_ok=True,
)
+ shutil.rmtree(self.rgame.install_path)
+
else:
- manifest_data, _ = self.core.get_installed_manifest(self.igame.app_name)
+ manifest_data, _ = self.core.get_installed_manifest(self.rgame.app_name)
manifest = self.core.load_manifest(manifest_data)
files = sorted(
manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower(),
)
- self.file_list = [(f.filename, f.sha_hash.hex()) for f in files]
- self.total = len(self.file_list)
+ if config_tags := self.core.lgd.config.get(self.rgame.app_name, 'install_tags', fallback=None):
+ install_tags = set(i.strip() for i in config_tags.split(','))
+ file_list = [
+ (f.filename, f.sha_hash.hex())
+ for f in files
+ if any(it in install_tags for it in f.install_tags) or not f.install_tags
+ ]
+ else:
+ file_list = [(f.filename, f.sha_hash.hex()) for f in files]
+
+ total_size = sum(manifest.file_manifest_list.get_file_by_path(fm[0]).file_size
+ for fm in file_list)
+ dst_size = 0
+
+ # This method is a copy_func, and only copies the src if it's a dir.
+ # Thus, it can be used to re-create the dir structure.
+ def copy_dir_structure(src, dst):
+ if os.path.isdir(dst):
+ dst = os.path.join(dst, os.path.basename(src))
+ if os.path.isdir(src):
+ shutil.copyfile(src, dst)
+ shutil.copystat(src, dst)
+ return dst
# recreate dir structure
shutil.copytree(
- self.install_path,
- self.dest_path,
- copy_function=self.copy_dir_structure,
+ self.rgame.install_path,
+ self.dst_path,
+ copy_function=copy_dir_structure,
dirs_exist_ok=True,
)
- for i, (result, relative_path, _, _) in enumerate(
- validate_files(str(self.dest_path), self.file_list)
- ):
- dst_path = f"{self.dest_path}/{relative_path}"
- src_path = f"{self.install_path}/{relative_path}"
- if Path(src_path).is_file():
- if result == VerifyResult.HASH_MISMATCH:
+ for result, path, _, _ in validate_files(self.dst_path, file_list):
+ dst_path = os.path.join(self.dst_path, path)
+ src_path = os.path.join(self.rgame.install_path, path)
+ if os.path.isfile(src_path):
+ if result == VerifyResult.HASH_MATCH:
+ dst_size += os.stat(dst_path).st_size
+ if result == VerifyResult.HASH_MISMATCH or result == VerifyResult.FILE_MISSING:
try:
- shutil.copy(src_path, dst_path)
- except IOError:
- self.signals.no_space_left.emit()
+ shutil.copy2(src_path, dst_path)
+ dst_size += os.stat(dst_path).st_size
+ except (IOError, OSError) as e:
+ self.rgame.signals.progress.finish.emit(True)
+ self.signals.error.emit(self.rgame, str(e))
return
- elif result == VerifyResult.FILE_MISSING:
- try:
- shutil.copy(src_path, dst_path)
- except (IOError, OSError):
- self.signals.no_space_left.emit()
- return
- elif result == VerifyResult.OTHER_ERROR:
+ else:
logger.warning(f"Copying file {src_path} to {dst_path} failed")
- self.signals.progress.emit(int(i * 10 / self.total * 10))
+ self.progress(total_size, dst_size)
else:
logger.warning(
- f"Source dir does not have file {src_path}. File will be missing in the destination "
- f"dir. "
+ f"Source dir does not have file {src_path}. File will be missing in the destination dir."
)
+ self.rgame.needs_verification = True
+ shutil.rmtree(self.rgame.install_path)
- shutil.rmtree(self.install_path)
- self.signals.finished.emit(str(self.dest_path))
+ self.rgame.install_path = self.dst_path
- def copy_each_file_with_progress(self, src, dst):
- shutil.copy(src, dst)
- self.dest_size += Path(src).stat().st_size
- self.signals.progress.emit(int(self.dest_size * 10 / self.source_size * 10))
-
- # This method is a copy_func, and only copies the src if it's a dir.
- # Thus, it can be used to re-create the dir strucute.
- @staticmethod
- def copy_dir_structure(src, dst):
- if os.path.isdir(dst):
- dst = os.path.join(dst, os.path.basename(src))
- if os.path.isdir(src):
- shutil.copyfile(src, dst)
- shutil.copystat(src, dst)
- return dst
+ self.rgame.signals.progress.finish.emit(False)
+ self.signals.result.emit(self.rgame, self.dst_path)
diff --git a/rare/utils/steam_grades.py b/rare/utils/steam_grades.py
index 275a0080..07d7b9d7 100644
--- a/rare/utils/steam_grades.py
+++ b/rare/utils/steam_grades.py
@@ -4,7 +4,6 @@ import os
from datetime import date
import requests
-from PyQt5.QtCore import pyqtSignal, QRunnable, QObject, QCoreApplication
from rare.lgndr.core import LegendaryCore
from rare.utils.paths import data_dir, cache_dir
@@ -13,33 +12,6 @@ replace_chars = ",;.:-_ "
url = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
-class SteamWorker(QRunnable):
- class Signals(QObject):
- rating = pyqtSignal(str)
-
- def __init__(self, core: LegendaryCore, app_name: str):
- super(SteamWorker, self).__init__()
- self.signals = SteamWorker.Signals()
- self.core = core
- self.app_name = app_name
- _tr = QCoreApplication.translate
- self.ratings = {
- "platinum": _tr("SteamWorker", "Platinum"),
- "gold": _tr("SteamWorker", "Gold"),
- "silver": _tr("SteamWorker", "Silver"),
- "bronze": _tr("SteamWorker", "Bronze"),
- "fail": _tr("SteamWorker", "Could not get grade"),
- "borked": _tr("SteamWorker", "Borked"),
- "pending": _tr("SteamWorker", "Loading..."),
- }
-
- def run(self) -> None:
- self.signals.rating.emit(
- self.ratings.get(get_rating(self.core, self.app_name), self.ratings["fail"])
- )
- self.signals.deleteLater()
-
-
__steam_ids_json = None
__grades_json = None