diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5df27713..7da7f6ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,6 @@ ## What you can do -To Contribute first fork the repository - ### Add translations 1. Execute ```pylupdate5 $(find -name "*.py") -ts Rare/languages/{your lang (two letters)}.ts``` in project directrory @@ -21,4 +19,7 @@ exmples: Select one Card of the project and implement it or make other changes -If you made your changes, create a pull request +##Git crash-course +To contribute fork the repository and clone **your** repo. Then make your changes, add it to git with `git add .` and upload it to Github with `git commit -m "message"` and `git push` or with your IDE. + +If you uploaded your changes, create a pull request diff --git a/README.md b/README.md index bf56a771..f8d3df82 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ There are more options to contribute. - If you are a designer, you can add Stylesheets or create a logo or a banner - You can translate the application +**Note:** Pull Requests please to "dev" branch + More Information is in CONTRIBUTING.md ## Images diff --git a/Rare/Components/Dialogs/Login/ImportWidget.py b/Rare/Components/Dialogs/Login/ImportWidget.py index a526c39a..a54c3a46 100644 --- a/Rare/Components/Dialogs/Login/ImportWidget.py +++ b/Rare/Components/Dialogs/Login/ImportWidget.py @@ -88,12 +88,15 @@ class ImportWidget(QWidget): if os.name != "nt": self.core.egl.appdata_path = os.path.join(self.data_path, f"drive_c/users/{getuser()}/Local Settings/Application Data/EpicGamesLauncher/Saved/Config/Windows") + try: + if self.core.auth_import(): + logger.info(f"Logged in as {self.core.lgd.userdata['displayName']}") + self.success.emit() + else: + logger.warning("Failed to import existing session") + except Exception as e: + logger.warning(e) - if self.core.auth_import(): - logger.info(f"Logged in as {self.core.lgd.userdata['displayName']}") - self.success.emit() - else: - print("Lol") logger.warning("Error: No valid session found") self.login_text.setText(self.tr("Error: No valid session found")) self.import_button.setText(self.tr("Import")) diff --git a/Rare/Components/Dialogs/UninstallDialog.py b/Rare/Components/Dialogs/UninstallDialog.py new file mode 100644 index 00000000..3a18248d --- /dev/null +++ b/Rare/Components/Dialogs/UninstallDialog.py @@ -0,0 +1,44 @@ +from PyQt5.QtWidgets import QDialog, QLabel, QVBoxLayout, QCheckBox, QFormLayout, QHBoxLayout, QPushButton +from qtawesome import icon + +from custom_legendary.models.game import Game + + +class UninstallDialog(QDialog): + def __init__(self, game: Game): + super(UninstallDialog, self).__init__() + self.setWindowTitle("Uninstall Game") + self.info = 0 + self.layout = QVBoxLayout() + self.info_text = QLabel(self.tr("Do you really want to uninstall {}").format(game.app_title)) + self.layout.addWidget(self.info_text) + self.keep_files = QCheckBox(self.tr("Keep Files")) + self.form = QFormLayout() + self.form.setContentsMargins(0, 10, 0, 10) + self.form.addRow(QLabel(self.tr("Do you want to keep files?")), self.keep_files) + self.layout.addLayout(self.form) + + self.button_layout = QHBoxLayout() + self.ok_button = QPushButton(icon("ei.remove-circle", color="red"), self.tr("Uninstall")) + self.ok_button.clicked.connect(self.ok) + + self.cancel_button = QPushButton(self.tr("Cancel")) + self.cancel_button.clicked.connect(self.cancel) + + self.button_layout.addStretch(1) + self.button_layout.addWidget(self.ok_button) + self.button_layout.addWidget(self.cancel_button) + self.layout.addLayout(self.button_layout) + self.setLayout(self.layout) + + def get_information(self): + self.exec_() + return self.info + + def ok(self): + self.info = {"keep_files": self.keep_files.isChecked()} + self.close() + + def cancel(self): + self.info = 0 + self.close() diff --git a/Rare/Components/MainWindow.py b/Rare/Components/MainWindow.py index a0116212..d22c9ea9 100644 --- a/Rare/Components/MainWindow.py +++ b/Rare/Components/MainWindow.py @@ -1,4 +1,6 @@ -from PyQt5.QtWidgets import QMainWindow +from PyQt5.QtCore import QSettings +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtWidgets import QMainWindow, QMessageBox from Rare.Components.TabWidget import TabWidget @@ -8,5 +10,15 @@ class MainWindow(QMainWindow): super(MainWindow, self).__init__() self.setGeometry(0, 0, 1200, 800) self.setWindowTitle("Rare - GUI for legendary") - self.setCentralWidget(TabWidget(core)) + self.tab_widget = TabWidget(core) + self.setCentralWidget(self.tab_widget) self.show() + + def closeEvent(self, e: QCloseEvent): + if QSettings().value("sys_tray", True, bool): + self.hide() + e.ignore() + elif self.tab_widget.downloadTab.active_game is not None and QMessageBox.question(self, "Close", self.tr("There is a download active. Do you really want to exit app?"), QMessageBox.Yes, QMessageBox.No) == QMessageBox.Yes: + e.accept() + else: + e.accept() diff --git a/Rare/Components/TabWidget.py b/Rare/Components/TabWidget.py index 3be3189e..26b5adab 100644 --- a/Rare/Components/TabWidget.py +++ b/Rare/Components/TabWidget.py @@ -3,10 +3,10 @@ from PyQt5.QtWidgets import QTabWidget, QWidget from qtawesome import icon from Rare.Components.TabUtils import TabBar, TabButtonWidget -from Rare.Components.Tabs.CloudSaves.CloudSaves import SyncSaves +from Rare.Components.Tabs.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 +from Rare.Components.Tabs.Games import GameTab +from Rare.Components.Tabs.Settings import SettingsTab from Rare.utils.Models import InstallOptions from custom_legendary.core import LegendaryCore @@ -23,7 +23,7 @@ class TabWidget(QTabWidget): self.addTab(self.game_list, self.tr("Games")) self.downloadTab = DownloadTab(core, updates) self.addTab(self.downloadTab, "Downloads" + (" (" + str(len(updates)) + ")" if len(updates) != 0 else "")) - self.downloadTab.finished.connect(self.game_list.default_widget.game_list.update_list) + self.downloadTab.finished.connect(self.dl_finished) self.game_list.default_widget.game_list.install_game.connect(lambda x: self.downloadTab.install_game(x)) self.game_list.game_info.info.verify_game.connect(lambda app_name: self.downloadTab.install_game( @@ -31,7 +31,6 @@ class TabWidget(QTabWidget): self.tabBarClicked.connect(lambda x: self.game_list.layout.setCurrentIndex(0) if x == 0 else None) - # Commented, because it is not finished self.cloud_saves = SyncSaves(core) self.addTab(self.cloud_saves, "Cloud Saves") @@ -43,10 +42,15 @@ class TabWidget(QTabWidget): self.addTab(self.account, "") self.setTabEnabled(disabled_tab + 1, False) # self.settings = SettingsTab(core) - self.addTab(self.settings, icon("fa.gear", color='white'), None) + + self.addTab(self.settings, icon("fa.gear", color='white'), "(!)" if self.settings.about.update_available else "") self.setIconSize(QSize(25, 25)) self.tabBar().setTabButton(3, self.tabBar().RightSide, TabButtonWidget(core)) + def dl_finished(self): + self.game_list.default_widget.game_list.update_list() + self.setTabText(1, "Downloads") + def resizeEvent(self, event): self.tabBar().setMinimumWidth(self.width()) super(TabWidget, self).resizeEvent(event) diff --git a/Rare/Components/Tabs/Account/AccountWidget.py b/Rare/Components/Tabs/Account/AccountWidget.py index 9d16b589..8aff5de0 100644 --- a/Rare/Components/Tabs/Account/AccountWidget.py +++ b/Rare/Components/Tabs/Account/AccountWidget.py @@ -12,15 +12,16 @@ class MiniWidget(QWidget): self.layout = QVBoxLayout() self.core = core self.layout.addWidget(QLabel("Account")) - username = str(self.core.lgd.userdata.get("display_name")) + username = self.core.lgd.userdata.get("display_name") if not username: self.core.login() - username = str(self.core.lgd.userdata.get("display_name")) + username = self.core.lgd.userdata.get("display_name") - self.layout.addWidget(QLabel(self.tr("Logged in as ") + username)) + self.layout.addWidget(QLabel(self.tr("Logged in as ") + str(username))) self.open_browser = QPushButton(self.tr("Account settings")) - self.open_browser.clicked.connect(self.open_account) + self.open_browser.clicked.connect( + lambda: webbrowser.open("https://www.epicgames.com/account/personal?productName=epicgames")) self.layout.addWidget(self.open_browser) self.logout_button = QPushButton(self.tr("Logout")) @@ -35,7 +36,5 @@ class MiniWidget(QWidget): if reply == QMessageBox.Yes: self.core.lgd.invalidate_userdata() - QCoreApplication.exit() - - def open_account(self): - webbrowser.open("https://www.epicgames.com/account/personal?productName=epicgames") + # restart app + QCoreApplication.instance().exit_action(-133742) # restart exit code diff --git a/Rare/Components/Tabs/CloudSaves/CloudSaves.py b/Rare/Components/Tabs/CloudSaves/CloudSaves.py deleted file mode 100644 index 72b52b6f..00000000 --- a/Rare/Components/Tabs/CloudSaves/CloudSaves.py +++ /dev/null @@ -1,123 +0,0 @@ -from logging import getLogger - -from PyQt5.QtCore import QThread, pyqtSignal, Qt -from PyQt5.QtWidgets import * - -from Rare.Components.Dialogs.PathInputDialog import PathInputDialog -from Rare.Components.Tabs.CloudSaves.SyncWidget import SyncWidget -from Rare.utils.QtExtensions import WaitingSpinner -from custom_legendary.core import LegendaryCore -from custom_legendary.models.game import SaveGameStatus - -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.load_saves() - - def load_saves(self): - 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.main_layout.addWidget(self.title) - - saves_games = [] - for i in saves: - if not i.app_name in saves_games and self.core.is_installed(i.app_name): - saves_games.append(i.app_name) - if len(saves_games) == 0: - # QMessageBox.information(self.tr("No Games Found"), self.tr("Your games don't support cloud save")) - self.title.setText( - f"

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

\n" + self.tr("Your games does not support Cloud Saves")) - self.setWidget(self.title) - return - - self.sync_all_button = QPushButton(self.tr("Sync all games")) - self.sync_all_button.clicked.connect(self.sync_all) - self.main_layout.addWidget(self.sync_all_button) - - latest_save = {} - for i in sorted(saves, key=lambda a: a.datetime): - latest_save[i.app_name] = i - - logger.info(f'Got {len(latest_save)} remote save game(s)') - - 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 - sync_widget.reload.connect(self.reload) - 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 reload(self): - self.setWidget(QWidget()) - self.load_saves() - self.update() - - def sync_all(self): - logger.info("Sync all Games") - 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_label("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: - logger.info("Download") - w.download() - elif w.res == SaveGameStatus.LOCAL_NEWER: - logger.info("Upload") - w.upload() diff --git a/Rare/Components/Tabs/CloudSaves/SyncWidget.py b/Rare/Components/Tabs/CloudSaves/SyncWidget.py index e19ce92b..1f24795c 100644 --- a/Rare/Components/Tabs/CloudSaves/SyncWidget.py +++ b/Rare/Components/Tabs/CloudSaves/SyncWidget.py @@ -1,8 +1,8 @@ import os from logging import getLogger -from PyQt5.QtCore import QThread, pyqtSignal -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton, QHBoxLayout, QLabel +from PyQt5.QtCore import QThread, pyqtSignal, Qt +from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QHBoxLayout, QLabel, QGroupBox from Rare.Components.Dialogs.PathInputDialog import PathInputDialog from custom_legendary.core import LegendaryCore @@ -37,12 +37,14 @@ class _DownloadThread(QThread): self.core.download_saves(self.app_name, self.latest_save.manifest_name, self.save_path, clean_dir=True) -class SyncWidget(QWidget): +class SyncWidget(QGroupBox): reload = pyqtSignal() def __init__(self, igame: InstalledGame, save, core: LegendaryCore): - super(SyncWidget, self).__init__() + super(SyncWidget, self).__init__(igame.title) + self.setObjectName("group") self.layout = QVBoxLayout() + self.setContentsMargins(10, 20, 10, 20) self.thr = None self.core = core self.save = save @@ -70,7 +72,7 @@ class SyncWidget(QWidget): self.logger.info('No cloud or local savegame found.') return - game_title = QLabel(f"

{igame.title}

") + # game_title = QLabel(f"

{igame.title}

") if self.dt_local: local_save_date = QLabel( @@ -124,14 +126,16 @@ class SyncWidget(QWidget): 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(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.setTextInteractionFlags(Qt.TextSelectableByMouse) self.save_path_text.setWordWrap(True) self.change_save_path = QPushButton(self.tr("Change path")) + self.change_save_path.setFixedWidth(100) self.change_save_path.clicked.connect(self.change_path) save_path_layout.addWidget(self.save_path_text) save_path_layout.addWidget(self.change_save_path) @@ -141,7 +145,7 @@ class SyncWidget(QWidget): button_layout.addWidget(self.upload_button) button_layout.addWidget(self.download_button) self.layout.addLayout(button_layout) - + self.layout.addStretch(1) self.setLayout(self.layout) def change_path(self): diff --git a/Rare/Components/Tabs/CloudSaves/__init__.py b/Rare/Components/Tabs/CloudSaves/__init__.py index e69de29b..9dc45abb 100644 --- a/Rare/Components/Tabs/CloudSaves/__init__.py +++ b/Rare/Components/Tabs/CloudSaves/__init__.py @@ -0,0 +1,121 @@ +from logging import getLogger + +from PyQt5.QtCore import QThread, pyqtSignal, Qt +from PyQt5.QtWidgets import * + +from Rare.Components.Dialogs.PathInputDialog import PathInputDialog +from Rare.Components.Tabs.CloudSaves.SyncWidget import SyncWidget +from Rare.utils.QtExtensions import WaitingSpinner +from custom_legendary.core import LegendaryCore +from custom_legendary.models.game import SaveGameStatus + +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.load_saves() + + def load_saves(self): + 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.main_layout.addWidget(self.title) + + saves_games = [] + for i in saves: + if not i.app_name in saves_games and self.core.is_installed(i.app_name): + saves_games.append(i.app_name) + if len(saves_games) == 0: + # QMessageBox.information(self.tr("No Games Found"), self.tr("Your games don't support cloud save")) + self.title.setText( + f"

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

\n" + self.tr("Your games does not support Cloud Saves")) + self.setWidget(self.title) + return + + self.sync_all_button = QPushButton(self.tr("Sync all games")) + self.sync_all_button.clicked.connect(self.sync_all) + self.main_layout.addWidget(self.sync_all_button) + + latest_save = {} + for i in sorted(saves, key=lambda a: a.datetime): + latest_save[i.app_name] = i + + logger.info(f'Got {len(latest_save)} remote save game(s)') + + 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 + sync_widget.reload.connect(self.reload) + self.main_layout.addWidget(sync_widget) + self.widgets.append(sync_widget) + + self.widget = QWidget() + self.main_layout.addStretch(1) + self.widget.setLayout(self.main_layout) + self.setWidgetResizable(True) + self.setWidget(self.widget) + + def reload(self): + self.setWidget(QWidget()) + self.load_saves() + self.update() + + def sync_all(self): + logger.info("Sync all Games") + 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_label("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: + logger.info("Download") + w.download() + elif w.res == SaveGameStatus.LOCAL_NEWER: + logger.info("Upload") + w.upload() diff --git a/Rare/Components/Tabs/Downloads/DlQueueWidget.py b/Rare/Components/Tabs/Downloads/DlQueueWidget.py new file mode 100644 index 00000000..943a48b2 --- /dev/null +++ b/Rare/Components/Tabs/Downloads/DlQueueWidget.py @@ -0,0 +1,131 @@ +from logging import getLogger + +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QGroupBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QWidget +from qtawesome import icon + +logger = getLogger("QueueWidget") + + +class DlWidget(QWidget): + move_up = pyqtSignal(str) # app_name + move_down = pyqtSignal(str) # app_name + remove = pyqtSignal(str) # app_name + + def __init__(self, index, item): + super(DlWidget, self).__init__() + self.app_name = item[1].app_name + self.layout = QHBoxLayout() + + self.left_layout = QVBoxLayout() + self.move_up_button = QPushButton(icon("fa.arrow-up", color="white"), "") + if index == 0: + self.move_up_button.setDisabled(True) + self.move_up_button.clicked.connect(lambda: self.move_up.emit(self.app_name)) + self.move_up_button.setFixedWidth(20) + self.left_layout.addWidget(self.move_up_button) + + self.move_down_buttton = QPushButton(icon("fa.arrow-down", color="white"), "") + self.move_down_buttton.clicked.connect(lambda: self.move_down.emit(self.app_name)) + self.left_layout.addWidget(self.move_down_buttton) + self.move_down_buttton.setFixedWidth(20) + + self.layout.addLayout(self.left_layout) + + self.right_layout = QVBoxLayout() + self.title = QLabel(item[1].app_title) + self.right_layout.addWidget(self.title) + + dl_size = item[-1].dl_size + install_size = item[-1].install_size + + self.size = QHBoxLayout() + + self.size.addWidget(QLabel(self.tr("Download size: {} GB").format(dl_size / 1024 ** 3))) + self.size.addWidget(QLabel(self.tr("Install size: {} GB").format(install_size / 1024 ** 3))) + self.right_layout.addLayout(self.size) + + self.delete = QPushButton(self.tr("Remove Download")) + self.delete.clicked.connect(lambda: self.remove.emit(self.app_name)) + self.right_layout.addWidget(self.delete) + + self.layout.addLayout(self.right_layout) + self.setLayout(self.layout) + + +class DlQueueWidget(QGroupBox): + update_list = pyqtSignal(list) + dl_queue = [] + + def __init__(self): + + super(DlQueueWidget, self).__init__() + self.setTitle(self.tr("Download Queue")) + self.layout = QVBoxLayout() + self.setObjectName("group") + self.text = QLabel(self.tr("No downloads in queue")) + self.layout.addWidget(self.text) + + self.setLayout(self.layout) + + def update_queue(self, dl_queue: list): + logger.info("Update Queue " + ", ".join(i[1].app_title for i in dl_queue)) + self.dl_queue = dl_queue + self.setLayout(QVBoxLayout()) + QWidget().setLayout(self.layout) + self.layout = QVBoxLayout() + + if len(dl_queue) == 0: + self.layout.addWidget(QLabel(self.tr("No downloads in queue"))) + self.setLayout(self.layout) + return + + for index, item in enumerate(dl_queue): + widget = DlWidget(index, item) + widget.remove.connect(self.remove) + widget.move_up.connect(self.move_up) + widget.move_down.connect(self.move_down) + self.layout.addWidget(widget) + if index + 1 == len(dl_queue): + widget.move_down_buttton.setDisabled(True) + + self.setLayout(self.layout) + + def remove(self, app_name): + for index, i in enumerate(self.dl_queue): + if i[1].app_name == app_name: + self.dl_queue.pop(index) + break + else: + logger.warning("BUG! ", app_name) + return + self.update_list.emit(self.dl_queue) + self.update_queue(self.dl_queue) + + def move_up(self, app_name): + index: int + + for i, item in enumerate(self.dl_queue): + if item[1].app_name == app_name: + index = i + break + else: + logger.warning("Could not find appname" + app_name) + return + self.dl_queue.insert(index - 1, self.dl_queue.pop(index)) + self.update_list.emit(self.dl_queue) + self.update_queue(self.dl_queue) + + def move_down(self, app_name): + index: int + + for i, item in enumerate(self.dl_queue): + if item[1].app_name == app_name: + index = i + break + else: + logger.warning("Could not find appname" + app_name) + return + self.dl_queue.insert(index + 1, self.dl_queue.pop(index)) + self.update_list.emit(self.dl_queue) + self.update_queue(self.dl_queue) diff --git a/Rare/Components/Tabs/Downloads/DownloadTab.py b/Rare/Components/Tabs/Downloads/DownloadTab.py index bad38222..5da62057 100644 --- a/Rare/Components/Tabs/Downloads/DownloadTab.py +++ b/Rare/Components/Tabs/Downloads/DownloadTab.py @@ -1,19 +1,16 @@ import datetime -import os -import queue -import subprocess -import time from logging import getLogger from multiprocessing import Queue as MPQueue from PyQt5.QtCore import QThread, pyqtSignal, Qt from PyQt5.QtWidgets import QWidget, QMessageBox, QVBoxLayout, QLabel, QGridLayout, QProgressBar, QPushButton, QDialog, \ - QListWidget, QHBoxLayout + QListWidget, QHBoxLayout, QGroupBox from Rare.Components.Dialogs.InstallDialog import InstallInfoDialog, InstallDialog -from Rare.utils.Models import InstallOptions, KillDownloadException +from Rare.Components.Tabs.Downloads.DlQueueWidget import DlQueueWidget +from Rare.Components.Tabs.Downloads.DownloadThread import DownloadThread +from Rare.utils.Models import InstallOptions from custom_legendary.core import LegendaryCore -from custom_legendary.downloader.manager import DLManager from custom_legendary.models.downloading import UIUpdate from custom_legendary.models.game import Game, InstalledGame from custom_legendary.utils.selective_dl import games @@ -21,108 +18,10 @@ from custom_legendary.utils.selective_dl import games logger = getLogger("Download") -class DownloadThread(QThread): - status = pyqtSignal(str) - statistics = pyqtSignal(UIUpdate) - kill = False - - def __init__(self, dlm: DLManager, core: LegendaryCore, status_queue: MPQueue, igame, repair=False, - repair_file=None): - super(DownloadThread, self).__init__() - self.dlm = dlm - self.core = core - self.status_queue = status_queue - self.igame = igame - self.repair = repair - self.repair_file = repair_file - - def run(self): - start_time = time.time() - try: - - self.dlm.start() - time.sleep(1) - while self.dlm.is_alive(): - if self.kill: - # raise KillDownloadException() - # TODO kill download queue, workers - pass - try: - self.statistics.emit(self.status_queue.get(timeout=1)) - except queue.Empty: - pass - - self.dlm.join() - - except KillDownloadException: - self.status.emit("stop") - logger.info("Downlaod can be continued later") - self.dlm.kill() - return - - except Exception as e: - logger.error(f"Installation failed after {time.time() - start_time:.02f} seconds: {e}") - self.status.emit("error") - return - - else: - self.status.emit("dl_finished") - end_t = time.time() - - game = self.core.get_game(self.igame.app_name) - postinstall = self.core.install_game(self.igame) - if postinstall: - self._handle_postinstall(postinstall, self.igame) - - dlcs = self.core.get_dlc_for_game(self.igame.app_name) - if dlcs: - print('The following DLCs are available for this game:') - for dlc in dlcs: - print(f' - {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})') - print('Manually installing DLCs works the same; just use the DLC app name instead.') - - # install_dlcs = QMessageBox.question(self, "", "Do you want to install the prequisites", QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes - # TODO - if game.supports_cloud_saves and not game.is_dlc: - logger.info('This game supports cloud saves, syncing is handled by the "sync-saves" command.') - logger.info(f'To download saves for this game run "legendary sync-saves {game.app_name}"') - old_igame = self.core.get_installed_game(game.app_name) - if old_igame and self.repair and os.path.exists(self.repair_file): - if old_igame.needs_verification: - old_igame.needs_verification = False - self.core.install_game(old_igame) - - logger.debug('Removing repair file.') - os.remove(self.repair_file) - if old_igame and old_igame.install_tags != self.igame.install_tags: - old_igame.install_tags = self.igame.install_tags - self.logger.info('Deleting now untagged files.') - self.core.uninstall_tag(old_igame) - self.core.install_game(old_igame) - - self.status.emit("finish") - - def _handle_postinstall(self, postinstall, igame): - print('This game lists the following prequisites to be installed:') - print(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}') - if os.name == 'nt': - if QMessageBox.question(self, "", "Do you want to install the prequisites", - QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: - self.core.prereq_installed(igame.app_name) - req_path, req_exec = os.path.split(postinstall['path']) - work_dir = os.path.join(igame.install_path, req_path) - fullpath = os.path.join(work_dir, req_exec) - subprocess.call([fullpath, postinstall['args']], cwd=work_dir) - else: - self.core.prereq_installed(self.igame.app_name) - - else: - logger.info('Automatic installation not available on Linux.') - - class DownloadTab(QWidget): finished = pyqtSignal() thread: QThread + dl_queue = [] def __init__(self, core: LegendaryCore, updates: list): super(DownloadTab, self).__init__() @@ -146,7 +45,6 @@ class DownloadTab(QWidget): self.layout.addLayout(self.info_layout) self.mini_layout = QHBoxLayout() - self.prog_bar = QProgressBar() self.prog_bar.setMaximum(100) self.mini_layout.addWidget(self.prog_bar) @@ -158,33 +56,39 @@ class DownloadTab(QWidget): self.layout.addLayout(self.mini_layout) - self.update_title = QLabel(f"

{self.tr('Updates')}

") - self.update_title.setStyleSheet(""" - QLabel{ - margin-top: 20px; - } - """) - self.layout.addWidget(self.update_title) - self.update_widgets = {} - if not updates: - self.update_text = QLabel(self.tr("No updates available")) - self.layout.addWidget(self.update_text) - else: - for igame in updates: - widget = UpdateWidget(core, igame) - self.update_widgets[igame.app_name] = widget - self.layout.addWidget(widget) - widget.update.connect(self.update_game) + self.queue_widget = DlQueueWidget() + self.layout.addWidget(self.queue_widget) + self.queue_widget.update_list.connect(self.update_dl_queue) + self.updates = QGroupBox(self.tr("Updates")) + self.updates.setObjectName("group") + self.update_layout = QVBoxLayout() + self.update_widgets = {} + + self.update_text = QLabel(self.tr("No updates available")) + self.update_text.setVisible(len(updates) == 0) + self.update_layout.addWidget(self.update_text) + + for igame in updates: + widget = UpdateWidget(core, igame) + self.update_widgets[igame.app_name] = widget + self.update_layout.addWidget(widget) + widget.update.connect(self.update_game) + + self.updates.setLayout(self.update_layout) + self.layout.addWidget(self.updates) self.layout.addStretch(1) self.setLayout(self.layout) + def update_dl_queue(self, dl_queue): + self.dl_queue = dl_queue + def stop_download(self): self.thread.kill = True def install_game(self, options: InstallOptions): - game = self.core.get_game(options.app_name, update_meta=True) + status_queue = MPQueue() try: dlm, analysis, game, igame, repair, repair_file = self.core.prepare_download( @@ -221,21 +125,27 @@ class DownloadTab(QWidget): if not analysis.dl_size: QMessageBox.information(self, "Warning", self.tr("Download size is 0. Game already exists")) return + # Information + if not InstallInfoDialog(dl_size=analysis.dl_size, install_size=analysis.install_size).get_accept(): return + if self.active_game is None: + self.start_installation(dlm, game, status_queue, igame, repair_file, options, analysis) + else: + self.dl_queue.append((dlm, game, status_queue, igame, repair_file, options, analysis)) + self.queue_widget.update_queue(self.dl_queue) + + def start_installation(self, dlm, game, status_queue, igame, repair_file, options: InstallOptions, analysis): + print("start installation", game.app_title) self.active_game = game - self.installing_game_widget.setText(self.tr("Installing game: ")+self.active_game.app_title) self.thread = DownloadThread(dlm, self.core, status_queue, igame, options.repair, repair_file) self.thread.status.connect(self.status) self.thread.statistics.connect(self.statistics) self.thread.start() self.kill_button.setDisabled(False) - self.installing_game.setText("Installing Game: " + self.active_game.app_title) - - for i in self.update_widgets.values(): - i.update_button.setDisabled(True) + self.installing_game.setText(self.tr("Installing Game: ") + self.active_game.app_title) def sdl_prompt(self, app_name: str = '', title: str = '') -> list: sdl = QDialog() @@ -279,6 +189,8 @@ class DownloadTab(QWidget): if text == "dl_finished": pass elif text == "finish": + self.installing_game.setText(self.tr("Download finished. Reload library")) + try: from notifypy import Notify except ModuleNotFoundError: @@ -290,30 +202,43 @@ class DownloadTab(QWidget): notification.send() # QMessageBox.information(self, "Info", "Download finished") logger.info("Download finished: " + self.active_game.app_title) + + if self.dl_queue: + if self.dl_queue[0][1] == self.active_game.app_name: + self.dl_queue.pop(0) + self.queue_widget.update_queue(self.dl_queue) + if self.active_game.app_name in self.update_widgets.keys(): self.update_widgets[self.active_game.app_name].setVisible(False) self.update_widgets.pop(self.active_game.app_name) + self.active_game = None + for i in self.update_widgets.values(): i.update_button.setDisabled(False) self.finished.emit() - self.installing_game.setText(self.tr("Installing Game: No active download")) - self.prog_bar.setValue(0) + self.reset_infos() + + if len(self.dl_queue) != 0: + self.start_installation(*self.dl_queue[0]) + else: + self.queue_widget.update_queue(self.dl_queue) - self.dl_speed.setText("") - self.cache_used.setText("") - self.downloaded.setText("") elif text == "error": QMessageBox.warning(self, "warn", "Download error") elif text == "stop": - self.kill_button.setDisabled(True) - self.installing_game.setText(self.tr("Installing Game: No active download")) - self.prog_bar.setValue(0) - self.dl_speed.setText("") - self.cache_used.setText("") - self.downloaded.setText("") + self.reset_infos() + + def reset_infos(self): + self.kill_button.setDisabled(True) + self.installing_game.setText(self.tr("Installing Game: No active download")) + self.prog_bar.setValue(0) + self.dl_speed.setText("") + self.time_left.setText("") + self.cache_used.setText("") + self.downloaded.setText("") def statistics(self, ui_update: UIUpdate): self.prog_bar.setValue(ui_update.progress) @@ -326,23 +251,19 @@ class DownloadTab(QWidget): return str(datetime.timedelta(seconds=seconds)) def update_game(self, app_name: str): - print("Update ", app_name) + logger.info("Update " + app_name) infos = InstallDialog(app_name, self.core, True).get_information() if infos != 0: path, max_workers, force, ignore_free_space = infos self.install_game(InstallOptions(app_name=app_name, max_workers=max_workers, path=path, force=force, ignore_free_space=ignore_free_space)) - def repair(self): - pass - class UpdateWidget(QWidget): update = pyqtSignal(str) def __init__(self, core: LegendaryCore, game: InstalledGame): super(UpdateWidget, self).__init__() - print(game) self.core = core self.game = game diff --git a/Rare/Components/Tabs/Downloads/DownloadThread.py b/Rare/Components/Tabs/Downloads/DownloadThread.py new file mode 100644 index 00000000..3fce81ad --- /dev/null +++ b/Rare/Components/Tabs/Downloads/DownloadThread.py @@ -0,0 +1,117 @@ +import os +import queue +import subprocess +import time +from logging import getLogger +from multiprocessing import Queue as MPQueue + +from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtWidgets import QMessageBox + +from Rare.utils.Models import KillDownloadException + +from custom_legendary.core import LegendaryCore +from custom_legendary.downloader.manager import DLManager +from custom_legendary.models.downloading import UIUpdate + +logger = getLogger("Download") + + +class DownloadThread(QThread): + status = pyqtSignal(str) + statistics = pyqtSignal(UIUpdate) + kill = False + + def __init__(self, dlm: DLManager, core: LegendaryCore, status_queue: MPQueue, igame, repair=False, + repair_file=None): + super(DownloadThread, self).__init__() + self.dlm = dlm + self.core = core + self.status_queue = status_queue + self.igame = igame + self.repair = repair + self.repair_file = repair_file + + def run(self): + start_time = time.time() + try: + + self.dlm.start() + time.sleep(1) + while self.dlm.is_alive(): + if self.kill: + # raise KillDownloadException() + # TODO kill download queue, workers + pass + try: + self.statistics.emit(self.status_queue.get(timeout=1)) + except queue.Empty: + pass + + self.dlm.join() + + except KillDownloadException: + self.status.emit("stop") + logger.info("Downlaod can be continued later") + self.dlm.kill() + return + + except Exception as e: + logger.error(f"Installation failed after {time.time() - start_time:.02f} seconds: {e}") + self.status.emit("error") + return + + else: + self.status.emit("dl_finished") + end_t = time.time() + + game = self.core.get_game(self.igame.app_name) + postinstall = self.core.install_game(self.igame) + if postinstall: + self._handle_postinstall(postinstall, self.igame) + + dlcs = self.core.get_dlc_for_game(self.igame.app_name) + if dlcs: + print('The following DLCs are available for this game:') + for dlc in dlcs: + print(f' - {dlc.app_title} (App name: {dlc.app_name}, version: {dlc.app_version})') + print('Manually installing DLCs works the same; just use the DLC app name instead.') + + # install_dlcs = QMessageBox.question(self, "", "Do you want to install the prequisites", QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes + # TODO + if game.supports_cloud_saves and not game.is_dlc: + logger.info('This game supports cloud saves, syncing is handled by the "sync-saves" command.') + logger.info(f'To download saves for this game run "legendary sync-saves {game.app_name}"') + old_igame = self.core.get_installed_game(game.app_name) + if old_igame and self.repair and os.path.exists(self.repair_file): + if old_igame.needs_verification: + old_igame.needs_verification = False + self.core.install_game(old_igame) + + logger.debug('Removing repair file.') + os.remove(self.repair_file) + if old_igame and old_igame.install_tags != self.igame.install_tags: + old_igame.install_tags = self.igame.install_tags + self.logger.info('Deleting now untagged files.') + self.core.uninstall_tag(old_igame) + self.core.install_game(old_igame) + + self.status.emit("finish") + + def _handle_postinstall(self, postinstall, igame): + print('This game lists the following prequisites to be installed:') + print(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}') + if os.name == 'nt': + if QMessageBox.question(self, "", "Do you want to install the prequisites", + QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: + self.core.prereq_installed(igame.app_name) + req_path, req_exec = os.path.split(postinstall['path']) + work_dir = os.path.join(igame.install_path, req_path) + fullpath = os.path.join(work_dir, req_exec) + subprocess.call([fullpath, postinstall['args']], cwd=work_dir) + else: + self.core.prereq_installed(self.igame.app_name) + + else: + logger.info('Automatic installation not available on Linux.') + diff --git a/Rare/Components/Tabs/Games/GameInfo/GameInfo.py b/Rare/Components/Tabs/Games/GameInfo/GameInfo.py index 159ee834..b67c64e8 100644 --- a/Rare/Components/Tabs/Games/GameInfo/GameInfo.py +++ b/Rare/Components/Tabs/Games/GameInfo/GameInfo.py @@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QLabel, QHBoxLayo QProgressBar, QStackedWidget, QGroupBox from qtawesome import icon +from Rare.Components.Dialogs.UninstallDialog import UninstallDialog from Rare.Components.Tabs.Games.GameInfo.GameSettings import GameSettings from Rare.utils import LegendaryApi from Rare.utils.LegendaryApi import VerifyThread @@ -65,6 +66,7 @@ class GameInfo(QWidget): right_layout.addWidget(self.game_title) self.dev = QLabel("Error") + self.dev.setTextInteractionFlags(Qt.TextSelectableByMouse) right_layout.addWidget(self.dev) self.app_name = QLabel("Error") @@ -72,12 +74,14 @@ class GameInfo(QWidget): right_layout.addWidget(self.app_name) self.version = QLabel("Error") + self.version.setTextInteractionFlags(Qt.TextSelectableByMouse) right_layout.addWidget(self.version) self.install_size = QLabel("Error") right_layout.addWidget(self.install_size) self.install_path = QLabel("Error") + self.install_path.setTextInteractionFlags(Qt.TextSelectableByMouse) right_layout.addWidget(self.install_path) top_layout.addLayout(right_layout) @@ -95,10 +99,13 @@ class GameInfo(QWidget): self.setLayout(self.layout) def uninstall(self): - if QMessageBox.question(self, "Uninstall", self.tr("Are you sure to uninstall {}").format(self.game.app_title), - QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: - LegendaryApi.uninstall(self.game.app_name, self.core) - self.update_list.emit() + infos = UninstallDialog(self.game).get_information() + if infos == 0: + print("Cancel Uninstall") + return + + LegendaryApi.uninstall(self.game.app_name, self.core, infos) + self.update_list.emit() def repair(self): repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{self.game.app_name}.repair') diff --git a/Rare/Components/Tabs/Games/GameList.py b/Rare/Components/Tabs/Games/GameList.py index 183dc193..884e3788 100644 --- a/Rare/Components/Tabs/Games/GameList.py +++ b/Rare/Components/Tabs/Games/GameList.py @@ -40,6 +40,7 @@ class GameList(QStackedWidget): self.icon_scrollarea.setWidgetResizable(True) self.icon_scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.list_scrollarea.setWidgetResizable(True) self.list_scrollarea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.info_text = self.tr("Installed Games: {} Available Games: {}").format( @@ -53,7 +54,7 @@ class GameList(QStackedWidget): self.list_layout = QVBoxLayout() self.list_layout.addWidget(QLabel(self.info_text)) - IMAGE_DIR = self.settings.value("img_dir", os.path.expanduser("~/.cache/rare"), str) + IMAGE_DIR = self.settings.value("img_dir", os.path.expanduser("~/.cache/rare/images"), str) self.updates = [] self.widgets = {} @@ -80,7 +81,7 @@ class GameList(QStackedWidget): icon_widget.show_info.connect(self.show_game_info.emit) list_widget.show_info.connect(self.show_game_info.emit) - icon_widget.launch_signal.connect(self.launch) + icon_widget.finish_signal.connect(self.launch) icon_widget.finish_signal.connect(self.finished) list_widget.launch_signal.connect(self.launch) list_widget.finish_signal.connect(self.finished) @@ -100,6 +101,7 @@ class GameList(QStackedWidget): if not game.app_name in installed: uninstalled_games.append(game) + # add uninstalled games for game in uninstalled_games: if os.path.exists(f"{IMAGE_DIR}/{game.app_name}/UninstalledArt.png"): pixmap = QPixmap(f"{IMAGE_DIR}/{game.app_name}/UninstalledArt.png") @@ -110,15 +112,15 @@ class GameList(QStackedWidget): pixmap = QPixmap(f"{IMAGE_DIR}/{game.app_name}/UninstalledArt.png") else: - logger.warning(f"No Image found: {self.game.app_title}") + logger.warning(f"No Image found: {game.app_title}") download_image(game, force=True) pixmap = QPixmap(f"{IMAGE_DIR}/{game.app_name}/UninstalledArt.png") icon_widget = IconWidgetUninstalled(game, self.core, pixmap) - icon_widget.install_game.connect(self.install_game.emit) + icon_widget.install_game.connect(self.install) list_widget = ListWidgetUninstalled(self.core, game, pixmap) - list_widget.install_game.connect(self.install_game.emit) + list_widget.install_game.connect(self.install) self.icon_layout.addWidget(icon_widget) self.list_layout.addWidget(list_widget) @@ -143,6 +145,14 @@ class GameList(QStackedWidget): if self.settings.value("installed_only", False, bool): self.installed_only(True) + def install(self, options: InstallOptions): + icon_widget, list_widget = self.widgets[options.app_name] + icon_widget.mousePressEvent = lambda e: None + icon_widget.installing = True + list_widget.install_button.setDisabled(True) + list_widget.installing = True + self.install_game.emit(options) + def finished(self, app_name): self.widgets[app_name][0].info_text = "" self.widgets[app_name][0].info_label.setText("") diff --git a/Rare/Components/Tabs/Games/GameWidgets/BaseInstalledWidget.py b/Rare/Components/Tabs/Games/GameWidgets/BaseInstalledWidget.py index 0b1b5224..3a654610 100644 --- a/Rare/Components/Tabs/Games/GameWidgets/BaseInstalledWidget.py +++ b/Rare/Components/Tabs/Games/GameWidgets/BaseInstalledWidget.py @@ -25,6 +25,8 @@ class BaseInstalledWidget(QGroupBox): self.game_running = False self.update_available = self.core.get_asset(self.game.app_name, True).build_version != igame.version + + self.setContentsMargins(0, 0, 0, 0) # self.setStyleSheet("border-radius: 5px") diff --git a/Rare/Components/Tabs/Games/GameWidgets/BaseUninstalledWidget.py b/Rare/Components/Tabs/Games/GameWidgets/BaseUninstalledWidget.py index 80b48177..74804e51 100644 --- a/Rare/Components/Tabs/Games/GameWidgets/BaseUninstalledWidget.py +++ b/Rare/Components/Tabs/Games/GameWidgets/BaseUninstalledWidget.py @@ -17,6 +17,7 @@ class BaseUninstalledWidget(QGroupBox): self.game = game self.core = core self.pixmap = pixmap + self.installing = False self.setContentsMargins(0, 0, 0, 0) diff --git a/Rare/Components/Tabs/Games/GameWidgets/InstalledIconWidget.py b/Rare/Components/Tabs/Games/GameWidgets/InstalledIconWidget.py index b2dda22a..85f511fe 100644 --- a/Rare/Components/Tabs/Games/GameWidgets/InstalledIconWidget.py +++ b/Rare/Components/Tabs/Games/GameWidgets/InstalledIconWidget.py @@ -37,7 +37,7 @@ class GameWidgetInstalled(BaseInstalledWidget): if self.pixmap: w = 200 - self.pixmap = self.pixmap.scaled(w, int(w * 4 / 3)) + self.pixmap = self.pixmap.scaled(w, int(w * 4 / 3), transformMode=Qt.SmoothTransformation) self.image = ClickableLabel() self.image.setObjectName("game_widget") self.image.setPixmap(self.pixmap) diff --git a/Rare/Components/Tabs/Games/GameWidgets/InstalledListWidget.py b/Rare/Components/Tabs/Games/GameWidgets/InstalledListWidget.py index 1a30231d..c25b634f 100644 --- a/Rare/Components/Tabs/Games/GameWidgets/InstalledListWidget.py +++ b/Rare/Components/Tabs/Games/GameWidgets/InstalledListWidget.py @@ -1,7 +1,7 @@ import os from logging import getLogger -from PyQt5.QtCore import QProcess, pyqtSignal +from PyQt5.QtCore import QProcess, pyqtSignal, Qt from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QStyle, QVBoxLayout from qtawesome import icon @@ -30,7 +30,7 @@ class InstalledListWidget(BaseInstalledWidget): self.childLayout = QVBoxLayout() if self.pixmap: - self.pixmap = self.pixmap.scaled(180, 240) + self.pixmap = self.pixmap.scaled(180, 240, transformMode=Qt.SmoothTransformation) self.image = QLabel() self.image.setPixmap(self.pixmap) self.layout.addWidget(self.image) diff --git a/Rare/Components/Tabs/Games/GameWidgets/UninstalledIconWidget.py b/Rare/Components/Tabs/Games/GameWidgets/UninstalledIconWidget.py index a5eec79b..32131581 100644 --- a/Rare/Components/Tabs/Games/GameWidgets/UninstalledIconWidget.py +++ b/Rare/Components/Tabs/Games/GameWidgets/UninstalledIconWidget.py @@ -18,7 +18,7 @@ class IconWidgetUninstalled(BaseUninstalledWidget): def __init__(self, game: Game, core: LegendaryCore, pixmap): super(IconWidgetUninstalled, self).__init__(game, core, pixmap) self.layout = QVBoxLayout() - + self.setObjectName("game_widget_icon") if self.pixmap: w = 200 self.pixmap = self.pixmap.scaled(w, int(w * 4 / 3)) @@ -37,10 +37,17 @@ class IconWidgetUninstalled(BaseUninstalledWidget): self.setFixedWidth(self.sizeHint().width()) def mousePressEvent(self, e) -> None: - self.install() + if not self.installing: + self.install() def enterEvent(self, e): - self.info_label.setText(self.tr("Install Game")) + if not self.installing: + self.info_label.setText(self.tr("Install Game")) + else: + self.info_label.setText(self.tr("Installation running")) def leaveEvent(self, e): - self.info_label.setText("") + if self.installing: + self.info_label.setText("Installation...") + else: + self.info_label.setText("") diff --git a/Rare/Components/Tabs/Games/GamesTab.py b/Rare/Components/Tabs/Games/GamesTab.py deleted file mode 100644 index 93c27038..00000000 --- a/Rare/Components/Tabs/Games/GamesTab.py +++ /dev/null @@ -1,107 +0,0 @@ -from PyQt5.QtCore import QSettings, QSize -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QLineEdit, QPushButton, QStackedLayout, QLabel -from qtawesome import icon - -from Rare.Components.Tabs.Games.GameInfo.GameInfo import InfoTabs -from Rare.Components.Tabs.Games.GameList import GameList -from Rare.Components.Tabs.Games.ImportWidget import ImportWidget -from Rare.utils.QtExtensions import SelectViewWidget - - -class GameTab(QWidget): - def __init__(self, core): - super(GameTab, self).__init__() - self.layout = QStackedLayout() - self.default_widget = Games(core) - self.default_widget.game_list.show_game_info.connect(self.show_info) - self.default_widget.head_bar.import_game.clicked.connect(lambda: self.layout.setCurrentIndex(2)) - self.layout.addWidget(self.default_widget) - self.game_info = InfoTabs(core) - self.game_info.info.update_list.connect(self.update_list) - self.layout.addWidget(self.game_info) - - self.default_widget.head_bar.refresh_list.clicked.connect(self.update_list) - - self.import_widget = ImportWidget(core) - self.layout.addWidget(self.import_widget) - self.import_widget.back_button.clicked.connect(lambda: self.layout.setCurrentIndex(0)) - self.import_widget.update_list.connect(self.update_list) - self.setLayout(self.layout) - - def update_list(self): - self.default_widget.game_list.update_list(not self.default_widget.head_bar.view.isChecked()) - self.layout.setCurrentIndex(0) - - def show_info(self, app_name): - self.game_info.update_game(app_name) - self.game_info.setCurrentIndex(1) - self.layout.setCurrentIndex(1) - - -class Games(QWidget): - def __init__(self, core): - super(Games, self).__init__() - self.layout = QVBoxLayout() - - self.head_bar = GameListHeadBar() - self.head_bar.setObjectName("head_bar") - - self.game_list = GameList(core) - - self.head_bar.search_bar.textChanged.connect( - lambda: self.game_list.filter(self.head_bar.search_bar.text())) - - self.head_bar.installed_only.stateChanged.connect(lambda: - self.game_list.installed_only( - self.head_bar.installed_only.isChecked())) - self.layout.addWidget(self.head_bar) - self.layout.addWidget(self.game_list) - # self.layout.addStretch(1) - self.head_bar.view.toggled.connect(self.toggle_view) - - self.setLayout(self.layout) - - def toggle_view(self): - self.game_list.setCurrentIndex(1 if self.head_bar.view.isChecked() else 0) - settings = QSettings() - settings.setValue("icon_view", not self.head_bar.view.isChecked()) - - -class GameListHeadBar(QWidget): - def __init__(self): - super(GameListHeadBar, self).__init__() - self.layout = QHBoxLayout() - self.installed_only = QCheckBox(self.tr("Installed only")) - self.settings = QSettings() - self.installed_only.setChecked(self.settings.value("installed_only", False, bool)) - self.layout.addWidget(self.installed_only) - - self.layout.addStretch(1) - - self.import_game = QPushButton(icon("mdi.import", color="white"), self.tr("Import Game")) - self.layout.addWidget(self.import_game) - - self.layout.addStretch(1) - - self.search_bar = QLineEdit() - self.search_bar.setObjectName("search_bar") - self.search_bar.setFrame(False) - icon_label = QLabel() - icon_label.setPixmap(icon("fa.search", color="white").pixmap(QSize(20, 20))) - self.layout.addWidget(icon_label) - self.search_bar.setMinimumWidth(200) - self.search_bar.setPlaceholderText(self.tr("Search Game")) - self.layout.addWidget(self.search_bar) - - self.layout.addStretch(2) - - checked = QSettings().value("icon_view", True, bool) - - self.view = SelectViewWidget(checked) - self.layout.addWidget(self.view) - self.layout.addStretch(1) - self.refresh_list = QPushButton() - self.refresh_list.setIcon(icon("fa.refresh", color="white")) # Reload icon - self.layout.addWidget(self.refresh_list) - - self.setLayout(self.layout) diff --git a/Rare/Components/Tabs/Games/ImportWidget.py b/Rare/Components/Tabs/Games/ImportWidget.py index 1a5bd4b6..a4c149a6 100644 --- a/Rare/Components/Tabs/Games/ImportWidget.py +++ b/Rare/Components/Tabs/Games/ImportWidget.py @@ -4,7 +4,8 @@ import string from logging import getLogger from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QPushButton, QVBoxLayout, QFileDialog, QMessageBox, QLineEdit +from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QPushButton, QVBoxLayout, QFileDialog, QMessageBox, QLineEdit, \ + QGroupBox from qtawesome import icon from Rare.utils import LegendaryApi @@ -34,11 +35,13 @@ class ImportWidget(QWidget): self.title = QLabel("

Import Game{self.tr('Import existing game from Epic Games Launcher')}

") - self.layout.addWidget(self.import_one_game) + # self.import_one_game = QLabel(f"

{self.tr('Import existing game from Epic Games Launcher')}

") + self.import_one_game = QGroupBox(self.tr('Import existing game from Epic Games Launcher')) + self.import_one_game.setObjectName("group") + self.gb_layout = QVBoxLayout() self.import_game_info = QLabel(self.tr("Select path to game")) - self.layout.addWidget(self.import_game_info) + self.gb_layout.addWidget(self.import_game_info) self.override_app_name_label = QLabel(self.tr("Override app name (Only if imported game from legendary or the app could not find the app name)")) self.app_name_input = QLineEdit() @@ -52,17 +55,21 @@ class ImportWidget(QWidget): self.path_edit = PathEdit(os.path.expanduser("~"), QFileDialog.DirectoryOnly) self.path_edit.text_edit.textChanged.connect(self.path_changed) - self.layout.addWidget(self.path_edit) + self.gb_layout.addWidget(self.path_edit) - self.layout.addWidget(self.override_app_name_label) - self.layout.addWidget(self.app_name_input) + self.gb_layout.addWidget(self.override_app_name_label) + self.gb_layout.addWidget(self.app_name_input) self.info_label = QLabel("") - self.layout.addWidget(self.info_label) + self.gb_layout.addWidget(self.info_label) self.import_button = QPushButton(self.tr("Import Game")) - self.layout.addWidget(self.import_button) + self.gb_layout.addWidget(self.import_button) self.import_button.clicked.connect(self.import_game) + self.import_one_game.setLayout(self.gb_layout) + + self.layout.addWidget(self.import_one_game) + self.layout.addStretch(1) self.auto_import = QLabel(f"

{self.tr('Auto import all existing games')}

") @@ -104,6 +111,7 @@ class ImportWidget(QWidget): if not path: path = self.path_edit.text() if not app_name: + # try to find app name if a_n := self.find_app_name(path): app_name = a_n else: @@ -134,7 +142,7 @@ class ImportWidget(QWidget): continue app_name = self.find_app_name(json_path) if not app_name: - logger.warning("Could not find app name") + logger.warning("Could not find app name at " + game_path) continue if LegendaryApi.import_game(self.core, app_name, game_path + path): @@ -159,4 +167,4 @@ class ImportWidget(QWidget): QMessageBox.information(self, "Imported Games", self.tr("Successfully imported {} Games. Reloading Library").format(imported)) self.update_list.emit() else: - QMessageBox.information(self, "Imported Games", "No Games were found") + QMessageBox.information(self, "Imported Games", self.tr("No Games were found")) diff --git a/Rare/Components/Tabs/Games/__init__.py b/Rare/Components/Tabs/Games/__init__.py index e69de29b..08ab89cf 100644 --- a/Rare/Components/Tabs/Games/__init__.py +++ b/Rare/Components/Tabs/Games/__init__.py @@ -0,0 +1,107 @@ +from PyQt5.QtCore import QSettings, QSize +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QLineEdit, QPushButton, QStackedLayout, QLabel +from qtawesome import icon + +from Rare.Components.Tabs.Games.GameInfo.GameInfo import InfoTabs +from Rare.Components.Tabs.Games.GameList import GameList +from Rare.Components.Tabs.Games.ImportWidget import ImportWidget +from Rare.utils.QtExtensions import SelectViewWidget + + +class GameTab(QWidget): + def __init__(self, core): + super(GameTab, self).__init__() + self.layout = QStackedLayout() + self.default_widget = Games(core) + self.default_widget.game_list.show_game_info.connect(self.show_info) + self.default_widget.head_bar.import_game.clicked.connect(lambda: self.layout.setCurrentIndex(2)) + self.layout.addWidget(self.default_widget) + self.game_info = InfoTabs(core) + self.game_info.info.update_list.connect(self.update_list) + self.layout.addWidget(self.game_info) + + self.default_widget.head_bar.refresh_list.clicked.connect(self.update_list) + + self.import_widget = ImportWidget(core) + self.layout.addWidget(self.import_widget) + self.import_widget.back_button.clicked.connect(lambda: self.layout.setCurrentIndex(0)) + self.import_widget.update_list.connect(self.update_list) + self.setLayout(self.layout) + + def update_list(self): + self.default_widget.game_list.update_list(self.default_widget.head_bar.view.isChecked()) + self.layout.setCurrentIndex(0) + + def show_info(self, app_name): + self.game_info.update_game(app_name) + self.game_info.setCurrentIndex(1) + self.layout.setCurrentIndex(1) + + +class Games(QWidget): + def __init__(self, core): + super(Games, self).__init__() + self.layout = QVBoxLayout() + + self.head_bar = GameListHeadBar() + self.head_bar.setObjectName("head_bar") + + self.game_list = GameList(core) + + self.head_bar.search_bar.textChanged.connect( + lambda: self.game_list.filter(self.head_bar.search_bar.text())) + + self.head_bar.installed_only.stateChanged.connect(lambda: + self.game_list.installed_only( + self.head_bar.installed_only.isChecked())) + self.layout.addWidget(self.head_bar) + self.layout.addWidget(self.game_list) + # self.layout.addStretch(1) + self.head_bar.view.toggled.connect(self.toggle_view) + + self.setLayout(self.layout) + + def toggle_view(self): + self.game_list.setCurrentIndex(1 if self.head_bar.view.isChecked() else 0) + settings = QSettings() + settings.setValue("icon_view", not self.head_bar.view.isChecked()) + + +class GameListHeadBar(QWidget): + def __init__(self): + super(GameListHeadBar, self).__init__() + self.layout = QHBoxLayout() + self.installed_only = QCheckBox(self.tr("Installed only")) + self.settings = QSettings() + self.installed_only.setChecked(self.settings.value("installed_only", False, bool)) + self.layout.addWidget(self.installed_only) + + self.layout.addStretch(1) + + self.import_game = QPushButton(icon("mdi.import", color="white"), self.tr("Import Game")) + self.layout.addWidget(self.import_game) + + self.layout.addStretch(1) + + self.search_bar = QLineEdit() + self.search_bar.setObjectName("search_bar") + self.search_bar.setFrame(False) + icon_label = QLabel() + icon_label.setPixmap(icon("fa.search", color="white").pixmap(QSize(20, 20))) + self.layout.addWidget(icon_label) + self.search_bar.setMinimumWidth(200) + self.search_bar.setPlaceholderText(self.tr("Search Game")) + self.layout.addWidget(self.search_bar) + + self.layout.addStretch(2) + + checked = QSettings().value("icon_view", True, bool) + + self.view = SelectViewWidget(checked) + self.layout.addWidget(self.view) + self.layout.addStretch(1) + self.refresh_list = QPushButton() + self.refresh_list.setIcon(icon("fa.refresh", color="white")) # Reload icon + self.layout.addWidget(self.refresh_list) + + self.setLayout(self.layout) diff --git a/Rare/Components/Tabs/Settings/About.py b/Rare/Components/Tabs/Settings/About.py index ab766115..4e9592da 100644 --- a/Rare/Components/Tabs/Settings/About.py +++ b/Rare/Components/Tabs/Settings/About.py @@ -1,6 +1,9 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel +import webbrowser + +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton from Rare import __version__ +from Rare.utils.utils import get_latest_version class About(QWidget): @@ -13,6 +16,15 @@ class About(QWidget): self.version = QLabel("Version: " + __version__) self.layout.addWidget(self.version) + latest_tag = get_latest_version() + self.update_available = latest_tag != __version__ + if latest_tag != __version__: + print(f"Update available: {__version__} -> {latest_tag}") + self.update_available = QLabel(self.tr("Update available: {} -> {}").format(__version__, latest_tag)) + self.layout.addWidget(self.update_available) + self.open_browser = QPushButton(self.tr("Download latest release")) + self.layout.addWidget(self.open_browser) + self.open_browser.clicked.connect(lambda: webbrowser.open("https://github.com/Dummerle/Rare/releases/latest")) self.dev = QLabel(self.tr("Developer:") + "Dummerle") self.dev.setToolTip("Github") diff --git a/Rare/Components/Tabs/Settings/Dxvk.py b/Rare/Components/Tabs/Settings/Dxvk.py index 993d3ba9..14cdbcc7 100644 --- a/Rare/Components/Tabs/Settings/Dxvk.py +++ b/Rare/Components/Tabs/Settings/Dxvk.py @@ -41,8 +41,10 @@ class DxvkWidget(QGroupBox): self.more_settings.setPopupMode(QToolButton.InstantPopup) self.more_settings.setMenu(QMenu()) self.more_settings.setText("More DXVK settings") + action = QWidgetAction(self) self.more_settings_widget = DxvkMoreSettingsWidget(self.dxvk_settings, self.core) + self.more_settings_widget.show_dxvk.connect(lambda x: self.show_dxvk.setChecked(x)) action.setDefaultWidget(self.more_settings_widget) self.more_settings.menu().addAction(action) @@ -71,7 +73,8 @@ class DxvkWidget(QGroupBox): def update_dxvk_active(self): if self.show_dxvk.isChecked(): if not f"{self.name}.env" in self.core.lgd.config.sections(): - self.core.lgd.config[f"{self.name}.env"] = {} + print("add section dxvk") + self.core.lgd.config.add_section(f"{self.name}.env") self.more_settings.setDisabled(False) for i in self.more_settings_widget.settings: @@ -93,11 +96,11 @@ class DxvkWidget(QGroupBox): self.core.lgd.config.remove_option(f"{self.name}.env", "DXVK_HUD") if not self.core.lgd.config[f"{self.name}.env"]: self.core.lgd.config.remove_section(f"{self.name}.env") - print("Remove Section DXVK_HUD") self.core.lgd.save_config() class DxvkMoreSettingsWidget(QWidget): + show_dxvk = pyqtSignal(bool) def __init__(self, settings: dict, core: LegendaryCore): super(DxvkMoreSettingsWidget, self).__init__() self.layout = QVBoxLayout() @@ -118,16 +121,22 @@ class DxvkMoreSettingsWidget(QWidget): y = list(self.settings[tag]) y[0] = checked self.settings[tag] = tuple(y) - # print(self.settings) + sett = [] logger.debug(self.settings) for i in self.settings: check, _ = self.settings[i] if check: sett.append(i) - if sett: - self.core.lgd.config[f"{self.name}.env"]["DXVK_HUD"] = ",".join(sett) - self.core.lgd.save_config() + if len(sett) != 0: + self.core.lgd.config.set(f"{self.name}.env", "DXVK_HUD", ",".join(sett)) + + else: + self.core.lgd.config.remove_option(f"{self.name}.env", "DXVK_HUD") + self.show_dxvk.emit(False) + if not self.core.lgd.config.options(f"{self.name}.env"): + self.core.lgd.config.remove_section(f"{self.name}.env") + self.core.lgd.save_config() class CheckBox(QCheckBox): diff --git a/Rare/Components/Tabs/Settings/Legendary.py b/Rare/Components/Tabs/Settings/Legendary.py index fcc2c428..6046e677 100644 --- a/Rare/Components/Tabs/Settings/Legendary.py +++ b/Rare/Components/Tabs/Settings/Legendary.py @@ -10,14 +10,15 @@ from custom_legendary.core import LegendaryCore logger = getLogger("LegendarySettings") -class LegendarySettings(QWidget): +class LegendarySettings(QGroupBox): def __init__(self, core: LegendaryCore): super(LegendarySettings, self).__init__() + self.setTitle(self.tr("Legendary settings")) self.layout = QVBoxLayout() self.core = core - self.title = QLabel("

" + self.tr("Legendary settings") + "

") - self.layout.addWidget(self.title) - + #self.title = QLabel("

" + self.tr("Legendary settings") + "

") + #self.layout.addWidget(self.title) + self.setObjectName("group") # Default installation directory self.select_path = PathEdit(core.get_default_install_dir(), type_of_file=QFileDialog.DirectoryOnly, infotext="Default") @@ -39,8 +40,7 @@ class LegendarySettings(QWidget): #cleanup self.clean_layout = QVBoxLayout() - self.cleanup_widget = QGroupBox() - self.cleanup_widget.setTitle(self.tr("Cleanup")) + self.cleanup_widget = QGroupBox(self.tr("Cleanup")) self.clean_button = QPushButton(self.tr("Remove everything")) self.clean_button.clicked.connect(lambda: self.cleanup(False)) self.clean_layout.addWidget(self.clean_button) @@ -59,20 +59,20 @@ class LegendarySettings(QWidget): self.core.lgd.config["Legendary"]["install_dir"] = self.select_path.text() if self.select_path.text() == "" and "install_dir" in self.core.lgd.config["Legendary"].keys(): self.core.lgd.config["Legendary"].pop("install_dir") - logger.info("Remove install_dir section") else: logger.info("Set config install_dir to " + self.select_path.text()) self.core.lgd.save_config() def max_worker_save(self, num_workers: str): - self.core.lgd.config["Legendary"]["max_workers"] = num_workers if num_workers == "": - self.core.lgd.config["Legendary"].pop("max_workers") + self.core.lgd.config.remove_option("Legendary", "max_workers") + self.core.lgd.save_config() return num_workers = int(num_workers) if num_workers == 0: - self.core.lgd.config["Legendary"].pop("max_workers") - logger.info("Updating config for max_workers") + self.core.lgd.config.remove_option("Legendary", "max_workers") + else: + self.core.lgd.config.set("Legendary", "max_workers", str(num_workers)) self.core.lgd.save_config() def cleanup(self, keep_manifests): @@ -92,7 +92,7 @@ class LegendarySettings(QWidget): after = self.core.lgd.get_dir_size() logger.info(f'Cleanup complete! Removed {(before - after) / 1024 / 1024:.02f} MiB.') - if cleaned := (before-after) != 0: + if cleaned := (before-after) > 0: QMessageBox.information(self, "Cleanup", self.tr("Cleanup complete! Successfully removed {} MB").format(round(cleaned / 1024 / 1024, 3))) else: QMessageBox.information(self, "Cleanup", "Nothing to clean") diff --git a/Rare/Components/Tabs/Settings/Rare.py b/Rare/Components/Tabs/Settings/Rare.py index 6587ab1b..b586f531 100644 --- a/Rare/Components/Tabs/Settings/Rare.py +++ b/Rare/Components/Tabs/Settings/Rare.py @@ -3,7 +3,7 @@ import shutil from logging import getLogger from PyQt5.QtCore import QSettings -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QFileDialog, QComboBox, QPushButton, QCheckBox +from PyQt5.QtWidgets import QVBoxLayout, QFileDialog, QComboBox, QPushButton, QCheckBox, QGroupBox from Rare.Components.Tabs.Settings.SettingsWidget import SettingsWidget from Rare.utils.QtExtensions import PathEdit @@ -12,21 +12,16 @@ from Rare.utils.utils import get_lang, get_possible_langs logger = getLogger("RareSettings") -class RareSettings(QWidget): +class RareSettings(QGroupBox): def __init__(self): super(RareSettings, self).__init__() + self.setTitle(self.tr("Rare settings")) + self.setObjectName("group") self.layout = QVBoxLayout() - self.title = QLabel("

" + self.tr("Rare settings") + "

") - self.layout.addWidget(self.title) settings = QSettings() - img_dir = settings.value("img_dir", type=str) - language = settings.value("language", type=str) - # default settings - if not img_dir: - settings.setValue("img_dir", os.path.expanduser("~/.cache/rare/")) - if not language: - settings.setValue("language", get_lang()) - del settings + img_dir = settings.value("img_dir", os.path.expanduser("~/.cache/rare/images/"), type=str) + language = settings.value("language", get_lang(), type=str) + # select Image dir self.select_path = PathEdit(img_dir, type_of_file=QFileDialog.DirectoryOnly) self.select_path.text_edit.textChanged.connect(lambda t: self.save_path_button.setDisabled(False)) @@ -37,19 +32,27 @@ class RareSettings(QWidget): # Select lang self.select_lang = QComboBox() - languages = ["English", "Deutsch"] + languages = ["English", "Deutsch", "Français"] self.select_lang.addItems(languages) if language in get_possible_langs(): if language == "de": self.select_lang.setCurrentIndex(1) elif language == "en": self.select_lang.setCurrentIndex(0) + elif language == "fr": + self.select_lang.setCurrentIndex(2) else: self.select_lang.setCurrentIndex(0) self.lang_widget = SettingsWidget(self.tr("Language"), self.select_lang) self.select_lang.currentIndexChanged.connect(self.update_lang) self.layout.addWidget(self.lang_widget) + self.exit_to_sys_tray = QCheckBox(self.tr("Hide to System Tray Icon")) + self.exit_to_sys_tray.setChecked(settings.value("sys_tray", True, bool)) + self.exit_to_sys_tray.stateChanged.connect(self.update_sys_tray) + self.sys_tray_widget = SettingsWidget(self.tr("Exit to System Tray Icon"), self.exit_to_sys_tray) + self.layout.addWidget(self.sys_tray_widget) + self.game_start_accept = QCheckBox(self.tr("Confirm launch of game")) self.game_start_accept.stateChanged.connect(self.update_start_confirm) self.game_start_accept_widget = SettingsWidget(self.tr("Confirm launch of game"), self.game_start_accept) @@ -59,6 +62,10 @@ class RareSettings(QWidget): self.setLayout(self.layout) + def update_sys_tray(self): + settings = QSettings() + settings.setValue("sys_tray", self.exit_to_sys_tray.isChecked()) + def update_start_confirm(self): settings = QSettings() settings.setValue("confirm_start", self.game_start_accept.isChecked()) @@ -73,7 +80,8 @@ class RareSettings(QWidget): settings.setValue("language", "en") elif i == 1: settings.setValue("language", "de") - del settings + elif i == 2: + settings.setValue("language", "fr") self.lang_widget.info_text.setText(self.tr("Restart Application to activate changes")) def update_path(self): diff --git a/Rare/Components/Tabs/Settings/SettingsTab.py b/Rare/Components/Tabs/Settings/SettingsTab.py deleted file mode 100644 index dfe5443b..00000000 --- a/Rare/Components/Tabs/Settings/SettingsTab.py +++ /dev/null @@ -1,22 +0,0 @@ -import os - -from PyQt5.QtWidgets import QTabWidget - -from Rare.Components.Tabs.Settings.About import About -from Rare.Components.Tabs.Settings.Legendary import LegendarySettings -from Rare.Components.Tabs.Settings.Linux import LinuxSettings -from Rare.Components.Tabs.Settings.Rare import RareSettings -from Rare.utils.QtExtensions import SideTabBar - - -class SettingsTab(QTabWidget): - def __init__(self, core): - super(SettingsTab, self).__init__() - self.core = core - self.setTabBar(SideTabBar()) - self.setTabPosition(QTabWidget.West) - self.addTab(RareSettings(), "Rare") - self.addTab(LegendarySettings(core), "Legendary") - if os.name != "nt": - self.addTab(LinuxSettings(core), "Linux") - self.addTab(About(), "About") diff --git a/Rare/Components/Tabs/Settings/__init__.py b/Rare/Components/Tabs/Settings/__init__.py index e69de29b..6674fb52 100644 --- a/Rare/Components/Tabs/Settings/__init__.py +++ b/Rare/Components/Tabs/Settings/__init__.py @@ -0,0 +1,24 @@ +import os + +from PyQt5.QtWidgets import QTabWidget + +from Rare.Components.Tabs.Settings.About import About +from Rare.Components.Tabs.Settings.Legendary import LegendarySettings +from Rare.Components.Tabs.Settings.Linux import LinuxSettings +from Rare.Components.Tabs.Settings.Rare import RareSettings +from Rare.utils.QtExtensions import SideTabBar + + +class SettingsTab(QTabWidget): + def __init__(self, core): + super(SettingsTab, self).__init__() + self.core = core + self.setTabBar(SideTabBar()) + self.setTabPosition(QTabWidget.West) + self.addTab(RareSettings(), "Rare") + self.addTab(LegendarySettings(core), "Legendary") + if os.name != "nt": + self.addTab(LinuxSettings(core), "Linux") + self.about = About() + + self.addTab(self.about, "About (!)" if self.about.update_available else "About") diff --git a/Rare/Components/TrayIcon.py b/Rare/Components/TrayIcon.py new file mode 100644 index 00000000..3b80f061 --- /dev/null +++ b/Rare/Components/TrayIcon.py @@ -0,0 +1,22 @@ +from PyQt5.QtCore import QCoreApplication +from PyQt5.QtWidgets import QSystemTrayIcon, QMenu, QAction +from qtawesome import icon + + +class TrayIcon(QSystemTrayIcon): + def __init__(self, parent): + super(TrayIcon, self).__init__(parent) + self.setIcon(icon("ei.cogs", color="white")) + self.setVisible(True) + self.setToolTip("Rare") + + self.menu = QMenu() + + self.start_rare = QAction("Rare") + self.menu.addAction(self.start_rare) + + self.exit_action = QAction(self.tr("Exit")) + self.menu.addSeparator() + self.menu.addAction(self.exit_action) + + self.setContextMenu(self.menu) diff --git a/Rare/Main.py b/Rare/Main.py index bb3b1566..a4283de4 100644 --- a/Rare/Main.py +++ b/Rare/Main.py @@ -2,21 +2,30 @@ import configparser import logging import os import sys +import time from PyQt5.QtCore import QSettings, QTranslator from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QApplication, QSystemTrayIcon from Rare import lang_path, style_path from Rare.Components.Launch.LaunchDialog import LaunchDialog from Rare.Components.MainWindow import MainWindow +from Rare.Components.TrayIcon import TrayIcon from Rare.utils.utils import get_lang from custom_legendary.core import LegendaryCore +start_time = time.strftime('%y-%m-%d--%H:%M') # year-month-day-hour-minute +file_name = os.path.expanduser(f"~/.cache/rare/logs/Rare_{start_time}.log") +if not os.path.exists(os.path.dirname(file_name)): + os.makedirs(os.path.dirname(file_name)) + logging.basicConfig( format='[%(name)s] %(levelname)s: %(message)s', - level=logging.INFO -) + level=logging.INFO, + filename=file_name, + filemode="w" + ) logger = logging.getLogger("Rare") @@ -40,6 +49,7 @@ class App(QApplication): self.core.lgd.save_config() # set Application name for settings + self.mainwindow = None self.setApplicationName("Rare") self.setOrganizationName("Rare") settings = QSettings() @@ -58,6 +68,8 @@ class App(QApplication): self.setStyleSheet(open(style_path + "RareStyle.qss").read()) self.setWindowIcon(QIcon(style_path + "Logo.png")) + # tray icon + # launch app self.launch_dialog = LaunchDialog(self.core) self.launch_dialog.start_app.connect(self.start_app) @@ -65,10 +77,27 @@ class App(QApplication): def start_app(self): self.mainwindow = MainWindow(self.core) - # close launch dialog after app widgets were created + self.tray_icon = TrayIcon(self) + self.tray_icon.exit_action.triggered.connect(lambda: exit(0)) + self.tray_icon.start_rare.triggered.connect(self.mainwindow.show) + self.tray_icon.activated.connect(self.tray) + self.mainwindow.tab_widget.downloadTab.finished.connect(lambda: self.tray_icon.showMessage( + self.tr("Download finished"), self.tr("Download finished. Game is playable now"), + QSystemTrayIcon.Information, 4000)) self.launch_dialog.close() + def tray(self, reason): + if reason == QSystemTrayIcon.DoubleClick: + self.mainwindow.show() + logger.info("Show App") + def start(): - app = App() - app.exec_() + while True: + app = App() + exit_code = app.exec_() + # if not restart + if exit_code != -133742: + break + # restart app + del app diff --git a/Rare/Styles/RareStyle.qss b/Rare/Styles/RareStyle.qss index d6670020..672096d1 100644 --- a/Rare/Styles/RareStyle.qss +++ b/Rare/Styles/RareStyle.qss @@ -30,8 +30,23 @@ QTabBar::tab:hover#main_tab_bar { } +QGroupBox{ + padding: 4px; + margin: 8px; +} + +QGroupBox#game_widget_icon{ + border: none; + padding: 0; + margin: 0; +} + QGroupBox#group{ font-size: 15px; + font-weight: bold; + border: 1px solid white; + margin-top: 10px; + padding: 8px; } QTabBar::tab:disabled { diff --git a/Rare/__init__.py b/Rare/__init__.py index b9ef77b6..4b051139 100644 --- a/Rare/__init__.py +++ b/Rare/__init__.py @@ -1,5 +1,5 @@ import os -__version__ = "0.9.8.3" +__version__ = "0.9.9" style_path = os.path.join(os.path.dirname(__file__), "Styles/") lang_path = os.path.join(os.path.dirname(__file__), "languages/") diff --git a/Rare/__main__.py b/Rare/__main__.py index 77a3c6b8..209f7c10 100644 --- a/Rare/__main__.py +++ b/Rare/__main__.py @@ -14,20 +14,3 @@ def main(): if __name__ == '__main__': main() - -""" - tray = QSystemTrayIcon() - tray.setIcon(icon("fa.gamepad", color="white")) - tray.setVisible(True) - menu = QMenu() - option1 = QAction("Geeks for Geeks") - option1.triggered.connect(lambda: app.exec_()) - option2 = QAction("GFG") - menu.addAction(option1) - menu.addAction(option2) - # To quit the app - quit = QAction("Quit") - quit.triggered.connect(app.quit) - menu.addAction(quit) - # Adding options to the System Tray - tray.setContextMenu(menu)""" diff --git a/Rare/languages/de.qm b/Rare/languages/de.qm index c3cc547d..73a052a2 100644 Binary files a/Rare/languages/de.qm and b/Rare/languages/de.qm differ diff --git a/Rare/languages/de.ts b/Rare/languages/de.ts index 04d4deed..445511c8 100644 --- a/Rare/languages/de.ts +++ b/Rare/languages/de.ts @@ -4,12 +4,12 @@ About - + Developer: Entwickler: - + Legendary developer: Legendary Entwickler: @@ -19,15 +19,38 @@ Dies ist eine beta version, also können Bugs entstehen. Wenn du einen Bug bemerkst, kontaktiere mich, indem du einen Issue auf <a href='https://github.com/Dummerle/Rare/issues'>Github</a> erstellst oder mir auf Discord eine Nachricht schickst. Ebenso bei einem Wunsch für Features - + This is a beta version, so you can get bugs. If you get a bug, please report it by creating a Issue on <a href='https://github.com/Dummerle/Rare/issues'>Github</a>. You can also contact me on Discord (Dummerle#7419). Or you can join the <a href='https://discord.gg/YvmABK9YSk'>Discord server</a> Dies ist eine Betaversion, also können Bugs und andere Unschönheiten auftreten. Falls ein Bug auftritt, bitte auf <a href='https://github.com/Dummerle/Rare/issues'>Github</a> melden, indem du einen Issue erstellst oder auf Discord. (Dummerle#7419). Ein Rare <a href='https://discord.gg/YvmABK9YSk'>Discord server</a> existiert ebenfalls + + + Update available: {} -> {} + Update verfügbar: {} -> {} + + + + Download latest release + Neueste Version herunterladen + + + + App + + + Download finished + Download abgeschlossen + + + + Download finished. Game is playable now + Downlaod abgeschlossen. Spiel kann jetzt gespielt werden + BaseInstalledWidget - + Do you want to launch {} Möchtest du {} starten @@ -55,68 +78,129 @@ Laden... + + DlQueueWidget + + + Download Queue + Eingereihte Downloads + + + + No downloads in queue + Keine eingereihten Downloads + + + + DlWidget + + + Download size: {} GB + Download Größe: {} GB + + + + Install size: {} GB + Installierte Größe: {} GB + + + + Remove Download + Download löschen + + DownloadTab - + No active Download Kein aktiver Download - + Stop Download Download anhalten - + No updates available Keine Updates verfügbar - + Error preparing download Fehler beim Vorbereiten des Downloads - + Download size is 0. Game already exists Die Größe des Downloads ist 0. Spiel existiert bereits - + Installation finished Installation abgeschlossen - + Installing Game: No active download Installierendes Spiel: Kein aktiver Download - + Download speed Geschwindigkeit - + Cache used Benutzter Cache - + Downloaded Runtergeladen - + Time left: Zeit übrig: - + Finished Download of game {} Downlaod von {} abgeschlossen + + + Download finished. Reload library + Download abgeschlossen. Spiele neu laden + + + + Download queue: Empty + Anschließende Downloads: Keine + + + + Download queue: + Anschließende Downloads: + + + + Empty + Keine + + + + Installing Game: + Installierendes Spiel: + + + + Updates + Updates + DxvkWidget @@ -159,32 +243,32 @@ GameActions - + Uninstall game Spiel deinstallieren - + Uninstall Deinstallieren - + Verify Game Spieldateien überprüfen - + Verify Überprüfen - + Repair Game Spiel reparieren - + Repair Reparieren @@ -192,50 +276,50 @@ GameInfo - + Repair file does not exist or game does not need a repair. Please verify game first Reparationsdatei existiert nicht oder das Spiel braucht keine Reperatur. Bitte das spiel zuerst überprüfen - + Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them? Überprüfung fehlgeschlagen, {} Datei(en) fehlerhaft, {} Datei(en) fehlen. Willst du das Spiel reparieren? - + Developer: Entwickler: - + Install size: Größe: - + Install path: Installationsordner: Are you sure to uninstall {} - Möchtest du {} wirklich deinstallieren + Möchtest du {} wirklich deinstallieren GameList - + Launch Starten - + Game running Spiel läuft - + Installed Games: {} Available Games: {} Installierte Spiele: {} Verfügbare Spiele: {} @@ -243,17 +327,17 @@ GameListHeadBar - + Installed only Nur Installierte - + Import Game Spiel importieren - + Search Game Spiel suchen @@ -322,15 +406,20 @@ IconWidgetUninstalled - + Install Game Spiel installieren + + + Installation running + Installation läuft + ImportWidget - + Import Importieren @@ -355,27 +444,27 @@ Laden... - + Error: No valid session found Keine valide Session gefunden - + Back Zurück - + Select path to game Wähle den Pfad zum Spiel - + Import Game Spiel importieren - + Import all games from Epic Games Launcher Alle Spiele aus dem Epic Games Launcher importieren @@ -385,45 +474,55 @@ {} Spiele erfolgreich importiert - + Override app name (Only if imported game from legendary or the app could not find the app name) App Name überschreiben (Nur falls das Spiel von Legendary importiert wird oder der App Name nicht gefunden wird - + Could not find app name Konnte den Appnamen nicht finden - + Successfully imported {}. Reload library Erfolgreich {} importiert. Spiele neu laden - + Failed to import {} {} Konnte nicht importiert werden - + Successfully imported {} Games. Reloading Library Erfolgreich {} Spiele importiert. Spiele neu laden + + + Import existing game from Epic Games Launcher + Ein bereits existierendes Spiel aus dem Epic Games Launcher importieren + + + + No Games were found + Keine Spiele wurden gefunden + InfoTabs - + Back Zurück - + Game Info Spielinfo - + Settings Einstellungen @@ -431,27 +530,42 @@ InstallDialog - + Max workers (0: Default) Maximale Anzahl Downloadprozessen(Standard: 0) + + + <h3>Install {}</h3> + <h3>Installiere {}</h3> + + + + Force download + Download erzwingen + + + + Ignore free space (Warning!) + Freien Speicherplatz ignorieren (Achtung!) + InstallInfoDialog - + Download size: {}GB Install size: {}GB Downloadgröße: {}GB Installationsgröße: {} GB - + Install Installieren - + Cancel Abbruch @@ -503,17 +617,17 @@ Installationsgröße: {} GB LegendarySettings - + Legendary settings Legendary Einstellungen - + Default installation directory Standardordner für Installationen - + Max workers for Download (Less: slower download)(0: Default) Maximale Anzahl Downloadprozesse (Weniger: langsamer)(Standard: 0) @@ -582,6 +696,14 @@ Installationsgröße: {} GB Dies öffnet den Browser, Einloggen und den Text kopieren + + MainWindow + + + There is a download active. Do you really want to exit app? + Ein Download läuft noch. Möchtest du die App wirklich beenden? + + MiniWidget @@ -595,12 +717,12 @@ Installationsgröße: {} GB Accounteinstellungen - + Logout Ausloggen - + Do you really want to logout Willst du dich wirklich abmelden @@ -629,65 +751,75 @@ Installationsgröße: {} GB RareSettings - + Rare settings Rare Einstellungen - + Save Speichern - + Image Directory Ordner für Bilder - + Language Sprache - + Restart Application to activate changes Starte die App neu um die Änderungen zu aktivieren - + Confirm launch of game Start des Spiels bestätigen + + + Exit to System Tray Icon + Beim verlassen auf das System Tray Icon minimieren + + + + Hide to System Tray Icon + In das System Tray Icon minimieren + SyncSaves - + Cloud Saves Cloud Speicherstände - + Found Saves for folowing Games Spielstände für folgende Spiele gefunden - + Your games does not support Cloud Saves Deine Spiele unterstützen keine Online Speicherstände - + Sync all games Alle Spiele synchronisieren - + Found no savepath Kein Speicherort gefunden - + No save path was found. Please select path or skip Kein Speicherort wurde gefunden. Wähle einen Ordner oder überspringe @@ -695,97 +827,97 @@ Installationsgröße: {} GB SyncWidget - + Path not found Ordner nicht gefunden - + Local Save date: Lokales Speicherdatum: - + No Local Save files Keine Lokalen Dateien - + Cloud save date: Online Speicherdatum: - + No Cloud saves Keine Online Speicherstände - + Game is up to date Spiel ist aktuell - + Upload anyway Trotzdem hochladen - + Download anyway Trotzdem herunterladen - + Cloud save is newer Online Speicherstand ist aktueller - + Download Cloud saves Online Speicherstand herunterladen - + Upload Saves Spielstände hochladen - + Local save is newer Lokaler Speicher ist aktueller - + Upload saves Spielstände hochladen - + Download saves Spielstand herunterladen - + Change path Pfad ändern - + Uploading... Hochladen... - + Upload finished Hochladen abgeschlossen - + Downloading... Runterladen... - + Download finished Download abgeschlossen @@ -798,10 +930,46 @@ Installationsgröße: {} GB Spiele + + TrayIcon + + + Exit + Schließen + + + + UninstallDialog + + + Do you really want to uninstall {} + Möchtest du wirklich {} deinstallieren + + + + Keep Files + Dateien behalten + + + + Do you want to keep files? + Willst du die Dateien behalten? + + + + Uninstall + Deinstallieren + + + + Cancel + Abbruch + + UpdateWidget - + Update Game Spiel updaten diff --git a/Rare/languages/fr.qm b/Rare/languages/fr.qm new file mode 100644 index 00000000..d5ae2341 Binary files /dev/null and b/Rare/languages/fr.qm differ diff --git a/Rare/languages/fr.ts b/Rare/languages/fr.ts new file mode 100644 index 00000000..2aa8f3d8 --- /dev/null +++ b/Rare/languages/fr.ts @@ -0,0 +1,818 @@ + + + + About + + + Developer: + Développeur + + + + Legendary developer: + Legendary Développeur + + + + This is a beta version, so you can get bugs. If you get a bug, please report it by creating a Issue on <a href='https://github.com/Dummerle/Rare/issues'>Github</a>. You can also contact me on Discord (Dummerle#7419). Or you can join the <a href='https://discord.gg/YvmABK9YSk'>Discord server</a> + Il s'agit d'une version bêta, vous pouvez donc rencontrer des bogues. Si vous rencontrez un bug, veuillez le signaler en créant un Issue sur <a href='https://github.com/Dummerle/Rare/issues'>Github</a>. Vous pouvez également me contacter sur Discord (Dummerle#7419). Ou vous pouvez rejoindre le <a href='https://discord.gg/YvmABK9YSk'>serveur Discord</a> + + + + BaseInstalledWidget + + + Do you want to launch {} + Voulez-vous lancer {} + + + + BrowserLogin + + + Opens a browser. You login and copy the json code in the field below. Click <a href='{}'>here</a> to open Browser + Ouvre un navigateur. Vous vous connectez et copiez le code json dans le champ ci-dessous. Cliquez <a href='{}'>here</a> pour ouvrir un navigateur + + + + Insert SID here + Insérer le SID ici + + + + Login + Login + + + + Loading... + charge... + + + + DownloadTab + + + No active Download + Aucun téléchargement actif + + + + Stop Download + Stop Télécharger + + + + No updates available + Aucune mise à jour disponible + + + + Error preparing download + Erreur lors de la préparation du téléchargement + + + + Download size is 0. Game already exists + La taille du téléchargement est de 0. Le jeu existe déjà + + + + Installing game: + Installation du jeu: + + + + Installation finished + Installation terminée + + + + Finished Download of game {} + Fin du téléchargement du jeu {} + + + + Installing Game: No active download + Installation du jeu: Aucun téléchargement actif + + + + Download speed + Vitesse de téléchargement + + + + Cache used + Cache utilisé + + + + Downloaded + Téléchargé + + + + Time left: + Il reste du temps: + + + + DxvkWidget + + + GPU usage + GPU Utilisation + + + + Used Memory + utilisé Memory + + + + Device info + Info sur le dispositif + + + + DXVK version + DXVK version + + + + D3D Level of application + D3D Niveau d'application + + + + Frame time graph + Graphique de temps de trame + + + + dxvk settings + dxvk paramètres + + + + GameActions + + + Uninstall game + Désinstaller le jeu + + + + Uninstall + Désinstaller + + + + Verify Game + Vérifier le jeu + + + + Verify + Vérifier + + + + Repair Game + Jeu de réparation + + + + Repair + Réparation + + + + GameInfo + + + Are you sure to uninstall {} + Etes-vous sûr de désinstaller {} + + + + Repair file does not exist or game does not need a repair. Please verify game first + Le fichier de réparation n'existe pas ou le jeu ne nécessite pas de réparation. Veuillez d'abord vérifier le jeu. + + + + Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them? + La vérification a échoué, {} fichier(s) corrompu(s), {} fichier(s) manquant(s). Voulez-vous les réparer ? + + + + Developer: + Développeur: + + + + Install size: + Taille d'installation: + + + + Install path: + Chemin d'installation: + + + + GameList + + + Installed Games: {} Available Games: {} + Jeux installés: {} Jeux disponibles: {} + + + + Launch + Lancer + + + + Game running + Jeu en cours + + + + GameListHeadBar + + + Installed only + Installé uniquement + + + + Import Game + Jeu d'importation + + + + Search Game + Rechercher un jeu + + + + GameSettings + + + Launch Game offline + Lancer le jeu hors ligne + + + + Skip update check before launching + Sauter la vérification de la mise à jour avant le lancement + + + + Save + Sauvez + + + + Wrapper (e.g. optirun) + Wrapper (p.e. optirun) + + + + Proton Wrapper + Enveloppeur de Proton + + + + Proton prefix + Préfixe du proton + + + + No permission to create folder + Pas de permission pour créer un dossier + + + + Please select path for proton prefix + Veuillez sélectionner le chemin pour le préfixe proton + + + + GameWidgetInstalled + + + Update available + Mise à jour disponible + + + + Start game without version check + Démarrer le jeu sans vérifier la version + + + + Game running + Jeu en cours + + + + IconWidgetUninstalled + + + Install Game + Installer le jeu + + + + ImportWidget + + + Import + Importer + + + + Could not find EGL program data + Impossible de trouver les données du programme EGL + + + + Found EGL program Data. Do you want to import them? + Les données du programme EGL ont été trouvées. Voulez-vous les importer ? + + + + Could not find any Epic Games login data + Impossible de trouver les données de connexion d'Epic Games + + + + Loading... + Chargement... + + + + Error: No valid session found + Erreur : Aucune session valide n'a été trouvée + + + + Back + Dos + + + + Select path to game + Sélectionnez le chemin vers le jeu + + + + Override app name (Only if imported game from legendary or the app could not find the app name) + Remplacer le nom de l'application (uniquement si le jeu a été importé depuis le légendaire ou si l'application n'a pas pu trouver le nom de l'application) + + + + Import Game + Import jeu + + + + Import all games from Epic Games Launcher + Importer tous les jeux du Epic Games Launcher + + + + Could not find app name + Impossible de trouver le nom de l'application + + + + Successfully imported {}. Reload library + Importation réussie de {}. Recharger la bibliothèque + + + + Failed to import {} + Impossible d'importer {} + + + + Successfully imported {} Games. Reloading Library + Importation réussie de {} Jeux. Bibliothèque de rechargement + + + + InfoTabs + + + Back + Dos + + + + Game Info + Info de jeu + + + + Settings + Paramètres + + + + InstallDialog + + + <h3>Install {}</h3> + <h3>Installer {}</h3> + + + + Max workers (0: Default) + Travailleurs maximum (0: Par défaut) + + + + Force download + Téléchargement forcé + + + + Ignore free space (Warning!) + Ignorer l'espace libre (Attention!) + + + + InstallInfoDialog + + + Download size: {}GB +Install size: {}GB + Taille du téléchargement: {}GB +Taille de l'installation: {}GB + + + + Install + Installer + + + + Cancel + Annuler + + + + InstalledListWidget + + + Launch + Lancer + + + + Developer: + Développeur: + + + + LaunchDialog + + + Launching Rare + Lancer Rare + + + + Logging in + Se connecter + + + + Downloading Images + Téléchargement d'images + + + + Starting... + Démarrage... + + + + LaunchThread + + + Downloading Images + Téléchargement d'images + + + + LegendarySettings + + + Legendary settings + Legendary paramètres + + + + Default installation directory + Répertoire d'installation par défaut + + + + Max workers for Download (Less: slower download)(0: Default) + Nombre maximum de travailleurs pour le téléchargement (Moins: téléchargement plus lent)(0: Défaut) + + + + Cleanup + Nettoyage + + + + Remove everything + Enlever tout + + + + Clean, but keep manifests + Nettoyer, mais garder les manifestes + + + + Cleanup complete! Successfully removed {} MB + Nettoyage terminé ! J'ai réussi à supprimer {} MB + + + + LinuxSettings + + + Linux settings + Linux paramètres + + + + Default Wine Prefix + Défaut Wine Prefix + + + + Default Wine executable + Défaut Wine exécutable + + + + ListWidgetUninstalled + + + Install + Installer + + + + LoginDialog + + + Select one option to Login + Sélectionnez une option pour vous connecter + + + + Use Browser + Utiliser le navigateur + + + + This opens your default browser. Login and copy the text + Cela ouvre votre navigateur par défaut. Connectez-vous et copiez le texte + + + + MiniWidget + + + Logged in as + Connecté en tant que + + + + Account settings + paramètres du compte + + + + Logout + Déconnexion + + + + Do you really want to logout + Voulez-vous vraiment vous déconnecter? + + + + PathEdit + + + Select Path + Sélectionner le chemin + + + + Choose Path + Choisir le chemin + + + + PathInputDialog + + + Cancel + Annuler + + + + RareSettings + + + Rare settings + Rare paramètres + + + + Save + Sauvez + + + + Image Directory + Répertoire d'images + + + + Language + Langue + + + + Confirm launch of game + Confirmation du lancement du jeu + + + + Restart Application to activate changes + Redémarrez l'application pour activer les changements + + + + SyncSaves + + + Cloud Saves + Cloud Saves + + + + Found Saves for folowing Games + Sauvegardes trouvées pour les jeux suivants + + + + Your games does not support Cloud Saves + Vos jeux ne prennent pas en charge les sauvegardes en nuage + + + + Sync all games + Sync tous les jeux + + + + Found no savepath + Pas de chemin de sauvegarde trouvé + + + + No save path was found. Please select path or skip + Aucun chemin de sauvegarde n'a été trouvé. Veuillez sélectionner le chemin ou passer + + + + SyncWidget + + + Path not found + Chemin non trouvé + + + + Local Save date: + Local Save date: + + + + No Local Save files + Pas de fichiers de sauvegarde locaux + + + + Cloud save date: + Cloud save date: + + + + No Cloud saves + Pas Cloud saves + + + + Game is up to date + Le jeu est à jour + + + + Upload anyway + Télécharger quand même + + + + Download anyway + Télécharger en tout cas + + + + Cloud save is newer + La sauvegarde en nuage est plus récente + + + + Download Cloud saves + Télécharger Cloud saves + + + + Upload Saves + Upload Saves + + + + Local save is newer + La sauvegarde locale est plus récente + + + + Upload saves + Upload Saves + + + + Download saves + Télécharger les sauvegardes + + + + Change path + Changement de trajectoire + + + + Uploading... + Téléchargement... + + + + Upload finished + Téléchargement terminé + + + + Downloading... + Téléchargement... + + + + Download finished + Téléchargement terminé + + + + TabWidget + + + Games + Jeus + + + + UpdateWidget + + + Update Game + Jeu de mise à jour + + + diff --git a/Rare/utils/LegendaryApi.py b/Rare/utils/LegendaryApi.py index e22baf89..0f5ce4cd 100644 --- a/Rare/utils/LegendaryApi.py +++ b/Rare/utils/LegendaryApi.py @@ -47,7 +47,9 @@ def launch_game(core, app_name: str, offline: bool = False, skip_version_check: return process, params -def uninstall(app_name: str, core): +def uninstall(app_name: str, core, options=None): + if not options: + options = {"keep_files": False} igame = core.get_installed_game(app_name) try: # Remove DLC first so directory is empty when game uninstall runs @@ -55,12 +57,13 @@ def uninstall(app_name: str, core): for dlc in dlcs: if (idlc := core.get_installed_game(dlc.app_name)) is not None: logger.info(f'Uninstalling DLC "{dlc.app_name}"...') - core.uninstall_game(idlc, delete_files=True) + core.uninstall_game(idlc, delete_files=not options["keep_files"]) logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...') - core.uninstall_game(igame, delete_files=True, delete_root_directory=True) + core.uninstall_game(igame, delete_files=not options["keep_files"], delete_root_directory=True) logger.info('Game has been uninstalled.') - shutil.rmtree(igame.install_path) + if not options["keep_files"]: + shutil.rmtree(igame.install_path) except Exception as e: logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.') diff --git a/Rare/utils/utils.py b/Rare/utils/utils.py index d25436bf..517e8796 100644 --- a/Rare/utils/utils.py +++ b/Rare/utils/utils.py @@ -4,15 +4,15 @@ import shutil from logging import getLogger import requests -from PIL import Image +from PIL import Image, UnidentifiedImageError from PyQt5.QtCore import pyqtSignal, QLocale, QSettings -from Rare import lang_path +from Rare import lang_path, __version__ from custom_legendary.core import LegendaryCore logger = getLogger("Utils") s = QSettings("Rare", "Rare") -IMAGE_DIR = s.value("img_dir", os.path.expanduser("~/.cache/rare"), type=str) +IMAGE_DIR = s.value("img_dir", os.path.expanduser("~/.cache/rare/images"), type=str) logger.info("IMAGE DIRECTORY: " + IMAGE_DIR) @@ -38,6 +38,7 @@ def download_image(game, force=False): if not os.path.isdir(f"{IMAGE_DIR}/" + game.app_name): os.mkdir(f"{IMAGE_DIR}/" + game.app_name) + # to git picture updates if not os.path.isfile(f"{IMAGE_DIR}/{game.app_name}/image.json"): json_data = {"DieselGameBoxTall": None, "DieselGameBoxLogo": None} else: @@ -56,7 +57,13 @@ def download_image(game, force=False): url = image["url"] with open(f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png", "wb") as f: f.write(requests.get(url).content) - f.close() + try: + img = Image.open(f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png") + img = img.resize((200, int(200*4/3))) + img.save(f"{IMAGE_DIR}/{game.app_name}/{image['type']}.png") + except UnidentifiedImageError as e: + logger.warning(e) + # scale and grey if not os.path.isfile(f'{IMAGE_DIR}/' + game.app_name + '/UninstalledArt.png'): @@ -67,6 +74,7 @@ def download_image(game, force=False): bg = Image.open(f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png") uninstalledArt = bg.convert('L') + uninstalledArt = uninstalledArt.resize((200, int(200*4/3))) uninstalledArt.save(f'{IMAGE_DIR}/{game.app_name}/UninstalledArt.png') elif os.path.isfile(f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png"): bg: Image.Image = Image.open(f"{IMAGE_DIR}/{game.app_name}/DieselGameBoxLogo.png") @@ -91,7 +99,7 @@ def download_image(game, force=False): uninstalledArt = uninstalledArt.convert('L') uninstalledArt.save(f'{IMAGE_DIR}/' + game.app_name + '/UninstalledArt.png') else: - logger.warning(f"File {IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png dowsn't exist") + logger.warning(f"File {IMAGE_DIR}/{game.app_name}/DieselGameBoxTall.png doesn't exist") def get_lang(): @@ -110,3 +118,9 @@ def get_possible_langs(): if i.endswith(".qm"): langs.append(i.split(".")[0]) return langs + + +def get_latest_version(): + resp = requests.get("https://api.github.com/repos/Dummerle/Rare/releases/latest") + tag = json.loads(resp.content.decode("utf-8"))["tag_name"] + return tag