diff --git a/.idea/misc.xml b/.idea/misc.xml
index 1260ee9e..d56657ad 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/Rare/Components/MainWindow.py b/Rare/Components/MainWindow.py
index 188856b0..a0116212 100644
--- a/Rare/Components/MainWindow.py
+++ b/Rare/Components/MainWindow.py
@@ -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()
diff --git a/Rare/Components/TabWidget.py b/Rare/Components/TabWidget.py
index 2d87f4f5..3a230305 100644
--- a/Rare/Components/TabWidget.py
+++ b/Rare/Components/TabWidget.py
@@ -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")
diff --git a/Rare/Components/Tabs/Downloads/DownloadTab.py b/Rare/Components/Tabs/Downloads/DownloadTab.py
index 540154ac..95a7215e 100644
--- a/Rare/Components/Tabs/Downloads/DownloadTab.py
+++ b/Rare/Components/Tabs/Downloads/DownloadTab.py
@@ -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
diff --git a/Rare/Components/Tabs/Games/GameInfo/GameInfo.py b/Rare/Components/Tabs/Games/GameInfo/GameInfo.py
index 6da5d7bb..1153148b 100644
--- a/Rare/Components/Tabs/Games/GameInfo/GameInfo.py
+++ b/Rare/Components/Tabs/Games/GameInfo/GameInfo.py
@@ -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("
Game actions
")
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)
\ No newline at end of file
+
+ 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)
diff --git a/Rare/Components/Tabs/Games/GameList.py b/Rare/Components/Tabs/Games/GameList.py
index d12b60ee..470c0e7a 100644
--- a/Rare/Components/Tabs/Games/GameList.py
+++ b/Rare/Components/Tabs/Games/GameList.py
@@ -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
\ No newline at end of file
diff --git a/Rare/Components/Tabs/Games/GameWidgetInstalled.py b/Rare/Components/Tabs/Games/GameWidgetInstalled.py
index e53188d7..52d405e4 100644
--- a/Rare/Components/Tabs/Games/GameWidgetInstalled.py
+++ b/Rare/Components/Tabs/Games/GameWidgetInstalled.py
@@ -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"{game.title}
")
+ 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()
diff --git a/Rare/Components/Tabs/Games/GamesTab.py b/Rare/Components/Tabs/Games/GamesTab.py
index 309581a0..6ddc5492 100644
--- a/Rare/Components/Tabs/Games/GamesTab.py
+++ b/Rare/Components/Tabs/Games/GamesTab.py
@@ -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)
diff --git a/Rare/Components/Tabs/Games/ImportWidget.py b/Rare/Components/Tabs/Games/ImportWidget.py
new file mode 100644
index 00000000..975198a5
--- /dev/null
+++ b/Rare/Components/Tabs/Games/ImportWidget.py
@@ -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("Import Game
Import existing game")
+ 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("Auto import all existing games
")
+ 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")
diff --git a/Rare/Components/Tabs/Settings/Rare.py b/Rare/Components/Tabs/Settings/Rare.py
index 249eb63d..d41a665d 100644
--- a/Rare/Components/Tabs/Settings/Rare.py
+++ b/Rare/Components/Tabs/Settings/Rare.py
@@ -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)
diff --git a/Rare/Main.py b/Rare/Main.py
index 6e4b6999..8d0ca194 100644
--- a/Rare/Main.py
+++ b/Rare/Main.py
@@ -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)"""
diff --git a/Rare/languages/de.qm b/Rare/languages/de.qm
index 351fe124..7977c30b 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 129175a8..2c4f9dd5 100644
--- a/Rare/languages/de.ts
+++ b/Rare/languages/de.ts
@@ -22,91 +22,160 @@
DownloadTab
-
+
<b>Warnung</b>: 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
-
+
Keine aktiven Downloads
-
+
Keine verfügbaren Updates
-
+
Downloadgröße ist 0. Spiel exitstiert bereits
-
+
Installation fehlgeschlagen. Siehe Log für mehr Informationen
-
+
Installation abgeschlossen
-
+
Download des Spiels
-
+
Zu installierendes Spiel: Kein aktiver Download
+
+ GameActions
+
+
+
+ Spiel deinstallieren
+
+
+
+
+ Spieldateien überprüfen
+
+
+
+ GameInfo
+
+
+
+ Möchstes du das Spiel deinstallieren
+
+
+
+
+ Es ist keine Reperatur nötig oder das Spiel wurde noch nicht verifiziert. Verifiziere das Spiel zu erst
+
+
+
+
+ Überprüfung fehlgeschlagen, {} Dateien sind falsch, {} Dateien fehlen. Willst du das Spiel reparieren?
+
+
+
+
+ Entwickler:
+
+
+
+
+ Installationsgröße:
+
+
+
+
+ Installations pfad:
+
+
GameListHeadBar
-
+
Nur installierte
-
+
Spiel suchen
-
+
Listenansicht
-
+
Iconansicht
+
+ GameWidget
+
+
+
+ Starten
+
+
+
+
+ Entwickler:
+
+
+
+
+ Spiel läuft
+
+
GameWidgetInstalled
-
+
Update verfügbar
-
+
Spiel läuft
- Möchtest du das Spiel deinstallieren:
+ Möchtest du das Spiel deinstallieren:
+
+
+
+
+ Bitte das Spiel updaten
GameWidgetUninstalled
-
+
Spiel installieren
@@ -176,12 +245,12 @@
- Spielinformation
+ Spielinformation
- Deinstallieren
+ Deinstallieren
@@ -210,12 +279,12 @@
PathEdit
-
+
Wähle Pfad
-
+
Wähle Pfad
@@ -245,6 +314,16 @@
Starte die App neu um die Änderungen zu aktivieren
+
+
+
+ Speichern
+
+
+
+
+ Sprache
+
SyncSaves
@@ -385,15 +464,23 @@
TabWidget
-
+
Spiele
+
+ UninstalledGameWidget
+
+
+
+ Installieren
+
+
UpdateWidget
-
+
Spiel updaten
diff --git a/Rare/utils/LegendaryApi.py b/Rare/utils/LegendaryApi.py
index 773ea6ad..f6381d43 100644
--- a/Rare/utils/LegendaryApi.py
+++ b/Rare/utils/LegendaryApi.py
@@ -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
diff --git a/Rare/utils/QtExtensions.py b/Rare/utils/QtExtensions.py
index bb4d53ba..90a9485d 100644
--- a/Rare/utils/QtExtensions.py
+++ b/Rare/utils/QtExtensions.py
@@ -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()