From d77808ee05d0d34f0ad9649103156bb66ac433a7 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Sat, 27 Feb 2021 17:20:56 +0100 Subject: [PATCH] Sync Saves --- Rare/Components/Dialogs/PathInputDialog.py | 44 +++++ Rare/Components/TabWidget.py | 10 +- Rare/Components/Tabs/CloudSaves/CloudSaves.py | 99 +++++++++++ Rare/Components/Tabs/CloudSaves/SyncWidget.py | 164 ++++++++++++++++++ Rare/Components/Tabs/CloudSaves/__init__.py | 0 .../Tabs/Games/GameInfo/GameInfo.py | 1 - Rare/utils/QtExtensions.py | 14 ++ 7 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 Rare/Components/Dialogs/PathInputDialog.py create mode 100644 Rare/Components/Tabs/CloudSaves/CloudSaves.py create mode 100644 Rare/Components/Tabs/CloudSaves/SyncWidget.py create mode 100644 Rare/Components/Tabs/CloudSaves/__init__.py diff --git a/Rare/Components/Dialogs/PathInputDialog.py b/Rare/Components/Dialogs/PathInputDialog.py new file mode 100644 index 00000000..b6c9410d --- /dev/null +++ b/Rare/Components/Dialogs/PathInputDialog.py @@ -0,0 +1,44 @@ +from PyQt5.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QLabel, QDialog + +from Rare.utils.QtExtensions import PathEdit + + +class PathInputDialog(QDialog): + def __init__(self, title_text, text): + super().__init__() + self.path = "" + + self.setWindowTitle(title_text) + self.info_label = QLabel(text) + self.info_label.setWordWrap(True) + + self.input = PathEdit(self.tr("Select directory"), QFileDialog.DirectoryOnly) + + self.layout = QVBoxLayout() + self.layout.addWidget(self.info_label) + self.layout.addWidget(self.input) + + self.child_layout = QHBoxLayout() + self.ok_button = QPushButton("Ok") + self.ok_button.clicked.connect(self.ok) + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(self.cancel) + self.child_layout.addStretch() + self.child_layout.addWidget(self.ok_button) + self.child_layout.addWidget(self.cancel_button) + + self.layout.addLayout(self.child_layout) + + self.setLayout(self.layout) + + def get_path(self): + self.exec_() + return self.path + + def cancel(self): + self.path = "" + self.close() + + def ok(self): + self.path = self.input.text() + self.close() \ No newline at end of file diff --git a/Rare/Components/TabWidget.py b/Rare/Components/TabWidget.py index 79b828e1..58738838 100644 --- a/Rare/Components/TabWidget.py +++ b/Rare/Components/TabWidget.py @@ -4,6 +4,7 @@ from legendary.core import LegendaryCore from Rare import style_path from Rare.Components.Tabs.Account.AccountWidget import MiniWidget +from Rare.Components.Tabs.CloudSaves.CloudSaves import SyncSaves from Rare.Components.Tabs.Downloads.DownloadTab import DownloadTab from Rare.Components.Tabs.Games.GamesTab import GameTab from Rare.Components.Tabs.Settings.SettingsTab import SettingsTab @@ -12,7 +13,7 @@ from Rare.Components.Tabs.Settings.SettingsTab import SettingsTab class TabWidget(QTabWidget): def __init__(self, core: LegendaryCore): super(TabWidget, self).__init__() - self.setTabBar(TabBar(2)) + self.setTabBar(TabBar(3)) self.settings = SettingsTab(core) self.game_list = GameTab(core) self.addTab(self.game_list, self.tr("Games")) @@ -20,13 +21,16 @@ class TabWidget(QTabWidget): self.addTab(self.downloadTab, "Downloads") self.downloadTab.finished.connect(self.game_list.default_widget.game_list.update_list) self.game_list.default_widget.game_list.install_game.connect(lambda x: self.downloadTab.install_game(x)) + + self.cloud_saves = SyncSaves(core) + self.addTab(self.cloud_saves, "Cloud Saves") # Space Tab self.addTab(QWidget(), "") - self.setTabEnabled(2, False) + self.setTabEnabled(3, False) self.account = QWidget() self.addTab(self.account, "") - self.setTabEnabled(3, False) + self.setTabEnabled(4, False) # self.settings = SettingsTab(core) self.addTab(self.settings, QIcon(style_path + "/Icons/settings.png"), None) diff --git a/Rare/Components/Tabs/CloudSaves/CloudSaves.py b/Rare/Components/Tabs/CloudSaves/CloudSaves.py new file mode 100644 index 00000000..92222ad8 --- /dev/null +++ b/Rare/Components/Tabs/CloudSaves/CloudSaves.py @@ -0,0 +1,99 @@ +from logging import getLogger + +from PyQt5.QtCore import QThread, pyqtSignal, QObjectCleanupHandler, Qt +from PyQt5.QtWidgets import * +from legendary.core import LegendaryCore +from legendary.models.game import SaveGameStatus + +from Rare.Components.Dialogs.PathInputDialog import PathInputDialog +from Rare.Components.Tabs.CloudSaves.SyncWidget import SyncWidget +from Rare.utils.QtExtensions import WaitingSpinner + +logger = getLogger("Sync Saves") + + +class LoadThread(QThread): + signal = pyqtSignal(list) + + def __init__(self, core: LegendaryCore): + super(LoadThread, self).__init__() + self.core = core + + def run(self) -> None: + saves = self.core.get_save_games() + self.signal.emit(saves) + + +class SyncSaves(QScrollArea): + + def __init__(self, core: LegendaryCore): + super(SyncSaves, self).__init__() + self.core = core + self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(WaitingSpinner()) + layout.addWidget(QLabel("

Loading Cloud Saves

")) + layout.addStretch() + self.widget.setLayout(layout) + self.setWidget(self.widget) + + self.start_thread = LoadThread(self.core) + self.start_thread.signal.connect(self.setup_ui) + self.start_thread.start() + self.igames = self.core.get_installed_list() + + def setup_ui(self, saves: list): + self.start_thread.disconnect() + + self.main_layout = QVBoxLayout() + self.title = QLabel( + f"

" + self.tr("Cloud Saves") + "

\n" + self.tr("Found Saves for folowing Games")) + self.sync_all_button = QPushButton(self.tr("Sync all games")) + self.sync_all_button.clicked.connect(self.sync_all) + self.main_layout.addWidget(self.title) + self.main_layout.addWidget(self.sync_all_button) + + latest_save = {} + for i in saves: + latest_save[i.app_name] = i + logger.info(f'Got {len(latest_save)} remote save game(s)') + if len(latest_save) == 0: + QMessageBox.information(self.tr("No Games Found"), self.tr("Your games don't support cloud save")) + self.close() + return + self.widgets = [] + + for igame in self.igames: + game = self.core.get_game(igame.app_name) + if not game.supports_cloud_saves: + continue + if latest_save.get(igame.app_name): + sync_widget = SyncWidget(igame, latest_save[igame.app_name], self.core) + else: + continue + self.main_layout.addWidget(sync_widget) + self.widgets.append(sync_widget) + + self.widget = QWidget() + self.widget.setLayout(self.main_layout) + self.setWidget(self.widget) + + def sync_all(self): + for w in self.widgets: + if not w.igame.save_path: + save_path = self.core.get_save_path(w.igame.app_name) + if '%' in save_path or '{' in save_path: + self.logger.info("Could not find save_path") + save_path = PathInputDialog(self.tr("Found no savepath"), + self.tr("No save path was found. Please select path or skip")) + if save_path == "": + continue + else: + w.igame.save_path = save_path + if w.res == SaveGameStatus.SAME_AGE: + continue + if w.res == SaveGameStatus.REMOTE_NEWER: + w.download() + elif w.res == SaveGameStatus.LOCAL_NEWER: + w.upload() \ No newline at end of file diff --git a/Rare/Components/Tabs/CloudSaves/SyncWidget.py b/Rare/Components/Tabs/CloudSaves/SyncWidget.py new file mode 100644 index 00000000..740c5896 --- /dev/null +++ b/Rare/Components/Tabs/CloudSaves/SyncWidget.py @@ -0,0 +1,164 @@ +import os +from logging import getLogger + +from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QHBoxLayout, QLabel +from legendary.core import LegendaryCore +from legendary.models.game import InstalledGame, SaveGameStatus + + + +class _UploadThread(QThread): + signal = pyqtSignal() + + def __init__(self, app_name, date_time, save_path, core: LegendaryCore): + super(_UploadThread, self).__init__() + self.core = core + self.app_name = app_name + self.date_time = date_time + self.save_path = save_path + + def run(self) -> None: + self.core.upload_save(self.app_name, self.save_path, self.date_time) + + +class _DownloadThread(QThread): + signal = pyqtSignal() + + def __init__(self, app_name, latest_save, save_path, core: LegendaryCore): + super(_DownloadThread, self).__init__() + self.core = core + self.app_name = app_name + self.latest_save = latest_save + self.save_path = save_path + + def run(self) -> None: + self.core.download_saves(self.app_name, self.latest_save.manifest_name, self.save_path, clean_dir=True) + + +class SyncWidget(QWidget): + def __init__(self, igame: InstalledGame, save, core: LegendaryCore): + super(SyncWidget, self).__init__() + self.layout = QVBoxLayout() + + self.core = core + self.save = save + self.logger = getLogger("Sync " + igame.app_name) + self.game = self.core.get_game(igame.app_name) + self.igame = igame + self.has_save_path = True + if not igame.save_path: + save_path = self.core.get_save_path(igame.app_name) + if '%' in save_path or '{' in save_path: + status = self.tr("Path not found") + self.logger.info("Could not find save path") + else: + igame.save_path = save_path + + if not os.path.exists(igame.save_path): + os.makedirs(igame.save_path) + + self.res, (self.dt_local, dt_remote) = self.core.check_savegame_state(igame.save_path, save) + if self.res == SaveGameStatus.NO_SAVE: + self.logger.info('No cloud or local savegame found.') + return + game_title = QLabel(f"

{igame.title}

") + if self.dt_local: + local_save_date = QLabel( + self.tr("Local Save date: ") + str(self.dt_local.strftime('%Y-%m-%d %H:%M:%S'))) + else: + local_save_date = QLabel(self.tr("No Local Save files")) + if dt_remote: + cloud_save_date = QLabel(self.tr("Cloud save date: ") + str(dt_remote.strftime('%Y-%m-%d %H:%M:%S'))) + else: + cloud_save_date = QLabel(self.tr("No Cloud saves")) + + if self.res == SaveGameStatus.SAME_AGE: + self.logger.info(f'Save game for "{igame.title}" is up to date') + status = self.tr("Game is up to date") + self.upload_button = QPushButton(self.tr("Upload anyway")) + self.download_button = QPushButton(self.tr("Download anyway")) + elif self.res == SaveGameStatus.REMOTE_NEWER: + status = self.tr("Cloud save is newer") + self.download_button = QPushButton(self.tr("Download Cloud saves")) + self.download_button.setStyleSheet(""" + QPushButton{ background-color: lime} + """) + self.upload_button = QPushButton(self.tr("Upload Saves")) + self.logger.info(f'Cloud save for "{igame.title}" is newer:') + self.logger.info(f'- Cloud save date: {dt_remote.strftime("%Y-%m-%d %H:%M:%S")}') + if self.dt_local: + self.logger.info(f'- Local save date: {self.dt_local.strftime("%Y-%m-%d %H:%M:%S")}') + else: + self.logger.info('- Local save date: N/A') + self.upload_button.setDisabled(True) + self.upload_button.setToolTip("No local save") + + elif self.res == SaveGameStatus.LOCAL_NEWER: + status = self.tr("Local save is newer") + self.upload_button = QPushButton(self.tr("Upload saves")) + self.upload_button.setStyleSheet(""" + QPushButton{ background-color: lime} + """) + self.download_button = QPushButton(self.tr("Download saves")) + self.logger.info(f'Local save for "{igame.title}" is newer') + if dt_remote: + self.logger.info(f'- Cloud save date: {dt_remote.strftime("%Y-%m-%d %H:%M:%S")}') + else: + self.logger.info('- Cloud save date: N/A') + self.download_button.setDisabled(True) + self.logger.info(f'- Local save date: {self.dt_local.strftime("%Y-%m-%d %H:%M:%S")}') + else: + self.logger.error("Error") + return + + self.upload_button.clicked.connect(self.upload) + self.download_button.clicked.connect(self.download) + self.info_text = QLabel(status) + self.layout.addWidget(game_title) + self.layout.addWidget(local_save_date) + self.layout.addWidget(cloud_save_date) + + save_path_layout = QHBoxLayout() + self.save_path_text = QLabel(igame.save_path) + self.save_path_text.setWordWrap(True) + self.change_save_path = QPushButton(self.tr("Change path")) + save_path_layout.addWidget(self.save_path_text) + save_path_layout.addWidget(self.change_save_path) + self.layout.addLayout(save_path_layout) + self.layout.addWidget(self.info_text) + button_layout = QHBoxLayout() + button_layout.addWidget(self.upload_button) + button_layout.addWidget(self.download_button) + self.layout.addLayout(button_layout) + + self.setLayout(self.layout) + + def upload(self): + self.logger.info("Uploading Saves for game " + self.igame.title) + self.info_text.setText(self.tr("Uploading...")) + self.upload_button.setDisabled(True) + self.download_button.setDisabled(True) + self.thr = _UploadThread(self.igame.app_name, self.dt_local, self.igame.save_path, self.core) + self.thr.finished.connect(self.uploaded) + self.thr.start() + + def uploaded(self): + self.info_text.setText(self.tr("Upload finished")) + + def download(self): + if not os.path.exists(self.igame.save_path): + os.makedirs(self.igame.save_path) + self.upload_button.setDisabled(True) + self.download_button.setDisabled(True) + self.logger.info("Downloading Saves for game " + self.igame.title) + self.info_text.setText(self.tr("Downloading...")) + thr = _DownloadThread(self.igame.app_name, self.save, self.igame.save_path, self.core) + thr.finished.connect(self.downloaded) + thr.start() + + def downloaded(self): + self.info_text.setText(self.tr("Download finished")) + self.upload_button.setDisabled(True) + self.download_button.setDisabled(True) + self.download_button.setStyleSheet("QPushButton{background-color: black}") diff --git a/Rare/Components/Tabs/CloudSaves/__init__.py b/Rare/Components/Tabs/CloudSaves/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Rare/Components/Tabs/Games/GameInfo/GameInfo.py b/Rare/Components/Tabs/Games/GameInfo/GameInfo.py index 16e41c68..00ee64a0 100644 --- a/Rare/Components/Tabs/Games/GameInfo/GameInfo.py +++ b/Rare/Components/Tabs/Games/GameInfo/GameInfo.py @@ -18,7 +18,6 @@ class InfoTabs(QTabWidget): self.info = GameInfo(core) self.addTab(self.info, "Game Info") - self.addTab(QLabel("Coming soon"), "Cloud Saves") self.addTab(QLabel("Coming soon"), "Settings") diff --git a/Rare/utils/QtExtensions.py b/Rare/utils/QtExtensions.py index 6398c92d..fc5794d8 100644 --- a/Rare/utils/QtExtensions.py +++ b/Rare/utils/QtExtensions.py @@ -1,9 +1,12 @@ import os from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal +from PyQt5.QtGui import QMovie from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, QHBoxLayout, QWidget, QLineEdit, \ QPushButton, QStyleOptionTab, QStylePainter, QTabBar +from Rare import style_path + class FlowLayout(QLayout): def __init__(self, parent=None, margin=-1, hspacing=-1, vspacing=-1): @@ -177,3 +180,14 @@ class SideTabBar(QTabBar): painter.translate(-c) painter.drawControl(QStyle.CE_TabBarTabLabel, opt); painter.restore() + +class WaitingSpinner(QLabel): + def __init__(self): + super(WaitingSpinner, self).__init__() + self.setStyleSheet(""" + margin-left: auto; + margin-right: auto; + """) + self.movie = QMovie(style_path+"Icons/loader.gif") + self.setMovie(self.movie) + self.movie.start()