From 75b39cb1896d0ef7bc389ce2bc781905b871e1f9 Mon Sep 17 00:00:00 2001 From: Dummerle Date: Wed, 17 Feb 2021 17:46:03 +0100 Subject: [PATCH] Download Games, update List --- README.md | 2 +- Rare/Components/TabWidget.py | 17 +- Rare/Components/Tabs/Downloads/DownloadTab.py | 150 +++++++++++++++++- Rare/Components/Tabs/Games/GameList.py | 48 ++++-- .../Tabs/Games/GameWidgetInstalled.py | 38 +++-- .../Tabs/Games/GameWidgetUninstalled.py | 17 +- .../Tabs/Games/{Games.py => GamesTab.py} | 14 +- Rare/utils/Dialogs/InstallDialog.py | 52 +++++- 8 files changed, 289 insertions(+), 49 deletions(-) rename Rare/Components/Tabs/Games/{Games.py => GamesTab.py} (75%) diff --git a/README.md b/README.md index 9039c624..58e925ac 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ GUI for legendary. An Epic Games Launcher open source alternative ## This is a new version with better Styles. It has not many features, but i work on it. -It will take some time, because I want to reinmplement some features on a smoother way. +It will take some time, because I want to reimplement some features on a smoother way. diff --git a/Rare/Components/TabWidget.py b/Rare/Components/TabWidget.py index 6145d6cf..78d92a7d 100644 --- a/Rare/Components/TabWidget.py +++ b/Rare/Components/TabWidget.py @@ -1,8 +1,10 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QTabWidget, QTabBar, QWidget, QToolButton, QWidgetAction, QMenu + from Rare import style_path from Rare.Components.Tabs.Account.AccountWidget import MiniWidget -from Rare.Components.Tabs.Games.Games import Games +from Rare.Components.Tabs.Downloads.DownloadTab import DownloadTab +from Rare.Components.Tabs.Games.GamesTab import Games class TabWidget(QTabWidget): @@ -11,12 +13,12 @@ class TabWidget(QTabWidget): self.setTabBar(TabBar(2)) self.game_list = Games(core) - self.addTab(self.game_list, self.tr("Games")) - self.downloads = QWidget() - self.addTab(self.downloads, "Downloads") - + self.downloadTab = DownloadTab(core) + self.addTab(self.downloadTab, "Downloads") + self.downloadTab.finished.connect(self.game_list.game_list.update_list) + self.game_list.game_list.install_game.connect(lambda x: self.downloadTab.install_game(x)) # Space Tab self.addTab(QWidget(), "") self.setTabEnabled(2, False) @@ -35,9 +37,6 @@ class TabWidget(QTabWidget): self.tabBar().setMinimumWidth(self.width()) super(TabWidget, self).resizeEvent(event) - def download(self): - self.downloads.download() - class TabBar(QTabBar): def __init__(self, expanded): @@ -59,7 +58,7 @@ class TabButtonWidget(QToolButton): super(TabButtonWidget, self).__init__() self.setText("Icon") self.setPopupMode(QToolButton.InstantPopup) - self.setIcon(QIcon(style_path+"/Icons/account.png")) + self.setIcon(QIcon(style_path + "/Icons/account.png")) self.setToolTip("Account") self.setMenu(QMenu()) action = QWidgetAction(self) diff --git a/Rare/Components/Tabs/Downloads/DownloadTab.py b/Rare/Components/Tabs/Downloads/DownloadTab.py index d51c85c7..097aa2de 100644 --- a/Rare/Components/Tabs/Downloads/DownloadTab.py +++ b/Rare/Components/Tabs/Downloads/DownloadTab.py @@ -1,6 +1,152 @@ -from PyQt5.QtWidgets import QWidget +import os +import subprocess +import time +from logging import getLogger + +from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtWidgets import QWidget, QMessageBox, QVBoxLayout, QLabel, QGridLayout, QProgressBar +from legendary.core import LegendaryCore + +from Rare.utils.Dialogs.InstallDialog import InstallInfoDialog + +logger = getLogger("Download") + + +class DownloadThread(QThread): + status = pyqtSignal(str) + + def __init__(self, dlm, core: LegendaryCore, igame): + super(DownloadThread, self).__init__() + self.dlm = dlm + self.core = core + self.igame = igame + + def run(self): + start_time = time.time() + try: + + self.dlm.start() + self.dlm.join() + except: + logger.error(f"Installation failed after{time.time() - start_time:.02f} seconds.") + 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}"') + + 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): - def __init__(self): + finished = pyqtSignal() + thread: QThread + + def __init__(self, core: LegendaryCore): super(DownloadTab, self).__init__() + self.core = core + self.layout = QVBoxLayout() + + self.installing_game = QLabel("Installing Game: None") + self.dl_speed = QLabel("Download speed: 0MB/s") + self.cache_used = QLabel("Cache used: 0MB") + self.downloaded = QLabel("Downloaded: 0MB") + + self.info_layout = QGridLayout() + + self.info_layout.addWidget(self.installing_game, 0, 0) + self.info_layout.addWidget(self.dl_speed, 0, 1) + self.info_layout.addWidget(self.cache_used, 1,0) + self.info_layout.addWidget(self.downloaded, 1,1) + + self.layout.addLayout(self.info_layout) + self.prog_bar = QProgressBar() + self.layout.addWidget(self.prog_bar) + + self.layout.addWidget(QLabel("WARNING: This feature is not implemented. It is normal, if there is no progress. The progress is in console")) + + self.installing_game_widget = QLabel("No active Download") + self.layout.addWidget(self.installing_game_widget) + + self.layout.addStretch(1) + + self.setLayout(self.layout) + + def install_game(self, options: {}): + game = self.core.get_game(options["app_name"]) + dlm, analysis, igame = self.core.prepare_download( + game=game, + base_path=options["options"]["path"], + max_workers=options["options"]["max_workers"]) + if not analysis.dl_size: + QMessageBox.information(self, "Warning", "Download size is 0") + return + # Information + if not InstallInfoDialog(dl_size=analysis.dl_size, install_size=analysis.install_size).get_accept(): + return + + self.installing_game_widget.setText("") + self.installing_game.setText("Installing Game: "+ game.app_title) + res = self.core.check_installation_conditions(analysis=analysis, install=igame, game=game, + updating=self.core.is_installed(options["app_name"]), + ) + if res.warnings: + for w in sorted(res.warnings): + logger.warning(w) + if res.failures: + for msg in sorted(res.failures): + logger.error(msg) + logger.error('Installation cannot proceed, exiting.') + QMessageBox.warning(self, "Installation failed", "Installation failed. See logs for more information") + return + + self.thread = DownloadThread(dlm, self.core, igame) + self.thread.status.connect(self.status) + self.thread.start() + + def status(self, text): + if text == "dl_finished": + pass + elif text == "finish": + QMessageBox.information(self, "Info", "Download finished") + self.finished.emit() + self.installing_game.setText("Installing Game: No running download") + elif text == "error": + QMessageBox.warning(self, "warn", "Download error") + + def update_game(self, app_name: str): + print("Update ", app_name) diff --git a/Rare/Components/Tabs/Games/GameList.py b/Rare/Components/Tabs/Games/GameList.py index e45f3799..a41f92c9 100644 --- a/Rare/Components/Tabs/Games/GameList.py +++ b/Rare/Components/Tabs/Games/GameList.py @@ -1,4 +1,4 @@ -from PyQt5.QtCore import Qt +from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import * from legendary.core import LegendaryCore @@ -8,30 +8,45 @@ from Rare.utils.QtExtensions import FlowLayout class GameList(QScrollArea): + install_game = pyqtSignal(dict) + def __init__(self, core: LegendaryCore): super(GameList, self).__init__() self.core = core - self.widget = QWidget() - self .setObjectName("list_widget") + self.widgets = [] + + self.setObjectName("list_widget") self.setWidgetResizable(True) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.init_ui() + + def init_ui(self): + self.widget = QWidget() + self.widgets=[] self.layout = FlowLayout() - - for i in self.core.get_installed_list(): + # Installed Games + for game in self.core.get_installed_list(): # continue - self.layout.addWidget(GameWidgetInstalled(core, i)) + widget = GameWidgetInstalled(self.core, game) + self.layout.addWidget(widget) + widget.update_list.connect(self.update_list) uninstalled_games = [] - installed = [i.app_name for i in core.get_installed_list()] - for game in sorted(core.get_game_list(), key=lambda x: x.app_title): + installed = [] + installed = [i.app_name for i in self.core.get_installed_list()] + print(len(installed)) + # get Uninstalled games + print(len(self.core.get_game_list())) + for game in sorted(self.core.get_game_list(), key=lambda x: x.app_title): if not game.app_name in installed: uninstalled_games.append(game) - self.widgets = [] - - for i in uninstalled_games: - widget = GameWidgetUninstalled(core, i) + # add uninstalled to gui + print(len(uninstalled_games)) + for game in uninstalled_games: + widget = GameWidgetUninstalled(self.core, game) + widget.install_game.connect(lambda options: self.install_game.emit(options)) self.layout.addWidget(widget) self.widgets.append(widget) @@ -48,4 +63,11 @@ class GameList(QScrollArea): def installed_only(self, i_o: bool): # TODO save state for w in self.widgets: - w.setVisible(not i_o) \ No newline at end of file + w.setVisible(not i_o) + + def update_list(self): + print("Updating List") + self.setWidget(QWidget()) + self.core.login() + self.init_ui() + self.update() \ No newline at end of file diff --git a/Rare/Components/Tabs/Games/GameWidgetInstalled.py b/Rare/Components/Tabs/Games/GameWidgetInstalled.py index e187d0e0..ea3f0ec1 100644 --- a/Rare/Components/Tabs/Games/GameWidgetInstalled.py +++ b/Rare/Components/Tabs/Games/GameWidgetInstalled.py @@ -1,8 +1,8 @@ import os from logging import getLogger -from PyQt5.QtCore import Qt, QEvent -from PyQt5.QtGui import QPixmap, QIcon, QMouseEvent +from PyQt5.QtCore import QEvent, pyqtSignal +from PyQt5.QtGui import QPixmap, QIcon from PyQt5.QtWidgets import * from legendary.core import LegendaryCore from legendary.models.game import InstalledGame @@ -16,6 +16,8 @@ logger = getLogger("GameWidgetInstalled") class GameWidgetInstalled(QWidget): + update_list = pyqtSignal() + def __init__(self, core: LegendaryCore, game: InstalledGame): super(GameWidgetInstalled, self).__init__() self.setObjectName("game_widget_parent") @@ -26,7 +28,7 @@ class GameWidgetInstalled(QWidget): self.update_available = self.core.get_asset(self.game.app_name, True).build_version != game.version if self.update_available: - logger.info("Update available for game: "+ self.game.app_name) + logger.info("Update available for game: " + self.game.app_name) if os.path.exists(f"{IMAGE_DIR}/{game.app_name}/FinalArt.png"): pixmap = QPixmap(f"{IMAGE_DIR}/{game.app_name}/FinalArt.png") @@ -40,7 +42,6 @@ class GameWidgetInstalled(QWidget): if pixmap: w = 200 pixmap = pixmap.scaled(w, int(w * 4 / 3)) - self.image = ClickableLabel() self.image.setObjectName("game_widget") self.image.setPixmap(pixmap) @@ -53,11 +54,13 @@ class GameWidgetInstalled(QWidget): self.title_label.setObjectName("game_widget") minilayout.addWidget(self.title_label) # minilayout.addStretch(1) - self.menu = QPushButton(QIcon(style_path + "/Icons/menu.png"), "") - self.menu.setMenu(Menu()) - self.menu.setObjectName("menu") - self.menu.setFixedWidth(10) - minilayout.addWidget(self.menu) + self.menu_btn = QPushButton(QIcon(style_path + "/Icons/menu.png"), "") + self.menu = Menu() + self.menu.action.connect(self.menu_action) + self.menu_btn.setMenu(self.menu) + self.menu_btn.setObjectName("menu") + self.menu_btn.setFixedWidth(10) + minilayout.addWidget(self.menu_btn) minilayout.addStretch(1) self.layout.addLayout(minilayout) @@ -93,14 +96,25 @@ class GameWidgetInstalled(QWidget): self.info_label.setText("") self.running = False + def menu_action(self, action: str): + if action == "uninstall": + if QMessageBox.question(self, "Uninstall", f"Do you want to uninstall {self.game.title}", + QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes: + logger.info("Uninstalling " + self.game.title) + self.core.uninstall_game(self.game) + self.update_list.emit() + + class Menu(QMenu): + action = pyqtSignal(str) + def __init__(self): super(Menu, self).__init__() - self.addAction("Game info", self.info) - self.addAction("Uninstall", self.uninstall) + self.addAction("Game info", lambda: self.action.emit("info")) + self.addAction("Uninstall", lambda: self.action.emit("uninstall")) def info(self): pass def uninstall(self): - pass \ No newline at end of file + pass diff --git a/Rare/Components/Tabs/Games/GameWidgetUninstalled.py b/Rare/Components/Tabs/Games/GameWidgetUninstalled.py index 47a3c420..f7c508cc 100644 --- a/Rare/Components/Tabs/Games/GameWidgetUninstalled.py +++ b/Rare/Components/Tabs/Games/GameWidgetUninstalled.py @@ -1,8 +1,10 @@ import os from logging import getLogger +from multiprocessing import Queue as MPQueue +from PyQt5.QtCore import pyqtSignal from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QInputDialog +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel from legendary.core import LegendaryCore from legendary.models.game import Game @@ -14,6 +16,8 @@ logger = getLogger("Uninstalled") class GameWidgetUninstalled(QWidget): + install_game = pyqtSignal(dict) + def __init__(self, core: LegendaryCore, game: Game): super(GameWidgetUninstalled, self).__init__() self.layout = QVBoxLayout() @@ -57,7 +61,16 @@ class GameWidgetUninstalled(QWidget): def install(self): logger.info("Install " + self.game.app_title) + + infos = InstallDialog().get_information() if infos != 0: path, max_workers = infos - print(path, max_workers) \ No newline at end of file + self.install_game.emit({ + "app_name": self.game.app_name, + "options": { + "path": path, + "max_workers": max_workers + } + }) + # wait for update of legendary to get progress diff --git a/Rare/Components/Tabs/Games/Games.py b/Rare/Components/Tabs/Games/GamesTab.py similarity index 75% rename from Rare/Components/Tabs/Games/Games.py rename to Rare/Components/Tabs/Games/GamesTab.py index 0721f5c4..4830a291 100644 --- a/Rare/Components/Tabs/Games/Games.py +++ b/Rare/Components/Tabs/Games/GamesTab.py @@ -1,4 +1,4 @@ -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QLineEdit, QLabel +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QLineEdit, QLabel, QPushButton, QStyle from Rare.Components.Tabs.Games.GameList import GameList @@ -12,13 +12,13 @@ class Games(QWidget): 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.head_bar.installed_only.stateChanged.connect(lambda: + self.game_list.installed_only( + self.head_bar.installed_only.isChecked())) + self.head_bar.refresh_list.clicked.connect(lambda: self.game_list.update_list()) self.layout.addWidget(self.head_bar) self.layout.addWidget(self.game_list) # self.layout.addStretch(1) @@ -45,4 +45,8 @@ class GameListHeadBar(QWidget): self.view = QCheckBox("Icon view") self.layout.addWidget(self.view) + self.refresh_list = QPushButton() + self.refresh_list.setIcon(self.style().standardIcon(getattr(QStyle, "SP_BrowserReload"))) # Reload icon + self.layout.addWidget(self.refresh_list) + self.setLayout(self.layout) diff --git a/Rare/utils/Dialogs/InstallDialog.py b/Rare/utils/Dialogs/InstallDialog.py index 817e8a1f..a83456c9 100644 --- a/Rare/utils/Dialogs/InstallDialog.py +++ b/Rare/utils/Dialogs/InstallDialog.py @@ -6,27 +6,31 @@ from Rare.utils.QtExtensions import PathEdit class InstallDialog(QDialog): + infos = 0 + def __init__(self): super(InstallDialog, self).__init__() self.layout = QVBoxLayout() self.form = QFormLayout() default_path = os.path.expanduser("~/legendary") - #TODO read from config + # TODO read from config self.install_path_field = PathEdit(text=default_path, type_of_file=QFileDialog.DirectoryOnly) self.form.addRow(QLabel("Install directory"), self.install_path_field) self.max_workes = QSpinBox() - self.form.addRow(QLabel("Max workers"), self.max_workes) + self.form.addRow(QLabel("Max workers (0: Default)"), self.max_workes) self.layout.addLayout(self.form) - self.ok = QPushButton("Install") + self.ok_btn = QPushButton("Get Download infos") + self.ok_btn.clicked.connect(self.ok) self.cancel = QPushButton("Cancel") + self.cancel.clicked.connect(lambda: self.close()) self.button_layout = QHBoxLayout() self.button_layout.addStretch(1) - self.button_layout.addWidget(self.ok) + self.button_layout.addWidget(self.ok_btn) self.button_layout.addWidget(self.cancel) self.layout.addLayout(self.button_layout) @@ -35,4 +39,42 @@ class InstallDialog(QDialog): def get_information(self): self.exec_() - return self.install_path_field.text(), self.max_workes.value() + return self.infos + + def ok(self): + self.infos = self.install_path_field.text(), self.max_workes.value() + self.close() + + +class InstallInfoDialog(QDialog): + accept: bool = False + + def __init__(self, dl_size, install_size): + super(InstallInfoDialog, self).__init__() + self.layout = QVBoxLayout() + self.infos = QLabel(f"Download size: {(dl_size/1024**3):.02f}GB\nInstall size: {(install_size/1024**3):.02f}GB") + self.layout.addWidget(self.infos) + + self.btn_layout = QHBoxLayout() + self.install_btn = QPushButton("Install") + self.install_btn.clicked.connect(self.install) + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(self.cancel) + self.btn_layout.addStretch(1) + self.btn_layout.addWidget(self.install_btn) + self.btn_layout.addWidget(self.cancel_button) + self.layout.addLayout(self.btn_layout) + + self.setLayout(self.layout) + + def get_accept(self): + self.exec_() + return self.accept + + def install(self): + self.accept = True + self.close() + + def cancel(self): + self.accept = False + self.close()