diff --git a/README.md b/README.md index 7d42d740..825be8e7 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Using the exe-file could cause an error with the stylesheets - Translations (English and German) - Design (Pretty Stylesheets, Icons, Icon view for installed Games) - Download Progressbar +- Offline mode ## Images diff --git a/Rare/Styles/Icons/loader.gif b/Rare/Styles/Icons/loader.gif new file mode 100644 index 00000000..28929508 Binary files /dev/null and b/Rare/Styles/Icons/loader.gif differ diff --git a/Rare/Tabs/GamesInstalled/InstalledList.py b/Rare/Tabs/GamesInstalled/InstalledList.py index 7b107599..7cac61ae 100644 --- a/Rare/Tabs/GamesInstalled/InstalledList.py +++ b/Rare/Tabs/GamesInstalled/InstalledList.py @@ -9,7 +9,7 @@ from legendary.core import LegendaryCore from Rare.Tabs.GamesInstalled.GameWidget import GameWidget from Rare.utils import legendaryUtils -from Rare.utils.Dialogs.SyncSavesDialog import SyncSavesDialog +from Rare.utils.Dialogs.SyncSaves.SyncSavesDialog import SyncSavesDialog logger = getLogger("InstalledList") diff --git a/Rare/utils/Dialogs/PathInputDialog.py b/Rare/utils/Dialogs/PathInputDialog.py new file mode 100644 index 00000000..bd679af6 --- /dev/null +++ b/Rare/utils/Dialogs/PathInputDialog.py @@ -0,0 +1,48 @@ +from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QFileDialog, QApplication + +from Rare.ext.QtExtensions import CustomQLabel, PathEdit + +class DLG(QDialog): + def __init__(self): + super(DLG, self).__init__() + print("lol") + +class PathInputDialog(QDialog): + def __init__(self, title_text, text): + super().__init__() + self.path = "" + + self.setWindowTitle(title_text) + self.info_label = CustomQLabel(text) + self.info_label.setWordWrap(True) + + self.input = PathEdit("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() diff --git a/Rare/utils/Dialogs/SyncSaves/SyncSavesDialog.py b/Rare/utils/Dialogs/SyncSaves/SyncSavesDialog.py new file mode 100644 index 00000000..697e1799 --- /dev/null +++ b/Rare/utils/Dialogs/SyncSaves/SyncSavesDialog.py @@ -0,0 +1,111 @@ +from logging import getLogger + +from PyQt5.QtCore import QThread, pyqtSignal, QObjectCleanupHandler +from PyQt5.QtWidgets import * +from legendary.core import LegendaryCore +from legendary.models.game import SaveGameStatus + +from Rare.ext.QtExtensions import CustomQLabel, WaitingSpinner +from Rare.utils.Dialogs.PathInputDialog import PathInputDialog +from Rare.utils.Dialogs.SyncSaves.SyncWidget import SyncWidget + +logger = getLogger("Sync Saves") + + +class UploadThread(QThread): + signal = pyqtSignal(str) + + def __init__(self, args): + super(UploadThread, self).__init__() + self.args = args + + def run(self): + logger.info("Uplouding saves") + res, dt_remote, dt_local, save_path, latest_save = self.status[app_name] + self.core.upload_save(app_name, save_path, dt_local) + + +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 SyncGame(QWidget): + def __init__(self): + super(SyncGame, self).__init__() + + +# noinspection PyAttributeOutsideInit +class SyncSavesDialog(QDialog): + def __init__(self, core: LegendaryCore): + super(SyncSavesDialog, self).__init__() + self.core = core + layout = QVBoxLayout() + layout.addWidget(WaitingSpinner()) + layout.addWidget(QLabel("

Loading Cloud Saves

")) + self.setLayout(layout) + + 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() + QObjectCleanupHandler().add(self.layout()) + self.main_layout = QVBoxLayout() + self.title = CustomQLabel("

Cloud Saves

\nFound Saves for folowing Games") + self.sync_all_button = QPushButton("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("No Games Found", "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 + sync_widget = SyncWidget(igame, latest_save[igame.app_name], self.core) + self.main_layout.addWidget(sync_widget) + self.widgets.append(sync_widget) + + self.ok_button = QPushButton("Ok") + self.ok_button.clicked.connect(lambda: self.close()) + self.main_layout.addWidget(self.ok_button) + + self.setLayout(self.main_layout) + + 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("Found no savepath", + "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() diff --git a/Rare/utils/Dialogs/SyncSaves/SyncWidget.py b/Rare/utils/Dialogs/SyncSaves/SyncWidget.py new file mode 100644 index 00000000..477c3ec2 --- /dev/null +++ b/Rare/utils/Dialogs/SyncSaves/SyncWidget.py @@ -0,0 +1,167 @@ +import os +from logging import getLogger + +from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QHBoxLayout +from legendary.core import LegendaryCore +from legendary.models.game import InstalledGame, SaveGameStatus + +from Rare.ext.QtExtensions import CustomQLabel + + +class _UploadThread(QThread): + signal = pyqtSignal() + + def __init__(self, app_name, latest_save, save_path, core: LegendaryCore): + super(_UploadThread, 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.upload_save(self.app_name, self.latest_save.manifest_nam, self.save_path) + + +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.setObjectName("syncwidget") + self.setStyleSheet(""" + QWidget#syncwidget{ + border: 2px solid white; + } + """) + 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 = "PathNotFound" + 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, (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 = CustomQLabel(f"

{igame.title}

") + if dt_local: + local_save_date = CustomQLabel(f"Local Save date: {dt_local.strftime('%Y-%m-%d %H:%M:%S')}") + else: + local_save_date = CustomQLabel("No Local Save files") + if dt_remote: + cloud_save_date = CustomQLabel(f"Cloud save date: {dt_remote.strftime('%Y-%m-%d %H:%M:%S')}") + else: + cloud_save_date = CustomQLabel(f"No Cloud saves") + + if self.res == SaveGameStatus.SAME_AGE: + self.logger.info(f'Save game for "{igame.title}" is up to date') + status = "Game is up to date" + self.upload_button = QPushButton("Upload anyway") + self.download_button = QPushButton("Download anyway") + if self.res == SaveGameStatus.REMOTE_NEWER: + status = "Cloud save is newer" + self.download_button = QPushButton("Download Cloud saves") + self.download_button.setStyleSheet(""" + QPushButton{ background-color: lime} + """) + self.upload_button = QPushButton("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 dt_local: + self.logger.info(f'- Local save date: {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 = "Local save is newer" + self.upload_button = QPushButton("Upload saves") + self.upload_button.setStyleSheet(""" + QPushButton{ background-color: lime} + """) + self.download_button = QPushButton("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: {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 = CustomQLabel(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 = CustomQLabel(igame.save_path) + self.change_save_path = QPushButton("Change path") + save_path_layout.addWidget(self.save_path_text) + save_path_layout.addWidget(self.change_save_path) + 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("Uploading...") + self.upload_button.setDisabled(True) + self.download_button.setDisabled(True) + thr = _UploadThread(self.igame.app_name, self.save, self.igame.save_path, self.core) + thr.finished.connect(self.uploaded) + thr.start() + + def uploaded(self): + self.info_text.setText("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("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("Download finished") + self.upload_button.setDisabled(True) + self.download_button.setDisabled(True) + self.download_button.setStyleSheet("QPushButton{background-color: black}") diff --git a/Rare/utils/Dialogs/SyncSaves/__init__.py b/Rare/utils/Dialogs/SyncSaves/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Rare/utils/Dialogs/SyncSavesDialog.py b/Rare/utils/Dialogs/SyncSavesDialog.py deleted file mode 100644 index 54320048..00000000 --- a/Rare/utils/Dialogs/SyncSavesDialog.py +++ /dev/null @@ -1,161 +0,0 @@ -import os -from logging import getLogger - -from PyQt5.QtCore import QThread, pyqtSignal, QObjectCleanupHandler -from PyQt5.QtWidgets import * -from legendary.core import LegendaryCore -from legendary.models.game import SaveGameStatus - -from Rare.ext.QtExtensions import CustomQLabel, 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 SyncGame(QWidget): - def __init__(self): - super(SyncGame, self).__init__() - - -class SyncSavesDialog(QDialog): - def __init__(self, core: LegendaryCore): - super(SyncSavesDialog, self).__init__() - self.core = core - layout = QVBoxLayout() - layout.addWidget(WaitingSpinner()) - layout.addWidget(QLabel("

Loading Cloud Saves

")) - self.setLayout(layout) - - 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() - # self.igames = self.core.get_installed_list() - # self.saves = self.core.get_save_games() - # latest_save = dict() - - # for save in sorted(self.saves, key=lambda a: a.datetime): - # latest_save[save.app_name] = save - - # self.setup_ui() - - def setup_ui(self, saves: list): - self.start_thread.disconnect() - QObjectCleanupHandler().add(self.layout()) - self.main_layout = QVBoxLayout() - self.title = CustomQLabel("

Cloud Saves

\nFound Saves for folowing Games") - self.sync_all_button = QPushButton("Sync all games") - self.main_layout.addWidget(self.title) - self.main_layout.addWidget(self.sync_all_button) - self.status = {} - - 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("No Games Found", "Your games don't support cloud save") - self.close() - return - for igame in self.igames: - game = self.core.get_game(igame.app_name) - if not game.supports_cloud_saves: - continue - logger.info(f'Checking "{igame.title}" ({igame.app_name})') - - if not igame.save_path: - save_path = self.core.get_save_path(igame.app_name) - if '%' in save_path or '{' in save_path: - status = "PathNotFound" - 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) - - res, (dt_local, dt_remote) = self.core.check_savegame_state(igame.save_path, - latest_save.get(igame.app_name)) - if res == SaveGameStatus.NO_SAVE: - logger.info('No cloud or local savegame found.') - continue - widget = QWidget() - layout = QVBoxLayout() - game_title = CustomQLabel(f"

{igame.title}

") - local_save_date = CustomQLabel(f"Local Save date: {dt_local.strftime('%Y-%m-%d %H:%M:%S')}") - cloud_save_date = CustomQLabel(f"Cloud save date: {dt_remote.strftime('%Y-%m-%d %H:%M:%S')}") - - if res == SaveGameStatus.SAME_AGE: - logger.info(f'Save game for "{igame.title}" is up to date') - status = "Game is up to date" - upload_button = QPushButton("Upload anyway") - download_button = QPushButton("Download anyway") - if res == SaveGameStatus.REMOTE_NEWER: - status = "Cloud save is newer" - download_button = QPushButton("Download Cloud saves") - download_button.setStyleSheet(""" - QPushButton{ background-color: lime} - """) - upload_button = QPushButton("Upload Saves") - logger.info(f'Cloud save for "{igame.title}" is newer:') - logger.info(f'- Cloud save date: {dt_remote.strftime("%Y-%m-%d %H:%M:%S")}') - if dt_local: - logger.info(f'- Local save date: {dt_local.strftime("%Y-%m-%d %H:%M:%S")}') - else: - logger.info('- Local save date: N/A') - upload_button.setDisabled(True) - upload_button.setToolTip("No local save") - - elif res == SaveGameStatus.LOCAL_NEWER: - status = "local is newer" - upload_button = QPushButton("Upload saves") - upload_button.setStyleSheet(""" - QPushButton{ background-color: lime} - """) - download_button = QPushButton("Download saves") - logger.info(f'Local save for "{igame.title}" is newer') - if dt_remote: - logger.info(f'- Cloud save date: {dt_remote.strftime("%Y-%m-%d %H:%M:%S")}') - else: - logger.info('- Cloud save date: N/A') - download_button.setDisabled(True) - logger.info(f'- Local save date: {dt_local.strftime("%Y-%m-%d %H:%M:%S")}') - upload_button.app_name = igame.app_name - download_button.app_name = igame.app_name - upload_button.clicked.connect(lambda: self.upload(upload_button.app_name)) - download_button.clicked.connect(lambda: self.download_save(download_button.app_name)) - mini_layout = QHBoxLayout() - mini_layout.addWidget(upload_button) - mini_layout.addWidget(download_button) - layout.addWidget(game_title) - layout.addWidget(local_save_date) - layout.addWidget(cloud_save_date) - layout.addWidget(CustomQLabel(status)) - layout.addLayout(mini_layout) - self.main_layout.addLayout(layout) - self.status[igame.app_name] = res, dt_remote, dt_local, igame.save_path, latest_save[igame.app_name] - - self.setLayout(self.main_layout) - - def download_save(self, app_name): - logger.info('Downloading saves ') - res, dt_remote, dt_local, save_path, latest_save = self.status[app_name] - self.core.download_saves(app_name, latest_save.manifest_name, save_path) - # todo Threading - - def upload(self, app_name): - logger.info("Uplouding saves") - res, dt_remote, dt_local, save_path, latest_save = self.status[app_name] - self.core.upload_save(app_name, save_path, dt_local)