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)