1
0
Fork 0
mirror of synced 2024-06-23 08:40:45 +12:00

Import Games, Toggle list/iconview Some other nice stuff

This commit is contained in:
Dummerle 2021-03-09 16:52:51 +01:00
parent 9f28666d66
commit a616bc67a3
15 changed files with 553 additions and 141 deletions

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (2)" project-jdk-type="Python SDK" />
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
</project>

View file

@ -6,7 +6,7 @@ from Rare.Components.TabWidget import TabWidget
class MainWindow(QMainWindow):
def __init__(self, core):
super(MainWindow, self).__init__()
self.setGeometry(0, 0, 1000, 800)
self.setGeometry(0, 0, 1200, 800)
self.setWindowTitle("Rare - GUI for legendary")
self.setCentralWidget(TabWidget(core))
self.show()

View file

@ -9,6 +9,7 @@ from Rare.Components.Tabs.Account.AccountWidget import MiniWidget
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.utils.Models import InstallOptions
class TabWidget(QTabWidget):
@ -26,6 +27,10 @@ class TabWidget(QTabWidget):
self.downloadTab.finished.connect(self.game_list.default_widget.game_list.update_list)
self.game_list.default_widget.game_list.install_game.connect(lambda x: self.downloadTab.install_game(x))
self.game_list.game_info.info.verify_game.connect(lambda app_name: self.downloadTab.install_game(InstallOptions(app_name, core.get_installed_game(app_name).install_path, repair=True)))
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")

View file

@ -10,6 +10,7 @@ from legendary.models.game import Game
from notifypy import Notify
from Rare.Components.Dialogs.InstallDialog import InstallInfoDialog
from Rare.utils.LegendaryApi import VerifyThread
from Rare.utils.Models import InstallOptions
logger = getLogger("Download")
@ -18,11 +19,13 @@ logger = getLogger("Download")
class DownloadThread(QThread):
status = pyqtSignal(str)
def __init__(self, dlm, core: LegendaryCore, igame):
def __init__(self, dlm, core: LegendaryCore, igame, repair=False, repair_file=None):
super(DownloadThread, self).__init__()
self.dlm = dlm
self.core = core
self.igame = igame
self.repair = repair
self.repair_file = repair_file
def run(self):
start_time = time.time()
@ -43,6 +46,7 @@ class DownloadThread(QThread):
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:')
@ -55,6 +59,19 @@ class DownloadThread(QThread):
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")
@ -137,28 +154,51 @@ class DownloadTab(QWidget):
igame = self.core.get_installed_game(options.app_name)
if igame.needs_verification and not options.repair:
options.repair = True
repair_file = None
if options.repair:
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{options.app_name}.repair')
if not game:
QMessageBox.warning(self, "Error", "Could not find Game in your library")
return
if game.is_dlc:
logger.info("DLCs are currently not supported")
return
if game.is_dlc:
logger.info('Install candidate is DLC')
app_name = game.metadata['mainGameItem']['releaseInfo'][0]['appId']
base_game = self.core.get_game(app_name)
# check if base_game is actually installed
if not self.core.is_installed(app_name):
# download mode doesn't care about whether or not something's installed
logger.error("Base Game is not installed")
return
else:
base_game = None
if options.repair:
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{options.app_name}.repair')
if not self.core.is_installed(game.app_name):
return
if not os.path.exists(repair_file):
logger.info("Game has not been verified yet")
if QMessageBox.question(self, "Verify", "Game has not been verified yet. Do you want to verify first?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes:
self.verify_thread = VerifyThread(self.core, game.app_name)
self.verify_thread.finished.connect(
lambda: self.prepare_download(game, options, base_game, repair_file))
self.verify_thread.start()
return
self.repair()
self.prepare_download(game, options, base_game, repair_file)
def prepare_download(self, game, options, base_game, repair_file):
dlm, analysis, igame = self.core.prepare_download(
game=game,
base_path=options.path,
max_workers=options.max_workers)
max_workers=options.max_workers, base_game=base_game, repair=options.repair)
if not analysis.dl_size:
QMessageBox.information(self, "Warning", self.tr("Download size is 0. Game already exists"))
return
@ -182,7 +222,7 @@ class DownloadTab(QWidget):
self.tr("Installation failed. See logs for more information"))
return
self.active_game = game
self.thread = DownloadThread(dlm, self.core, igame)
self.thread = DownloadThread(dlm, self.core, igame, options.repair, repair_file)
self.thread.status.connect(self.status)
self.thread.start()
@ -192,7 +232,7 @@ class DownloadTab(QWidget):
elif text == "finish":
notification = Notify()
notification.title = self.tr("Installation finished")
notification.message = self.tr("Download of game ")+self.active_game.app_title
notification.message = self.tr("Download of game ") + self.active_game.app_title
notification.send()
# QMessageBox.information(self, "Info", "Download finished")
self.finished.emit()
@ -204,7 +244,6 @@ class DownloadTab(QWidget):
print("Update ", app_name)
self.install_game(InstallOptions(app_name))
def repair(self):
pass

View file

@ -2,11 +2,13 @@ import os
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QLabel, QHBoxLayout, QTabWidget, QMessageBox
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QLabel, QHBoxLayout, QTabWidget, QMessageBox, \
QProgressBar, QStackedWidget
from legendary.core import LegendaryCore
from legendary.models.game import InstalledGame, Game
from Rare.utils import LegendaryApi
from Rare.utils.LegendaryApi import VerifyThread
from Rare.utils.QtExtensions import SideTabBar
from Rare.utils.utils import IMAGE_DIR
@ -27,6 +29,8 @@ class GameInfo(QWidget):
igame: InstalledGame
game: Game
update_list = pyqtSignal()
verify_game = pyqtSignal(str)
def __init__(self, core: LegendaryCore):
super(GameInfo, self).__init__()
self.core = core
@ -67,6 +71,8 @@ class GameInfo(QWidget):
self.game_actions = GameActions()
self.game_actions.uninstall_button.clicked.connect(self.uninstall)
self.game_actions.verify_button.clicked.connect(self.verify)
self.game_actions.repair_button.clicked.connect(self.repair)
self.layout.addLayout(top_layout)
self.layout.addWidget(self.game_actions)
@ -74,11 +80,39 @@ class GameInfo(QWidget):
self.setLayout(self.layout)
def uninstall(self):
if QMessageBox.question(self, "Uninstall", self.tr("Are you sure to uninstall " + self.game.app_title), QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
if QMessageBox.question(self, "Uninstall", self.tr("Are you sure to uninstall " + self.game.app_title),
QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
LegendaryApi.uninstall(self.game.app_name, self.core)
self.update_list.emit()
self.back_button.click()
def repair(self):
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{self.game.app_name}.repair')
if not os.path.exists(repair_file):
QMessageBox.warning(self, "Warning", self.tr(
"Repair file does not exist or game does not need a repair. Please verify game first"))
return
self.verify_game.emit(self.game.app_name)
def verify(self):
self.game_actions.verify_widget.setCurrentIndex(1)
self.verify_thread = VerifyThread(self.core, self.game.app_name)
self.verify_thread.status.connect(lambda x: self.game_actions.verify_progress_bar.setValue(x[0] * 100 / x[1]))
self.verify_thread.summary.connect(self.finish_verify)
self.verify_thread.start()
def finish_verify(self, failed):
failed, missing = failed
if failed == 0 and missing == 0:
QMessageBox.information(self, "Summary", "Game was verified successfully. No missing or corrupt files found")
else:
ans = QMessageBox.question(self, "Summary", self.tr(
'Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them?').format(
failed, missing), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes)
if ans == QMessageBox.Yes:
self.verify_game.emit(self.game.app_name)
self.game_actions.verify_widget.setCurrentIndex(0)
def update_game(self, app_name):
self.game = self.core.get_game(app_name)
self.igame = self.core.get_installed_game(app_name)
@ -112,10 +146,35 @@ class GameActions(QWidget):
self.layout = QVBoxLayout()
self.game_actions = QLabel("<h3>Game actions</h3>")
self.layout.addWidget(self.game_actions)
uninstall_layout = QHBoxLayout()
self.uninstall_game = QLabel(self.tr("Uninstall game"))
uninstall_layout.addWidget(self.uninstall_game)
self.uninstall_button = QPushButton("Uninstall")
self.uninstall_button.setFixedWidth(250)
uninstall_layout.addWidget(self.uninstall_button)
self.layout.addLayout(uninstall_layout)
self.setLayout(self.layout)
verify_layout = QHBoxLayout()
self.verify_game = QLabel(self.tr("Verify Game"))
verify_layout.addWidget(self.verify_game)
self.verify_widget = QStackedWidget()
self.verify_widget.setMaximumHeight(20)
self.verify_widget.setFixedWidth(250)
self.verify_button = QPushButton("Verify")
self.verify_widget.addWidget(self.verify_button)
self.verify_progress_bar = QProgressBar()
self.verify_progress_bar.setMaximum(100)
self.verify_widget.addWidget(self.verify_progress_bar)
verify_layout.addWidget(self.verify_widget)
self.layout.addLayout(verify_layout)
repair_layout = QHBoxLayout()
repair_info = QLabel("Repair Game")
repair_layout.addWidget(repair_info)
self.repair_button = QPushButton("Repair")
self.repair_button.setFixedWidth(250)
repair_layout.addWidget(self.repair_button)
self.layout.addLayout(repair_layout)
self.setLayout(self.layout)

View file

@ -19,7 +19,6 @@ class GameList(QScrollArea):
super(GameList, self).__init__()
self.core = core
self.widgets = []
self.setObjectName("list_widget")
self.setWidgetResizable(True)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
@ -88,3 +87,6 @@ class GameList(QScrollArea):
self.setWidget(QWidget())
self.init_ui(icon_view)
self.update()
def import_game(self):
pass

View file

@ -1,7 +1,7 @@
import os
from logging import getLogger
from PyQt5.QtCore import QEvent, pyqtSignal, QSettings, QSize
from PyQt5.QtCore import QEvent, pyqtSignal, QSettings, QSize, Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import *
from legendary.core import LegendaryCore
@ -22,6 +22,7 @@ class GameWidgetInstalled(QWidget):
def __init__(self, core: LegendaryCore, game: InstalledGame):
super(GameWidgetInstalled, self).__init__()
self.setObjectName("game_widget_parent")
self.layout = QVBoxLayout()
self.core = core
self.game = game
@ -54,6 +55,7 @@ class GameWidgetInstalled(QWidget):
self.layout.addWidget(self.image)
self.title_label = QLabel(f"<h4>{game.title}</h4>")
self.title_label.setAutoFillBackground(False)
self.title_label.setWordWrap(True)
self.title_label.setFixedWidth(175)
minilayout = QHBoxLayout()
@ -81,9 +83,14 @@ class GameWidgetInstalled(QWidget):
self.layout.addLayout(minilayout)
self.info_label = QLabel(self.info_text)
self.info_label.setAutoFillBackground(False)
self.info_label.setObjectName("info_label")
self.layout.addWidget(self.info_label)
#p = self.palette()
#p.setColor(self.backgroundRole(), Qt.red)
#self.setPalette(p)
self.setLayout(self.layout)
self.setFixedWidth(self.sizeHint().width())
@ -92,9 +99,14 @@ class GameWidgetInstalled(QWidget):
self.info_label.setText(self.tr("Please update Game"))
elif not self.running:
self.info_label.setText("Start Game")
else:
self.info_label.setText(self.tr("Game running"))
def leaveEvent(self, a0: QEvent) -> None:
self.info_label.setText(self.info_text)
if self.running:
self.info_label.setText(self.tr("Game running"))
else:
self.info_label.setText(self.info_text)
def mousePressEvent(self, a0) -> None:
self.launch()

View file

@ -1,11 +1,11 @@
from PyQt5.QtCore import QSettings
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QLineEdit, QLabel, QPushButton, QStyle, \
QStackedLayout
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QLineEdit, QPushButton, QStackedLayout
from qtawesome import icon
from Rare.Components.Tabs.Games.GameInfo.GameInfo import InfoTabs
from Rare.Components.Tabs.Games.GameList import GameList
from Rare.utils.QtExtensions import Switch
from Rare.Components.Tabs.Games.ImportWidget import ImportWidget
from Rare.utils.QtExtensions import SelectViewWidget
class GameTab(QWidget):
@ -14,14 +14,23 @@ class GameTab(QWidget):
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.back_button.clicked.connect(lambda: self.layout.setCurrentIndex(0))
self.game_info.info.update_list.connect(
lambda: self.default_widget.game_list.update_list(not self.default_widget.head_bar.view.isChecked()))
self.game_info.info.update_list.connect(self.update_list)
self.layout.addWidget(self.game_info)
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.info.update_game(app_name)
self.layout.setCurrentIndex(1)
@ -43,12 +52,15 @@ class Games(QWidget):
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(not self.head_bar.view.isChecked()))
self.head_bar.refresh_list.clicked.connect(
lambda: self.game_list.update_list(not self.head_bar.view.isChecked()))
self.layout.addWidget(self.head_bar)
self.layout.addWidget(self.game_list)
# self.layout.addStretch(1)
self.head_bar.view.toggled.connect(
lambda: self.game_list.update_list(not self.head_bar.view.isChecked()))
self.setLayout(self.layout)
@ -60,24 +72,25 @@ class GameListHeadBar(QWidget):
self.installed_only = QCheckBox(self.tr("Installed only"))
self.layout.addWidget(self.installed_only)
self.layout.addStretch()
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.setMinimumWidth(200)
self.search_bar.setPlaceholderText(self.tr("Search Game"))
self.layout.addWidget(self.search_bar)
self.layout.addStretch()
self.list_view = QLabel(self.tr("List view"))
self.layout.addStretch(2)
self.icon_view = QLabel(self.tr("Icon view"))
checked = QSettings().value("icon_view", True, bool)
self.view = Switch()
checked = not QSettings().value("icon_view", True, bool)
self.view.setChecked(checked)
self.layout.addWidget(self.icon_view)
self.view = SelectViewWidget(checked)
self.layout.addWidget(self.view)
self.layout.addWidget(self.list_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)

View file

@ -0,0 +1,120 @@
import json
import os
import string
from logging import getLogger
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QPushButton, QVBoxLayout, QFileDialog, QMessageBox
from qtawesome import icon
from Rare.utils import LegendaryApi
from Rare.utils.QtExtensions import PathEdit
logger = getLogger("Import")
class ImportWidget(QWidget):
update_list = pyqtSignal()
def __init__(self, core):
super(ImportWidget, self).__init__()
self.core = core
self.main_layout = QHBoxLayout()
self.back_button = QPushButton(icon("mdi.keyboard-backspace", color="white"), self.tr("Back"))
self.right_layout = QVBoxLayout()
self.right_layout.addWidget(self.back_button)
self.right_layout.addStretch(1)
self.main_layout.addLayout(self.right_layout)
self.back_button.setFixedWidth(75)
self.layout = QVBoxLayout()
self.title = QLabel("<h2>Import Game</h2")
self.layout.addWidget(self.title)
self.import_one_game = QLabel("<h3>Import existing game</h3>")
self.layout.addWidget(self.import_one_game)
self.import_game_info = QLabel(self.tr("Select path to game"))
self.layout.addWidget(self.import_game_info)
self.path_edit = PathEdit(os.path.expanduser("~"), QFileDialog.DirectoryOnly)
self.layout.addWidget(self.path_edit)
self.import_button = QPushButton("Import Game")
self.layout.addWidget(self.import_button)
self.import_button.clicked.connect(self.import_game)
self.layout.addStretch(1)
self.auto_import = QLabel("<h3>Auto import all existing games</h3>")
self.layout.addWidget(self.auto_import)
self.auto_import_button = QPushButton(self.tr("Import all games from Epic Games Launcher"))
self.auto_import_button.clicked.connect(self.import_games_prepare)
self.layout.addWidget(self.auto_import_button)
self.layout.addStretch(1)
self.main_layout.addLayout(self.layout)
self.setLayout(self.main_layout)
def import_game(self, path=None):
if not path:
path = self.path_edit.text()
if not path.endswith("/"):
path = path + "/"
for i in os.listdir(os.path.join(path, ".egstore")):
if i.endswith(".mancpn"):
file = path + ".egstore/" + i
break
else:
logger.warning("File was not found")
return
app_name = json.load(open(file, "r"))["AppName"]
if LegendaryApi.import_game(self.core, app_name=app_name, path=path):
self.update_list.emit()
else:
logger.warning("Failed to import" + app_name)
return
def auto_import_games(self, game_path):
imported = 0
if not os.path.exists(game_path):
return 0
if os.listdir(game_path) == 0:
logger.info(f"No Games found in {game_path}")
return 0
for path in os.listdir(game_path):
json_path = game_path + path + "/.egstore"
print(json_path)
if not os.path.isdir(json_path):
logger.info(f"Game at {game_path + path} doesn't exist")
continue
for file in os.listdir(json_path):
if file.endswith(".mancpn"):
app_name = json.load(open(os.path.join(json_path, file)))["AppName"]
if LegendaryApi.import_game(self.core, app_name, game_path + path):
imported += 1
return imported
def import_games_prepare(self):
# Automatically import from windows
imported = 0
if os.name == "nt":
available_drives = ['%s:' % d for d in string.ascii_uppercase if os.path.exists('%s:' % d)]
for drive in available_drives:
path = f"{drive}/Program Files/Epic Games/"
if os.path.exists(path):
imported += self.auto_import_games(path)
else:
possible_wineprefixes = [os.path.expanduser("~/.wine/"), os.path.expanduser("~/Games/epic-games-store/")]
for wine_prefix in possible_wineprefixes:
imported += self.auto_import_games(f"{wine_prefix}drive_c/Program Files/Epic Games/")
if imported > 0:
QMessageBox.information(self, "Imported Games", self.tr(f"Successfully imported {imported} Games"))
self.update_list.emit()
logger.info("Restarting app to import games")
else:
QMessageBox.information(self, "Imported Games", "No Games were found")

View file

@ -30,7 +30,7 @@ class RareSettings(QWidget):
# 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))
self.save_path_button = QPushButton("Save")
self.save_path_button = QPushButton(self.tr("Save"))
self.save_path_button.clicked.connect(self.save_path)
self.img_dir = SettingsWidget(self.tr("Image Directory"), self.select_path, self.save_path_button)
self.layout.addWidget(self.img_dir)
@ -46,7 +46,7 @@ class RareSettings(QWidget):
self.select_lang.setCurrentIndex(0)
else:
self.select_lang.setCurrentIndex(0)
self.lang_widget = SettingsWidget("Language", self.select_lang)
self.lang_widget = SettingsWidget(self.tr("Language"), self.select_lang)
self.select_lang.currentIndexChanged.connect(self.update_lang)
self.layout.addWidget(self.lang_widget)

View file

@ -23,6 +23,8 @@ def main():
app = QApplication([])
app.setApplicationName("Rare")
app.setOrganizationName("Rare")
# app.setQuitOnLastWindowClosed(False)
settings = QSettings()
# Translator
translator = QTranslator()
@ -35,7 +37,7 @@ def main():
app.installTranslator(translator)
# Style
app.setStyleSheet(open(style_path + "RareStyle.qss").read())
app.setWindowIcon(QIcon(style_path+"Logo.png"))
app.setWindowIcon(QIcon(style_path + "Logo.png"))
launch_dialog = LaunchDialog(core)
launch_dialog.exec_()
mainwindow = MainWindow(core)
@ -45,3 +47,20 @@ 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)"""

Binary file not shown.

View file

@ -22,91 +22,160 @@
<context>
<name>DownloadTab</name>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="102"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="121"/>
<source>&lt;b&gt;WARNING&lt;/b&gt;: The progress bar is not implemented. It is normal, if there is no progress. The progress is visible in console, because Legendary prints output to console. A pull request is active to get output</source>
<translation>&lt;b&gt;Warnung&lt;/b&gt;: Die Anzeige ist noch nicht fertig. Es wird normal sein, wenn dort kein Fortschritt angezeigt wird. Dafür musst du in die Konsole schauen, da es in Legendary noch keine Möglichkeit gibt, die Ausgabe zu verarbeiten. Ein Pull Request ist eingereicht</translation>
</message>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="109"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="128"/>
<source>No active Download</source>
<translation>Keine aktiven Downloads</translation>
</message>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="120"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="139"/>
<source>No updates available</source>
<translation>Keine verfügbaren Updates</translation>
</message>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="140"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="203"/>
<source>Download size is 0. Game already exists</source>
<translation>Downloadgröße ist 0. Spiel exitstiert bereits</translation>
</message>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="158"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="221"/>
<source>Installation failed. See logs for more information</source>
<translation>Installation fehlgeschlagen. Siehe Log für mehr Informationen</translation>
</message>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="171"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="234"/>
<source>Installation finished</source>
<translation>Installation abgeschlossen</translation>
</message>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="172"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="235"/>
<source>Download of game </source>
<translation>Download des Spiels </translation>
</message>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="176"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="239"/>
<source>Installing Game: No active download</source>
<translation>Zu installierendes Spiel: Kein aktiver Download</translation>
</message>
</context>
<context>
<name>GameActions</name>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="151"/>
<source>Uninstall game</source>
<translation>Spiel deinstallieren</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="159"/>
<source>Verify Game</source>
<translation>Spieldateien überprüfen</translation>
</message>
</context>
<context>
<name>GameInfo</name>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="83"/>
<source>Are you sure to uninstall </source>
<translation>Möchstes du das Spiel deinstallieren </translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="92"/>
<source>Repair file does not exist or game does not need a repair. Please verify game first</source>
<translation>Es ist keine Reperatur nötig oder das Spiel wurde noch nicht verifiziert. Verifiziere das Spiel zu erst</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="109"/>
<source>Verification failed, {} file(s) corrupted, {} file(s) are missing. Do you want to repair them?</source>
<translation>Überprüfung fehlgeschlagen, {} Dateien sind falsch, {} Dateien fehlen. Willst du das Spiel reparieren?</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="137"/>
<source>Developer: </source>
<translation>Entwickler: </translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="138"/>
<source>Install size: </source>
<translation>Installationsgröße: </translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameInfo/GameInfo.py" line="140"/>
<source>Install path: </source>
<translation>Installations pfad: </translation>
</message>
</context>
<context>
<name>GameListHeadBar</name>
<message>
<location filename="../Components/Tabs/Games/GamesTab.py" line="52"/>
<location filename="../Components/Tabs/Games/GamesTab.py" line="60"/>
<source>Installed only</source>
<translation>Nur installierte</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GamesTab.py" line="58"/>
<location filename="../Components/Tabs/Games/GamesTab.py" line="67"/>
<source>Search Game</source>
<translation>Spiel suchen</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GamesTab.py" line="62"/>
<location filename="../Components/Tabs/Games/GamesTab.py" line="71"/>
<source>List view</source>
<translation>Listenansicht</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GamesTab.py" line="64"/>
<location filename="../Components/Tabs/Games/GamesTab.py" line="73"/>
<source>Icon view</source>
<translation>Iconansicht</translation>
</message>
</context>
<context>
<name>GameWidget</name>
<message>
<location filename="../Components/Tabs/Games/InstalledListWidget.py" line="59"/>
<source>Launch</source>
<translation>Starten</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/InstalledListWidget.py" line="66"/>
<source>Developer: </source>
<translation>Entwickler: </translation>
</message>
<message>
<location filename="../Components/Tabs/Games/InstalledListWidget.py" line="91"/>
<source>Game running</source>
<translation>Spiel läuft</translation>
</message>
</context>
<context>
<name>GameWidgetInstalled</name>
<message>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="35"/>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="36"/>
<source>Update available</source>
<translation>Update verfügbar</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="99"/>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="115"/>
<source>Game running</source>
<translation>Spiel läuft</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="108"/>
<source>Do you want to uninstall</source>
<translation>Möchtest du das Spiel deinstallieren: </translation>
<translation type="obsolete">Möchtest du das Spiel deinstallieren: </translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="92"/>
<source>Please update Game</source>
<translation>Bitte das Spiel updaten</translation>
</message>
</context>
<context>
<name>GameWidgetUninstalled</name>
<message>
<location filename="../Components/Tabs/Games/GameWidgetUninstalled.py" line="60"/>
<location filename="../Components/Tabs/Games/GameWidgetUninstalled.py" line="61"/>
<source>Install Game</source>
<translation>Spiel installieren</translation>
</message>
@ -176,12 +245,12 @@
<message>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="122"/>
<source>Game info</source>
<translation>Spielinformation</translation>
<translation type="obsolete">Spielinformation</translation>
</message>
<message>
<location filename="../Components/Tabs/Games/GameWidgetInstalled.py" line="123"/>
<source>Uninstall</source>
<translation>Deinstallieren</translation>
<translation type="obsolete">Deinstallieren</translation>
</message>
</context>
<context>
@ -210,12 +279,12 @@
<context>
<name>PathEdit</name>
<message>
<location filename="../utils/QtExtensions.py" line="131"/>
<location filename="../utils/QtExtensions.py" line="132"/>
<source>Select Path</source>
<translation>Wähle Pfad</translation>
</message>
<message>
<location filename="../utils/QtExtensions.py" line="144"/>
<location filename="../utils/QtExtensions.py" line="145"/>
<source>Choose Path</source>
<translation>Wähle Pfad</translation>
</message>
@ -245,6 +314,16 @@
<source>Restart Application to activate changes</source>
<translation>Starte die App neu um die Änderungen zu aktivieren</translation>
</message>
<message>
<location filename="../Components/Tabs/Settings/Rare.py" line="33"/>
<source>Save</source>
<translation>Speichern</translation>
</message>
<message>
<location filename="../Components/Tabs/Settings/Rare.py" line="49"/>
<source>Language</source>
<translation>Sprache</translation>
</message>
</context>
<context>
<name>SyncSaves</name>
@ -385,15 +464,23 @@
<context>
<name>TabWidget</name>
<message>
<location filename="../Components/TabWidget.py" line="21"/>
<location filename="../Components/TabWidget.py" line="24"/>
<source>Games</source>
<translation>Spiele</translation>
</message>
</context>
<context>
<name>UninstalledGameWidget</name>
<message>
<location filename="../Components/Tabs/Games/GameWidgetListUninstalled.py" line="42"/>
<source>Install</source>
<translation>Installieren</translation>
</message>
</context>
<context>
<name>UpdateWidget</name>
<message>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="196"/>
<location filename="../Components/Tabs/Downloads/DownloadTab.py" line="263"/>
<source>Update Game</source>
<translation>Spiel updaten</translation>
</message>

View file

@ -1,7 +1,13 @@
import os
import shutil
from logging import getLogger
from sys import stdout
from PyQt5.QtCore import QProcess, QProcessEnvironment
from PyQt5.QtCore import QProcess, QProcessEnvironment, QThread, pyqtSignal
from PyQt5.QtWidgets import QMessageBox, QWidget
from legendary.core import LegendaryCore
from legendary.models.game import VerifyResult
from legendary.utils.lfs import validate_files
logger = getLogger("Legendary Utils")
@ -54,6 +60,99 @@ def uninstall(app_name: str, core):
logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...')
core.uninstall_game(igame, delete_files=True, delete_root_directory=True)
logger.info('Game has been uninstalled.')
shutil.rmtree(igame.install_path)
except Exception as e:
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
class VerifyThread(QThread):
status = pyqtSignal(tuple)
summary = pyqtSignal(tuple)
def __init__(self, core, app_name):
super(VerifyThread, self).__init__()
self.core, self.app_name = core, app_name
def run(self):
if not self.core.is_installed(self.app_name):
logger.error(f'Game "{self.app_name}" is not installed')
return
logger.info(f'Loading installed manifest for "{self.app_name}"')
igame = self.core.get_installed_game(self.app_name)
manifest_data, _ = self.core.get_installed_manifest(self.app_name)
manifest = self.core.load_manifest(manifest_data)
files = sorted(manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower())
# build list of hashes
file_list = [(f.filename, f.sha_hash.hex()) for f in files]
total = len(file_list)
num = 0
failed = []
missing = []
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
repair_file = []
for result, path, result_hash in validate_files(igame.install_path, file_list):
self.status.emit((num, total))
num += 1
if result == VerifyResult.HASH_MATCH:
repair_file.append(f'{result_hash}:{path}')
continue
elif result == VerifyResult.HASH_MISMATCH:
logger.error(f'File does not match hash: "{path}"')
repair_file.append(f'{result_hash}:{path}')
failed.append(path)
elif result == VerifyResult.FILE_MISSING:
logger.error(f'File is missing: "{path}"')
missing.append(path)
else:
logger.error(f'Other failure (see log), treating file as missing: "{path}"')
missing.append(path)
stdout.write(f'Verification progress: {num}/{total} ({num * 100 / total:.01f}%)\t\n')
# always write repair file, even if all match
if repair_file:
repair_filename = os.path.join(self.core.lgd.get_tmp_path(), f'{self.app_name}.repair')
with open(repair_filename, 'w') as f:
f.write('\n'.join(repair_file))
logger.debug(f'Written repair file to "{repair_filename}"')
if not missing and not failed:
logger.info('Verification finished successfully.')
self.summary.emit((0,0))
else:
logger.error(f'Verification failed, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
self.summary.emit((len(failed), len(missing)))
def import_game(core: LegendaryCore, app_name: str, path: str):
logger.info("Import " + app_name)
game = core.get_game(app_name)
manifest, igame = core.import_game(game, path)
exe_path = os.path.join(path, manifest.meta.launch_exe.lstrip('/'))
total = len(manifest.file_manifest_list.elements)
found = sum(os.path.exists(os.path.join(path, f.filename))
for f in manifest.file_manifest_list.elements)
ratio = found / total
if not os.path.exists(exe_path):
logger.error(f"Game {game.app_title} failed to import")
return False
if ratio < 0.95:
logger.error(
"Game files are missing. It may be not the lates version ore it is corrupt")
return False
core.install_game(igame)
if igame.needs_verification:
logger.info(logger.info(
f'NOTE: The game installation will have to be verified before it can be updated '
f'with legendary. Run "legendary repair {app_name}" to do so.'))
logger.info("Successfully imported Game: " + game.app_title)
return True

View file

@ -1,10 +1,10 @@
import os
from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal, QPropertyAnimation, pyqtSlot, QPointF, QEasingCurve, \
QObject, pyqtProperty
from PyQt5.QtGui import QMovie, QPainter, QPalette, QLinearGradient, QGradient
from PyQt5.QtCore import Qt, QRect, QSize, QPoint, pyqtSignal
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, QHBoxLayout, QWidget, QLineEdit, \
QPushButton, QStyleOptionTab, QStylePainter, QTabBar, QAbstractButton, QCheckBox
QPushButton, QStyleOptionTab, QStylePainter, QTabBar
from qtawesome import icon
from Rare import style_path
@ -121,8 +121,9 @@ class ClickableLabel(QLabel):
class PathEdit(QWidget):
def __init__(self, text: str = "", type_of_file: QFileDialog.FileType = QFileDialog.AnyFile, infotext: str = "",
filter: str = None):
def __init__(self, text: str = "",
type_of_file: QFileDialog.FileType = QFileDialog.AnyFile,
infotext: str = "", filter: str = None):
super(PathEdit, self).__init__()
self.filter = filter
self.type_of_file = type_of_file
@ -182,6 +183,7 @@ class SideTabBar(QTabBar):
painter.drawControl(QStyle.CE_TabBarTabLabel, opt);
painter.restore()
class WaitingSpinner(QLabel):
def __init__(self):
super(WaitingSpinner, self).__init__()
@ -189,92 +191,47 @@ class WaitingSpinner(QLabel):
margin-left: auto;
margin-right: auto;
""")
self.movie = QMovie(style_path+"Icons/loader.gif")
self.movie = QMovie(style_path + "Icons/loader.gif")
self.setMovie(self.movie)
self.movie.start()
class SwitchPrivate(QObject):
def __init__(self, q, parent=None):
QObject.__init__(self, parent=parent)
self.mPointer = q
self.mPosition = 0.0
self.mGradient = QLinearGradient()
self.mGradient.setSpread(QGradient.PadSpread)
class SelectViewWidget(QWidget):
toggled = pyqtSignal()
self.animation = QPropertyAnimation(self)
self.animation.setTargetObject(self)
self.animation.setPropertyName(b'position')
self.animation.setStartValue(0.0)
self.animation.setEndValue(1.0)
self.animation.setDuration(200)
self.animation.setEasingCurve(QEasingCurve.InOutExpo)
def __init__(self, icon_view: bool):
super(SelectViewWidget, self).__init__()
self.icon_view = icon_view
self.setStyleSheet("""QPushButton{border: none}""")
self.icon_view_button = QPushButton()
self.list_view = QPushButton()
if icon_view:
self.icon_view_button.setIcon(icon("mdi.view-grid-outline", color="orange"))
self.list_view.setIcon(icon("fa5s.list", color="white"))
else:
self.icon_view_button.setIcon(icon("mdi.view-grid-outline", color="white"))
self.list_view.setIcon(icon("fa5s.list", color="orange"))
self.animation.finished.connect(self.mPointer.update)
self.icon_view_button.clicked.connect(self.icon)
self.list_view.clicked.connect(self.list)
@pyqtProperty(float)
def position(self):
return self.mPosition
self.layout = QHBoxLayout()
self.layout.addWidget(self.icon_view_button)
self.layout.addWidget(self.list_view)
@position.setter
def position(self, value):
self.mPosition = value
self.mPointer.update()
self.setLayout(self.layout)
def draw(self, painter):
r = self.mPointer.rect()
margin = r.height()/10
shadow = self.mPointer.palette().color(QPalette.Dark)
light = self.mPointer.palette().color(QPalette.Light)
button = self.mPointer.palette().color(QPalette.Button)
painter.setPen(Qt.NoPen)
def isChecked(self):
return self.icon_view
#self.mGradient.setColorAt(0, shadow.darker(130))
#self.mGradient.setColorAt(1, light.darker(130))
#self.mGradient.setStart(0, r.height())
#self.mGradient.setFinalStop(0, 0)
painter.setBrush(self.mGradient)
painter.drawRoundedRect(r, r.height()/2, r.height()/2)
def icon(self):
self.icon_view_button.setIcon(icon("mdi.view-grid-outline", color="orange"))
self.list_view.setIcon(icon("fa5s.list", color="white"))
self.icon_view = False
self.toggled.emit()
self.mGradient.setColorAt(0, shadow.darker(140))
self.mGradient.setColorAt(1, light.darker(160))
self.mGradient.setStart(0, 0)
self.mGradient.setFinalStop(0, r.height())
painter.setBrush(self.mGradient)
painter.drawRoundedRect(r.adjusted(margin, margin, -margin, -margin), r.height()/2, r.height()/2)
self.mGradient.setColorAt(0, button.darker(130))
self.mGradient.setColorAt(1, button)
painter.setBrush(self.mGradient)
x = r.height()/2.0 + self.mPosition*(r.width()-r.height())
painter.drawEllipse(QPointF(x, r.height()/2), r.height()/2-margin, r.height()/2-margin)
@pyqtSlot(bool, name='animate')
def animate(self, checked):
self.animation.setDirection(QPropertyAnimation.Forward if checked else QPropertyAnimation.Backward)
self.animation.start()
class Switch(QAbstractButton):
def __init__(self):
QAbstractButton.__init__(self)
self.dPtr = SwitchPrivate(self)
self.setCheckable(True)
self.clicked.connect(self.dPtr.animate)
def sizeHint(self):
return QSize(42, 21)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
self.dPtr.draw(painter)
def setChecked(self, a0: bool) -> None:
super().setChecked(a0)
self.dPtr.animate(a0)
def resizeEvent(self, event):
self.update()
def list(self):
self.icon_view_button.setIcon(icon("mdi.view-grid-outline", color="white"))
self.list_view.setIcon(icon("fa5s.list", color="orange"))
self.icon_view = True
self.toggled.emit()