1
0
Fork 0
mirror of synced 2024-06-29 11:40:37 +12:00
Rare/rare/components/tabs/games/game_info/game_info.py
aznd 7a82091285
Implement moving game installations (#193)
Implement moving game installations
2022-04-10 21:14:46 +02:00

416 lines
15 KiB
Python

import os
import platform
import shutil
from pathlib import Path
from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import Qt, pyqtSignal, QThreadPool, pyqtSlot
from PyQt5.QtWidgets import (
QCheckBox,
QFileDialog,
QHBoxLayout,
QLabel,
QMenu,
QProgressBar,
QPushButton,
QVBoxLayout,
QWidget,
QMessageBox,
QWidgetAction,
)
from legendary.models.game import Game, InstalledGame
from rare.shared import (
LegendaryCoreSingleton,
GlobalSignalsSingleton,
ArgumentsSingleton,
)
from rare.ui.components.tabs.games.game_info.game_info import Ui_GameInfo
from rare.utils.extra_widgets import PathEdit
from rare.utils.legendary_utils import VerifyWorker
from rare.utils.models import InstallOptionsModel
from rare.utils.steam_grades import SteamWorker
from rare.utils.utils import get_size, get_pixmap
logger = getLogger("GameInfo")
class GameInfo(QWidget, Ui_GameInfo):
igame: InstalledGame
game: Game = None
verify_threads = dict()
verification_finished = pyqtSignal(InstalledGame)
uninstalled = pyqtSignal(str)
def __init__(self, parent, game_utils):
super(GameInfo, self).__init__(parent=parent)
self.setupUi(self)
self.core = LegendaryCoreSingleton()
self.signals = GlobalSignalsSingleton()
self.args = ArgumentsSingleton()
self.game_utils = game_utils
if platform.system() == "Windows":
self.lbl_grade.setVisible(False)
self.grade.setVisible(False)
else:
self.steam_worker = SteamWorker(self.core)
self.steam_worker.signals.rating_signal.connect(self.grade.setText)
self.steam_worker.setAutoDelete(False)
self.game_actions_stack.setCurrentIndex(0)
self.install_button.setText(self.tr("Link to Origin/Launch"))
self.game_actions_stack.resize(self.game_actions_stack.minimumSize())
self.uninstall_button.clicked.connect(self.uninstall)
self.verify_button.clicked.connect(self.verify)
self.verify_pool = QThreadPool()
self.verify_pool.setMaxThreadCount(2)
if self.args.offline:
self.repair_button.setDisabled(True)
else:
self.repair_button.clicked.connect(self.repair)
self.install_button.clicked.connect(
lambda: self.game_utils.launch_game(self.game.app_name)
)
self.move_game_pop_up = MoveGamePopUp()
self.move_action = QWidgetAction(self)
self.move_action.setDefaultWidget(self.move_game_pop_up)
self.move_button.setMenu(QMenu())
self.move_button.menu().addAction(self.move_action)
self.widget_container = QWidget()
box_layout = QHBoxLayout()
box_layout.setContentsMargins(0, 0, 0, 0)
box_layout.addWidget(self.move_button)
self.widget_container.setLayout(box_layout)
index = self.move_stack.addWidget(self.widget_container)
self.move_stack.setCurrentIndex(index)
self.move_game_pop_up.move_clicked.connect(self.move_game)
self.move_game_pop_up.browse_done.connect(self.show_menu_after_browse)
def uninstall(self):
if self.game_utils.uninstall_game(self.game.app_name):
self.game_utils.update_list.emit(self.game.app_name)
self.uninstalled.emit(self.game.app_name)
def repair(self):
repair_file = os.path.join(
self.core.lgd.get_tmp_path(), f"{self.game.app_name}.repair"
)
if not os.path.exists(repair_file):
QMessageBox.warning(
self,
"Warning",
self.tr(
"Repair file does not exist or game does not need a repair. Please verify game first"
),
)
return
self.signals.install_game.emit(
InstallOptionsModel(app_name=self.game.app_name, repair=True, update=True)
)
def verify(self):
if not os.path.exists(self.igame.install_path):
logger.error("Path does not exist")
QMessageBox.warning(
self,
"Warning",
self.tr("Installation path of {} does not exist. Cannot verify").format(
self.igame.title
),
)
return
self.verify_widget.setCurrentIndex(1)
verify_worker = VerifyWorker(self.game.app_name)
verify_worker.signals.status.connect(self.verify_staistics)
verify_worker.signals.summary.connect(self.finish_verify)
self.verify_progress.setValue(0)
self.verify_threads[self.game.app_name] = verify_worker
self.verify_pool.start(verify_worker)
def verify_staistics(self, num, total, app_name):
# checked, max, app_name
if app_name == self.game.app_name:
self.verify_progress.setValue(num * 100 // total)
def finish_verify(self, failed, missing, app_name):
if failed == missing == 0:
QMessageBox.information(
self,
"Summary",
"Game was verified successfully. No missing or corrupt files found",
)
igame = self.core.get_installed_game(app_name)
if igame.needs_verification:
igame.needs_verification = False
self.core.lgd.set_installed_game(self.igame.app_name, igame)
self.verification_finished.emit(igame)
elif failed == missing == -1:
QMessageBox.warning(self, "Warning", self.tr("Something went wrong"))
else:
ans = QMessageBox.question(
self,
"Summary",
self.tr(
"Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them?"
).format(failed, missing),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if ans == QMessageBox.Yes:
self.signals.install_game.emit(
InstallOptionsModel(
app_name=self.game.app_name, repair=True, update=True
)
)
self.verify_widget.setCurrentIndex(0)
self.verify_threads.pop(app_name)
@pyqtSlot(str)
def move_game(self, destination_path):
destination_path = Path(destination_path)
install_path = Path(self.igame.install_path)
destination_path_with_suffix = destination_path.joinpath(
install_path.stem
)
progress_of_moving = QProgressBar(self)
progress_of_moving.setValue(0)
for i in destination_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(destination_path_with_suffix)
)
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 destination_path_with_suffix.is_dir():
shutil.rmtree(destination_path_with_suffix)
else:
destination_path_with_suffix.unlink()
else:
return
shutil.move(self.igame.install_path, destination_path)
self.install_path.setText(str(destination_path_with_suffix))
self.igame.install_path = str(destination_path_with_suffix)
self.core.lgd.set_installed_game(self.igame.app_name, self.igame)
self.move_game_pop_up.install_path = self.igame.install_path
self.move_game_pop_up.refresh_indicator()
progress_of_moving.setValue(100)
def show_menu_after_browse(self):
self.move_button.showMenu()
def update_game(self, app_name: str):
self.game = self.core.get_game(app_name)
self.igame = self.core.get_installed_game(self.game.app_name)
self.title.setTitle(self.game.app_title)
pixmap = get_pixmap(self.game.app_name)
if pixmap.isNull():
pixmap = get_pixmap(self.parent().parent().parent().ue_name)
w = 200
pixmap = pixmap.scaled(w, int(w * 4 / 3))
self.image.setPixmap(pixmap)
self.app_name.setText(self.game.app_name)
if self.igame:
self.version.setText(self.igame.version)
else:
self.version.setText(
self.game.app_version(self.igame.platform if self.igame else "Windows")
)
self.dev.setText(self.game.metadata["developer"])
if self.igame:
self.install_size.setText(get_size(self.igame.install_size))
self.install_path.setText(self.igame.install_path)
self.install_size.setVisible(True)
self.install_path.setVisible(True)
self.platform.setText(self.igame.platform)
else:
self.install_size.setVisible(False)
self.install_path.setVisible(False)
self.platform.setText("Windows")
if not self.igame:
# origin game
self.uninstall_button.setDisabled(True)
self.verify_button.setDisabled(True)
self.repair_button.setDisabled(True)
self.game_actions_stack.setCurrentIndex(1)
else:
self.uninstall_button.setDisabled(False)
self.verify_button.setDisabled(False)
if not self.args.offline:
self.repair_button.setDisabled(False)
self.game_actions_stack.setCurrentIndex(0)
try:
is_ue = self.core.get_asset(app_name).namespace == "ue"
except ValueError:
is_ue = False
self.grade.setVisible(not is_ue)
self.lbl_grade.setVisible(not is_ue)
if platform.system() != "Windows" and not is_ue:
self.grade.setText(self.tr("Loading"))
self.steam_worker.set_app_name(self.game.app_name)
QThreadPool.globalInstance().start(self.steam_worker)
if len(self.verify_threads.keys()) == 0 or not self.verify_threads.get(
self.game.app_name
):
self.verify_widget.setCurrentIndex(0)
elif self.verify_threads.get(self.game.app_name):
self.verify_widget.setCurrentIndex(1)
self.verify_progress.setValue(
int(
self.verify_threads[self.game.app_name].num
/ self.verify_threads[self.game.app_name].total
* 100
)
)
self.move_game_pop_up.update_game(app_name)
class MoveGamePopUp(QWidget):
move_clicked = pyqtSignal(str)
browse_done = pyqtSignal()
def __init__(self):
super(MoveGamePopUp, self).__init__()
layout: QVBoxLayout = QVBoxLayout()
self.install_path = str()
self.core = LegendaryCoreSingleton()
self.move_path_edit = PathEdit(
str(), QFileDialog.Directory, edit_func=self.edit_func_move_game
)
self.move_path_edit.path_select.clicked.connect(self.emit_browse_done_signal)
self.move_game = QPushButton(self.tr("Move"))
self.move_game.setMaximumWidth(50)
self.move_game.clicked.connect(self.emit_move_game_signal)
self.warn_overwriting = QLabel()
bottom_layout = QHBoxLayout()
bottom_layout.setAlignment(Qt.AlignRight)
bottom_layout.addWidget(self.warn_overwriting, stretch=1)
bottom_layout.addWidget(self.move_game)
layout.addWidget(self.move_path_edit)
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)
# Thanks to lk.
def find_mount(self, path):
mount_point = path
while path != path.anchor:
if path.is_mount():
return path
else:
path = path.parent
return mount_point
def edit_func_move_game(self, dir_selected):
self.warn_overwriting.setHidden(True)
def helper_func(reason: str) -> Tuple[bool, str, str]:
self.move_game.setEnabled(False)
return False, dir_selected, self.tr(reason)
if not self.install_path or not dir_selected:
return helper_func("You need to provide a directory.")
current_path = Path(self.install_path).resolve()
destination_path = Path(dir_selected).resolve()
destination_path_with_suffix = destination_path.joinpath(
current_path.stem
).resolve()
if not destination_path.is_dir():
return helper_func("Directory doesn't exist or file selected.")
if not os.access(dir_selected, os.W_OK) or not os.access(self.install_path, os.W_OK):
return helper_func("No write permission on destination path/current install path.")
if (
current_path == destination_path
or current_path == destination_path_with_suffix
):
return helper_func("Same directory or parent directory selected.")
if str(current_path) in str(destination_path):
return helper_func(
"You can't select a directory that is inside the current install path."
)
if str(destination_path_with_suffix) in str(current_path):
return helper_func(
"You can't select a directory which contains the game installation."
)
if not platform.system() == "Windows":
if self.find_mount(destination_path) != self.find_mount(current_path):
return helper_func(
"Moving to a different drive is currently not supported."
)
else:
if current_path.drive != destination_path.drive:
return helper_func(
"Moving to a different drive is currently not supported."
)
for game in self.core.get_installed_list():
if game.install_path in dir_selected:
return helper_func(
"Game installations cannot be nested due to unintended sideeffects."
)
for i in destination_path.iterdir():
if current_path.stem in i.stem:
self.warn_overwriting.setHidden(False)
# Fallback
self.move_game.setEnabled(True)
return True, dir_selected, str()
def update_game(self, app_name):
igame = self.core.get_installed_game(app_name, False)
if igame is None:
return
self.install_path = igame.install_path
self.move_path_edit.setText(igame.install_path)
self.warn_overwriting.setText(self.tr("Moving here will overwrite the dir/file {}/").format(Path(self.install_path).stem))