1
0
Fork 0
mirror of synced 2024-05-19 12:02:54 +12:00

Rare: pass through Black formatter

This commit is contained in:
Stelios Tsampas 2021-12-24 11:09:50 +02:00 committed by Dummerle
parent 0d2f36e028
commit 8f89eb6e88
No known key found for this signature in database
GPG key ID: AB68CC59CA39F2F1
60 changed files with 2128 additions and 957 deletions

View file

@ -10,6 +10,7 @@ def main():
# fix cx_freeze
import multiprocessing
multiprocessing.freeze_support()
# insert legendary for installed via pip/setup.py submodule to path
@ -18,39 +19,61 @@ def main():
# CLI Options
parser = ArgumentParser()
parser.add_argument("-V", "--version", action="store_true", help="Shows version and exits")
parser.add_argument("-S", "--silent", action="store_true",
help="Launch Rare in background. Open it from System Tray Icon")
parser.add_argument(
"-V", "--version", action="store_true", help="Shows version and exits"
)
parser.add_argument(
"-S",
"--silent",
action="store_true",
help="Launch Rare in background. Open it from System Tray Icon",
)
parser.add_argument("--debug", action="store_true", help="Launch in debug mode")
parser.add_argument("--offline", action="store_true", help="Launch Rare in offline mode")
parser.add_argument("--test-start", action="store_true", help="Quit immediately after launch")
parser.add_argument(
"--offline", action="store_true", help="Launch Rare in offline mode"
)
parser.add_argument(
"--test-start", action="store_true", help="Quit immediately after launch"
)
parser.add_argument("--desktop-shortcut", action="store_true", dest="desktop_shortcut",
help="Use this, if there is no link on desktop to start Rare")
parser.add_argument("--startmenu-shortcut", action="store_true", dest="startmenu_shortcut",
help="Use this, if there is no link in start menu to launch Rare")
parser.add_argument(
"--desktop-shortcut",
action="store_true",
dest="desktop_shortcut",
help="Use this, if there is no link on desktop to start Rare",
)
parser.add_argument(
"--startmenu-shortcut",
action="store_true",
dest="startmenu_shortcut",
help="Use this, if there is no link in start menu to launch Rare",
)
subparsers = parser.add_subparsers(title="Commands", dest="subparser")
launch_parser = subparsers.add_parser("launch")
launch_parser.add_argument('app_name', help='Name of the app', metavar='<App Name>')
launch_parser.add_argument("app_name", help="Name of the app", metavar="<App Name>")
args = parser.parse_args()
if args.desktop_shortcut:
from rare.utils import utils
utils.create_rare_desktop_link("desktop")
print("Link created")
if args.startmenu_shortcut:
from rare.utils import utils
utils.create_rare_desktop_link("start_menu")
print("link created")
if args.version:
from rare import __version__, code_name
print(f"Rare {__version__} Codename: {code_name}")
return
from rare.utils import singleton
try:
# this object only allows one instance per machine
@ -58,6 +81,7 @@ def main():
except singleton.SingleInstanceException:
print("Rare is already running")
from rare import data_dir
with open(os.path.join(data_dir, "lockfile"), "w") as file:
if args.subparser == "launch":
file.write("launch " + args.app_name)
@ -70,13 +94,16 @@ def main():
args.silent = True
from rare.app import start
start(args)
if __name__ == '__main__':
if __name__ == "__main__":
# run from source
# insert raw legendary submodule
sys.path.insert(0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary"))
sys.path.insert(
0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")
)
# insert source directory
sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))

View file

@ -12,6 +12,7 @@ from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMessageBox
from requests import HTTPError
import legendary
# noinspection PyUnresolvedReferences
import rare.resources.resources
import rare.shared as shared
@ -21,7 +22,7 @@ from rare.components.main_window import MainWindow
from rare.components.tray_icon import TrayIcon
from rare.utils.utils import set_color_pallete, set_style_sheet
start_time = time.strftime('%y-%m-%d--%H-%M') # year-month-day-hour-minute
start_time = time.strftime("%y-%m-%d--%H-%M") # year-month-day-hour-minute
file_name = os.path.join(cache_dir, "logs", f"Rare_{start_time}.log")
if not os.path.exists(os.path.dirname(file_name)):
os.makedirs(os.path.dirname(file_name))
@ -63,10 +64,10 @@ class App(QApplication):
self.core = shared.init_legendary()
except configparser.MissingSectionHeaderError as e:
logger.warning(f"Config is corrupt: {e}")
if config_path := os.environ.get('XDG_CONFIG_HOME'):
path = os.path.join(config_path, 'legendary')
if config_path := os.environ.get("XDG_CONFIG_HOME"):
path = os.path.join(config_path, "legendary")
else:
path = os.path.expanduser('~/.config/legendary')
path = os.path.expanduser("~/.config/legendary")
with open(os.path.join(path, "config.ini"), "w") as config_file:
config_file.write("[Legendary]")
self.core = shared.init_legendary()
@ -95,12 +96,15 @@ class App(QApplication):
self.signals.exit_app.connect(self.exit_app)
self.signals.send_notification.connect(
lambda title:
self.tray_icon.showMessage(
lambda title: self.tray_icon.showMessage(
self.tr("Download finished"),
self.tr("Download finished. {} is playable now").format(title),
QSystemTrayIcon.Information, 4000)
if self.settings.value("notification", True, bool) else None)
QSystemTrayIcon.Information,
4000,
)
if self.settings.value("notification", True, bool)
else None
)
# Translator
self.translator = QTranslator()
@ -122,9 +126,12 @@ class App(QApplication):
# Style
# lk: this is a bit silly but serves well until we have a class
# lk: store the default qt style name from the system on startup as a property for later reference
self.setProperty('rareDefaultQtStyle', self.style().objectName())
self.setProperty("rareDefaultQtStyle", self.style().objectName())
if self.settings.value("color_scheme", None) is None and self.settings.value("style_sheet", None) is None:
if (
self.settings.value("color_scheme", None) is None
and self.settings.value("style_sheet", None) is None
):
self.settings.setValue("color_scheme", "")
self.settings.setValue("style_sheet", "RareStyle")
@ -157,23 +164,38 @@ class App(QApplication):
self.tray_icon = TrayIcon(self)
self.tray_icon.exit_action.triggered.connect(self.exit_app)
self.tray_icon.start_rare.triggered.connect(self.show_mainwindow)
self.tray_icon.activated.connect(lambda r: self.show_mainwindow() if r == QSystemTrayIcon.DoubleClick else None)
self.tray_icon.activated.connect(
lambda r: self.show_mainwindow()
if r == QSystemTrayIcon.DoubleClick
else None
)
if not self.args.silent:
self.mainwindow.show_window_centralized()
self.window_launched = True
if shared.args.subparser == "launch":
if shared.args.app_name in [i.app_name for i in self.core.get_installed_list()]:
logger.info("Launching " + self.core.get_installed_game(shared.args.app_name).title)
self.mainwindow.tab_widget.games_tab.game_utils.prepare_launch(shared.args.app_name)
if shared.args.app_name in [
i.app_name for i in self.core.get_installed_list()
]:
logger.info(
"Launching "
+ self.core.get_installed_game(shared.args.app_name).title
)
self.mainwindow.tab_widget.games_tab.game_utils.prepare_launch(
shared.args.app_name
)
else:
logger.error(
f"Could not find {shared.args.app_name} in Games or it is not installed")
QMessageBox.warning(self.mainwindow, "Warning",
self.tr(
"Could not find {} in installed games. Did you modify the shortcut? ").format(
shared.args.app_name))
f"Could not find {shared.args.app_name} in Games or it is not installed"
)
QMessageBox.warning(
self.mainwindow,
"Warning",
self.tr(
"Could not find {} in installed games. Did you modify the shortcut? "
).format(shared.args.app_name),
)
if shared.args.test_start:
self.exit_app(0)
@ -190,8 +212,12 @@ class App(QApplication):
question = QMessageBox.question(
self.mainwindow,
self.tr("Close"),
self.tr("There is a download active. Do you really want to exit app?"),
QMessageBox.Yes, QMessageBox.No)
self.tr(
"There is a download active. Do you really want to exit app?"
),
QMessageBox.Yes,
QMessageBox.No,
)
if question == QMessageBox.No:
return
else:
@ -218,19 +244,23 @@ def start(args):
# configure logging
if args.debug:
logging.basicConfig(format='[%(name)s] %(levelname)s: %(message)s', level=logging.DEBUG)
logging.basicConfig(
format="[%(name)s] %(levelname)s: %(message)s", level=logging.DEBUG
)
logging.getLogger().setLevel(level=logging.DEBUG)
# keep requests, asyncio and pillow quiet
logging.getLogger('requests').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.WARNING)
logger.info(f"Launching Rare version {rare.__version__} Codename: {rare.code_name}\n"
f"Using Legendary {legendary.__version__} Codename: {legendary.__codename__} as backend\n"
f"Operating System: {platform.system()}, Python version: {platform.python_version()}\n"
f"Running {sys.executable} {' '.join(sys.argv)}")
logger.info(
f"Launching Rare version {rare.__version__} Codename: {rare.code_name}\n"
f"Using Legendary {legendary.__version__} Codename: {legendary.__codename__} as backend\n"
f"Operating System: {platform.system()}, Python version: {platform.python_version()}\n"
f"Running {sys.executable} {' '.join(sys.argv)}"
)
else:
logging.basicConfig(
format='[%(name)s] %(levelname)s: %(message)s',
format="[%(name)s] %(levelname)s: %(message)s",
level=logging.INFO,
filename=file_name,
)

View file

@ -18,7 +18,9 @@ from rare.utils.utils import get_size
class InstallDialog(QDialog, Ui_InstallDialog):
result_ready = pyqtSignal(InstallQueueItemModel)
def __init__(self, dl_item: InstallQueueItemModel, update=False, silent=False, parent=None):
def __init__(
self, dl_item: InstallQueueItemModel, update=False, silent=False, parent=None
):
super(InstallDialog, self).__init__(parent)
self.setupUi(self)
self.setAttribute(Qt.WA_DeleteOnClose, True)
@ -40,17 +42,19 @@ class InstallDialog(QDialog, Ui_InstallDialog):
self.threadpool.setMaxThreadCount(1)
header = self.tr("Update") if update else self.tr("Install")
self.install_dialog_label.setText(f"<h3>{header} \"{self.game.app_title}\"</h3>")
self.setWindowTitle(f"{self.windowTitle()} - {header} \"{self.game.app_title}\"")
self.install_dialog_label.setText(f'<h3>{header} "{self.game.app_title}"</h3>')
self.setWindowTitle(f'{self.windowTitle()} - {header} "{self.game.app_title}"')
default_path = os.path.expanduser("~/legendary")
if self.core.lgd.config.has_option("Legendary", "install_dir"):
default_path = self.core.lgd.config.get("Legendary", "install_dir")
self.install_dir_edit = PathEdit(path=default_path,
file_type=QFileDialog.DirectoryOnly,
edit_func=self.option_changed,
parent=self)
self.install_dir_edit = PathEdit(
path=default_path,
file_type=QFileDialog.DirectoryOnly,
edit_func=self.option_changed,
parent=self,
)
self.install_dir_layout.addWidget(self.install_dir_edit)
if self.update:
@ -66,10 +70,23 @@ class InstallDialog(QDialog, Ui_InstallDialog):
if dl_item.options.app_name in shared.api_results.mac_games:
platforms.append("Mac")
self.platform_combo_box.addItems(platforms)
self.platform_combo_box.currentIndexChanged.connect(lambda: self.option_changed(None))
self.platform_combo_box.currentIndexChanged.connect(lambda i: QMessageBox.warning(self, "Warning", self.tr(
"You will not be able to run the Game if you choose {}").format(self.platform_combo_box.itemText(i)))
if (self.platform_combo_box.currentText() == "Mac" and platform.system() != "Darwin") else None)
self.platform_combo_box.currentIndexChanged.connect(
lambda: self.option_changed(None)
)
self.platform_combo_box.currentIndexChanged.connect(
lambda i: QMessageBox.warning(
self,
"Warning",
self.tr("You will not be able to run the Game if you choose {}").format(
self.platform_combo_box.itemText(i)
),
)
if (
self.platform_combo_box.currentText() == "Mac"
and platform.system() != "Darwin"
)
else None
)
if platform.system() == "Darwin" and "Mac" in platforms:
self.platform_combo_box.setCurrentIndex(platforms.index("Mac"))
@ -83,14 +100,16 @@ class InstallDialog(QDialog, Ui_InstallDialog):
self.force_download_check.stateChanged.connect(self.option_changed)
self.ignore_space_check.stateChanged.connect(self.option_changed)
self.download_only_check.stateChanged.connect(lambda: self.non_reload_option_changed("download_only"))
self.download_only_check.stateChanged.connect(
lambda: self.non_reload_option_changed("download_only")
)
self.sdl_list_checks = list()
try:
for key, info in games[self.app_name].items():
cb = QDataCheckBox(info['name'], info['tags'])
if key == '__required':
self.dl_item.options.sdl_list.extend(info['tags'])
cb = QDataCheckBox(info["name"], info["tags"])
if key == "__required":
self.dl_item.options.sdl_list.extend(info["tags"])
cb.setChecked(True)
cb.setDisabled(True)
self.sdl_list_layout.addWidget(cb)
@ -120,13 +139,15 @@ class InstallDialog(QDialog, Ui_InstallDialog):
self.show()
def get_options(self):
self.dl_item.options.base_path = self.install_dir_edit.text() if not self.update else None
self.dl_item.options.base_path = (
self.install_dir_edit.text() if not self.update else None
)
self.dl_item.options.max_workers = self.max_workers_spin.value()
self.dl_item.options.force = self.force_download_check.isChecked()
self.dl_item.options.ignore_space_req = self.ignore_space_check.isChecked()
self.dl_item.options.no_install = self.download_only_check.isChecked()
self.dl_item.options.platform = self.platform_combo_box.currentText()
self.dl_item.options.sdl_list = ['']
self.dl_item.options.sdl_list = [""]
for cb in self.sdl_list_checks:
if data := cb.isChecked():
# noinspection PyTypeChecker
@ -145,9 +166,13 @@ class InstallDialog(QDialog, Ui_InstallDialog):
def verify_clicked(self):
message = self.tr("Updating...")
self.download_size_info_label.setText(message)
self.download_size_info_label.setStyleSheet("font-style: italic; font-weight: normal")
self.download_size_info_label.setStyleSheet(
"font-style: italic; font-weight: normal"
)
self.install_size_info_label.setText(message)
self.install_size_info_label.setStyleSheet("font-style: italic; font-weight: normal")
self.install_size_info_label.setStyleSheet(
"font-style: italic; font-weight: normal"
)
self.cancel_button.setEnabled(False)
self.verify_button.setEnabled(False)
self.install_button.setEnabled(False)
@ -180,13 +205,19 @@ class InstallDialog(QDialog, Ui_InstallDialog):
install_size = self.dl_item.download.analysis.install_size
if download_size:
self.download_size_info_label.setText("{}".format(get_size(download_size)))
self.download_size_info_label.setStyleSheet("font-style: normal; font-weight: bold")
self.download_size_info_label.setStyleSheet(
"font-style: normal; font-weight: bold"
)
self.install_button.setEnabled(not self.options_changed)
else:
self.install_size_info_label.setText(self.tr("Game already installed"))
self.install_size_info_label.setStyleSheet("font-style: italics; font-weight: normal")
self.install_size_info_label.setStyleSheet(
"font-style: italics; font-weight: normal"
)
self.install_size_info_label.setText("{}".format(get_size(install_size)))
self.install_size_info_label.setStyleSheet("font-style: normal; font-weight: bold")
self.install_size_info_label.setStyleSheet(
"font-style: normal; font-weight: bold"
)
self.verify_button.setEnabled(self.options_changed)
self.cancel_button.setEnabled(True)
if self.silent:
@ -225,7 +256,6 @@ class InstallInfoWorkerSignals(QObject):
class InstallInfoWorker(QRunnable):
def __init__(self, core: LegendaryCore, dl_item: InstallQueueItemModel):
super(InstallInfoWorker, self).__init__()
self.signals = InstallInfoWorkerSignals()
@ -235,44 +265,47 @@ class InstallInfoWorker(QRunnable):
@pyqtSlot()
def run(self):
try:
download = InstallDownloadModel(*self.core.prepare_download(
app_name=self.dl_item.options.app_name,
base_path=self.dl_item.options.base_path,
force=self.dl_item.options.force,
no_install=self.dl_item.options.no_install,
status_q=self.dl_item.status_q,
# max_shm=,
max_workers=self.dl_item.options.max_workers,
# game_folder=,
# disable_patching=,
# override_manifest=,
# override_old_manifest=,
# override_base_url=,
platform=self.dl_item.options.platform,
# file_prefix_filter=,
# file_exclude_filter=,
# file_install_tag=,
# dl_optimizations=,
# dl_timeout=,
repair=self.dl_item.options.repair,
# repair_use_latest=,
ignore_space_req=self.dl_item.options.ignore_space_req,
# disable_delta=,
# override_delta_manifest=,
# reset_sdl=,
sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list
))
download = InstallDownloadModel(
*self.core.prepare_download(
app_name=self.dl_item.options.app_name,
base_path=self.dl_item.options.base_path,
force=self.dl_item.options.force,
no_install=self.dl_item.options.no_install,
status_q=self.dl_item.status_q,
# max_shm=,
max_workers=self.dl_item.options.max_workers,
# game_folder=,
# disable_patching=,
# override_manifest=,
# override_old_manifest=,
# override_base_url=,
platform=self.dl_item.options.platform,
# file_prefix_filter=,
# file_exclude_filter=,
# file_install_tag=,
# dl_optimizations=,
# dl_timeout=,
repair=self.dl_item.options.repair,
# repair_use_latest=,
ignore_space_req=self.dl_item.options.ignore_space_req,
# disable_delta=,
# override_delta_manifest=,
# reset_sdl=,
sdl_prompt=lambda app_name, title: self.dl_item.options.sdl_list,
)
)
if not download.res.failures:
self.signals.result.emit(download)
else:
self.signals.failed.emit("\n".join(str(i) for i in download.res.failures))
self.signals.failed.emit(
"\n".join(str(i) for i in download.res.failures)
)
except Exception as e:
self.signals.failed.emit(str(e))
self.signals.finished.emit()
class QDataCheckBox(QCheckBox):
def __init__(self, text, data=None, parent=None):
super(QDataCheckBox, self).__init__(parent)
self.setText(text)

View file

@ -75,8 +75,8 @@ class AssetWorker(QRunnable):
if not shared.core.egs.user:
return []
assets = [
GameAsset.from_egs_json(a) for a in
shared.core.egs.get_game_assets(platform=p)
GameAsset.from_egs_json(a)
for a in shared.core.egs.get_game_assets(platform=p)
]
return assets
@ -144,15 +144,24 @@ class LaunchDialog(QDialog, Ui_LaunchDialog):
# cloud save from another worker, because it is used in cloud_save_utils too
cloud_worker = CloudWorker()
cloud_worker.signals.result_ready.connect(lambda x: self.handle_api_worker_result(x, "saves"))
cloud_worker.signals.result_ready.connect(
lambda x: self.handle_api_worker_result(x, "saves")
)
self.thread_pool.start(cloud_worker)
else:
self.finished = 2
if self.core.lgd.assets:
self.api_results.game_list, self.api_results.dlcs = self.core.get_game_and_dlc_list(False)
self.api_results.bit32_games = list(map(lambda i: i.app_name, self.core.get_game_list(False, "Win32")))
self.api_results.mac_games = list(map(lambda i: i.app_name, self.core.get_game_list(False, "Mac")))
(
self.api_results.game_list,
self.api_results.dlcs,
) = self.core.get_game_and_dlc_list(False)
self.api_results.bit32_games = list(
map(lambda i: i.app_name, self.core.get_game_list(False, "Win32"))
)
self.api_results.mac_games = list(
map(lambda i: i.app_name, self.core.get_game_list(False, "Mac"))
)
else:
logger.warning("No assets found. Falling back to empty game lists")
self.api_results.game_list, self.api_results.dlcs = [], {}
@ -165,11 +174,18 @@ class LaunchDialog(QDialog, Ui_LaunchDialog):
if result:
self.api_results.game_list, self.api_results.dlcs = result
else:
self.api_results.game_list, self.api_results.dlcs = self.core.get_game_and_dlc_list(False)
(
self.api_results.game_list,
self.api_results.dlcs,
) = self.core.get_game_and_dlc_list(False)
elif text == "32bit":
self.api_results.bit32_games = [i.app_name for i in result[0]] if result else []
self.api_results.bit32_games = (
[i.app_name for i in result[0]] if result else []
)
elif text == "mac":
self.api_results.mac_games = [i.app_name for i in result[0]] if result else []
self.api_results.mac_games = (
[i.app_name for i in result[0]] if result else []
)
elif text == "no_assets":
self.api_results.no_asset_games = result if result else []

View file

@ -48,8 +48,12 @@ class LoginDialog(QDialog, Ui_LoginDialog):
self.next_button.setEnabled(False)
self.back_button.setEnabled(False)
self.login_browser_radio.clicked.connect(lambda: self.next_button.setEnabled(True))
self.login_import_radio.clicked.connect(lambda: self.next_button.setEnabled(True))
self.login_browser_radio.clicked.connect(
lambda: self.next_button.setEnabled(True)
)
self.login_import_radio.clicked.connect(
lambda: self.next_button.setEnabled(True)
)
self.exit_button.clicked.connect(self.close)
self.back_button.clicked.connect(self.back_clicked)
self.next_button.clicked.connect(self.next_clicked)
@ -97,4 +101,3 @@ class LoginDialog(QDialog, Ui_LoginDialog):
self.next_button.setEnabled(False)
self.logged_in = False
QMessageBox.warning(self, "Error", str(e))

View file

@ -25,9 +25,7 @@ class BrowserLogin(QWidget, Ui_BrowserLogin):
self.core = core
self.sid_edit = IndicatorLineEdit(
ph_text=self.tr("Insert SID here"),
edit_func=self.text_changed,
parent=self
ph_text=self.tr("Insert SID here"), edit_func=self.text_changed, parent=self
)
self.sid_layout.addWidget(self.sid_edit)
@ -58,7 +56,9 @@ class BrowserLogin(QWidget, Ui_BrowserLogin):
try:
token = self.core.auth_sid(sid)
if self.core.auth_code(token):
logger.info(f"Successfully logged in as {self.core.lgd.userdata['displayName']}")
logger.info(
f"Successfully logged in as {self.core.lgd.userdata['displayName']}"
)
self.success.emit()
else:
self.status_label.setText(self.tr("Login failed."))

View file

@ -17,7 +17,9 @@ class ImportLogin(QWidget, Ui_ImportLogin):
if os.name == "nt":
localappdata = os.path.expandvars("%LOCALAPPDATA%")
else:
localappdata = os.path.join("drive_c/users", getuser(), "Local Settings/Application Data")
localappdata = os.path.join(
"drive_c/users", getuser(), "Local Settings/Application Data"
)
appdata_path = os.path.join(localappdata, "EpicGamesLauncher/Saved/Config/Windows")
found = False
@ -27,7 +29,9 @@ class ImportLogin(QWidget, Ui_ImportLogin):
self.core = core
self.text_egl_found = self.tr("Found EGL Program Data. Click 'Next' to import them.")
self.text_egl_found = self.tr(
"Found EGL Program Data. Click 'Next' to import them."
)
self.text_egl_notfound = self.tr("Could not find EGL Program Data. ")
if os.name == "nt":
@ -39,14 +43,19 @@ class ImportLogin(QWidget, Ui_ImportLogin):
self.status_label.setText(self.text_egl_found)
self.found = True
else:
self.info_label.setText(self.tr(
"Please select the Wine prefix"
" where Epic Games Launcher is installed. ") + self.info_label.text()
)
self.info_label.setText(
self.tr(
"Please select the Wine prefix"
" where Epic Games Launcher is installed. "
)
+ self.info_label.text()
)
prefixes = self.get_wine_prefixes()
if len(prefixes):
self.prefix_combo.addItems(prefixes)
self.status_label.setText(self.tr("Select the Wine prefix you want to import."))
self.status_label.setText(
self.tr("Select the Wine prefix you want to import.")
)
else:
self.status_label.setText(self.text_egl_notfound)
@ -65,7 +74,9 @@ class ImportLogin(QWidget, Ui_ImportLogin):
return prefixes
def prefix_path(self):
prefix_dialog = QFileDialog(self, self.tr("Choose path"), os.path.expanduser("~/"))
prefix_dialog = QFileDialog(
self, self.tr("Choose path"), os.path.expanduser("~/")
)
prefix_dialog.setFileMode(QFileDialog.DirectoryOnly)
if prefix_dialog.exec_():
names = prefix_dialog.selectedFiles()
@ -75,14 +86,18 @@ class ImportLogin(QWidget, Ui_ImportLogin):
if os.name == "nt":
return self.found
else:
return os.path.exists(os.path.join(self.prefix_combo.currentText(), self.appdata_path))
return os.path.exists(
os.path.join(self.prefix_combo.currentText(), self.appdata_path)
)
def do_login(self):
self.status_label.setText(self.tr("Loading..."))
if os.name == "nt":
pass
else:
self.core.egl.appdata_path = os.path.join(self.prefix_combo.currentText(), self.appdata_path)
self.core.egl.appdata_path = os.path.join(
self.prefix_combo.currentText(), self.appdata_path
)
try:
if self.core.auth_import():
logger.info(f"Logged in as {self.core.lgd.userdata['displayName']}")

View file

@ -1,5 +1,12 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QHBoxLayout, QPushButton, QVBoxLayout, QLabel, QDialog, QFileDialog
from PyQt5.QtWidgets import (
QHBoxLayout,
QPushButton,
QVBoxLayout,
QLabel,
QDialog,
QFileDialog,
)
from rare.utils.extra_widgets import PathEdit

View file

@ -1,5 +1,13 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QLabel, QVBoxLayout, QCheckBox, QFormLayout, QHBoxLayout, QPushButton
from PyQt5.QtWidgets import (
QDialog,
QLabel,
QVBoxLayout,
QCheckBox,
QFormLayout,
QHBoxLayout,
QPushButton,
)
from qtawesome import icon
from legendary.models.game import Game
@ -12,7 +20,9 @@ class UninstallDialog(QDialog):
self.info = 0
self.setAttribute(Qt.WA_DeleteOnClose, True)
self.layout = QVBoxLayout()
self.info_text = QLabel(self.tr("Do you really want to uninstall {}").format(game.app_title))
self.info_text = QLabel(
self.tr("Do you really want to uninstall {}").format(game.app_title)
)
self.layout.addWidget(self.info_text)
self.keep_files = QCheckBox(self.tr("Keep Files"))
self.form = QFormLayout()
@ -21,7 +31,9 @@ class UninstallDialog(QDialog):
self.layout.addLayout(self.form)
self.button_layout = QHBoxLayout()
self.ok_button = QPushButton(icon("ei.remove-circle", color="red"), self.tr("Uninstall"))
self.ok_button = QPushButton(
icon("ei.remove-circle", color="red"), self.tr("Uninstall")
)
self.ok_button.clicked.connect(self.ok)
self.cancel_button = QPushButton(self.tr("Cancel"))

View file

@ -1,5 +1,11 @@
from PyQt5.QtGui import QTextCursor, QFont
from PyQt5.QtWidgets import QPlainTextEdit, QWidget, QPushButton, QFileDialog, QVBoxLayout
from PyQt5.QtWidgets import (
QPlainTextEdit,
QWidget,
QPushButton,
QFileDialog,
QVBoxLayout,
)
class ConsoleWindow(QWidget):
@ -22,7 +28,9 @@ class ConsoleWindow(QWidget):
self.setLayout(self.layout)
def save(self):
file, ok = QFileDialog.getSaveFileName(self, "Save output", "", "Log Files (*.log);;All Files (*)")
file, ok = QFileDialog.getSaveFileName(
self, "Save output", "", "Log Files (*.log);;All Files (*)"
)
if ok:
if "." not in file:
file += ".log"
@ -39,7 +47,6 @@ class ConsoleWindow(QWidget):
class Console(QPlainTextEdit):
def __init__(self):
super().__init__()
self.setReadOnly(True)
@ -51,12 +58,13 @@ class Console(QPlainTextEdit):
self.scroll_to_last_line()
def error(self, text):
self._cursor_output.insertHtml(f"<font color=\"Red\">{text}</font>")
self._cursor_output.insertHtml(f'<font color="Red">{text}</font>')
self.scroll_to_last_line()
def scroll_to_last_line(self):
cursor = self.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.movePosition(QTextCursor.Up if cursor.atBlockStart() else
QTextCursor.StartOfLine)
cursor.movePosition(
QTextCursor.Up if cursor.atBlockStart() else QTextCursor.StartOfLine
)
self.setTextCursor(cursor)

View file

@ -13,7 +13,6 @@ logger = getLogger("Window")
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setAttribute(Qt.WA_DeleteOnClose)
@ -54,10 +53,14 @@ class MainWindow(QMainWindow):
decor_width = margins.left() + margins.right()
decor_height = margins.top() + margins.bottom()
window_size = QSize(self.width(), self.height()).boundedTo(
screen_rect.size() - QSize(decor_width, decor_height))
screen_rect.size() - QSize(decor_width, decor_height)
)
self.resize(window_size)
self.move(screen_rect.center() - self.rect().adjusted(0, 0, decor_width, decor_height).center())
self.move(
screen_rect.center()
- self.rect().adjusted(0, 0, decor_width, decor_height).center()
)
def timer_finished(self):
file_path = os.path.join(data_dir, "lockfile")
@ -67,8 +70,12 @@ class MainWindow(QMainWindow):
file.close()
if action.startswith("launch"):
game = action.replace("launch ", "").replace("\n", "")
if game in [i.app_name for i in self.tab_widget.games_tab.game_list] and self.core.is_installed(game):
self.tab_widget.games_tab.game_utils.prepare_launch(game, offline=shared.args.offline)
if game in [
i.app_name for i in self.tab_widget.games_tab.game_list
] and self.core.is_installed(game):
self.tab_widget.games_tab.game_utils.prepare_launch(
game, offline=shared.args.offline
)
else:
logger.info(f"Could not find {game} in Games")
elif action.startswith("start"):

View file

@ -29,8 +29,15 @@ class TabWidget(QTabWidget):
if not shared.args.offline:
# updates = self.games_tab.default_widget.game_list.updates
self.downloadTab = DownloadsTab(self.games_tab.updates)
self.addTab(self.downloadTab, "Downloads" + (
" (" + str(len(self.games_tab.updates)) + ")" if len(self.games_tab.updates) != 0 else ""))
self.addTab(
self.downloadTab,
"Downloads"
+ (
" (" + str(len(self.games_tab.updates)) + ")"
if len(self.games_tab.updates) != 0
else ""
),
)
self.store = Shop(self.core)
self.addTab(self.store, self.tr("Store (Beta)"))
@ -49,14 +56,18 @@ class TabWidget(QTabWidget):
self.mini_widget = MiniWidget()
account_action = QWidgetAction(self)
account_action.setDefaultWidget(self.mini_widget)
account_button = TabButtonWidget('mdi.account-circle', 'Account')
account_button = TabButtonWidget("mdi.account-circle", "Account")
account_button.setMenu(QMenu())
account_button.menu().addAction(account_action)
self.tabBar().setTabButton(disabled_tab + 1, self.tabBar().RightSide, account_button)
self.tabBar().setTabButton(
disabled_tab + 1, self.tabBar().RightSide, account_button
)
self.addTab(self.settings, icon("fa.gear"), "")
self.settings.about.update_available_ready.connect(lambda: self.tabBar().setTabText(5, "(!)"))
self.settings.about.update_available_ready.connect(
lambda: self.tabBar().setTabText(5, "(!)")
)
# Signals
# set current index
self.signals.set_main_tab_index.connect(self.setCurrentIndex)
@ -75,8 +86,12 @@ class TabWidget(QTabWidget):
QShortcut("Alt+4", self).activated.connect(lambda: self.setCurrentIndex(5))
def update_dl_tab_text(self):
num_downloads = len(set([i.options.app_name for i in self.downloadTab.dl_queue] + [i for i in
self.downloadTab.update_widgets.keys()]))
num_downloads = len(
set(
[i.options.app_name for i in self.downloadTab.dl_queue]
+ [i for i in self.downloadTab.update_widgets.keys()]
)
)
if num_downloads != 0:
self.setTabText(1, f"Downloads ({num_downloads})")

View file

@ -20,7 +20,10 @@ class MiniWidget(QWidget):
self.open_browser = QPushButton(self.tr("Account settings"))
self.open_browser.clicked.connect(
lambda: webbrowser.open("https://www.epicgames.com/account/personal?productName=epicgames"))
lambda: webbrowser.open(
"https://www.epicgames.com/account/personal?productName=epicgames"
)
)
self.layout.addWidget(self.open_browser)
self.logout_button = QPushButton(self.tr("Logout"))
@ -29,9 +32,13 @@ class MiniWidget(QWidget):
self.setLayout(self.layout)
def logout(self):
reply = QMessageBox.question(self.parent().parent(), 'Message',
self.tr("Do you really want to logout"), QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
reply = QMessageBox.question(
self.parent().parent(),
"Message",
self.tr("Do you really want to logout"),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if reply == QMessageBox.Yes:
self.core.lgd.invalidate_userdata()

View file

@ -3,8 +3,14 @@ from logging import getLogger
from typing import List, Dict
from PyQt5.QtCore import QThread, pyqtSignal, QSettings
from PyQt5.QtWidgets import QWidget, QMessageBox, QVBoxLayout, QLabel, QPushButton, \
QGroupBox
from PyQt5.QtWidgets import (
QWidget,
QMessageBox,
QVBoxLayout,
QLabel,
QPushButton,
QGroupBox,
)
from legendary.core import LegendaryCore
from legendary.models.downloading import UIUpdate
@ -59,7 +65,9 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
self.signals.game_uninstalled.connect(self.queue_item_removed)
self.signals.game_uninstalled.connect(self.remove_update)
self.signals.add_download.connect(lambda app_name: self.add_update(self.core.get_installed_game(app_name)))
self.signals.add_download.connect(
lambda app_name: self.add_update(self.core.get_installed_game(app_name))
)
shared.signals.game_uninstalled.connect(self.game_uninstalled)
self.reset_infos()
@ -75,7 +83,9 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
self.update_widgets[igame.app_name] = widget
widget.update_signal.connect(self.get_install_options)
if QSettings().value("auto_update", False, bool):
self.get_install_options(InstallOptionsModel(app_name=igame.app_name, update=True, silent=True))
self.get_install_options(
InstallOptionsModel(app_name=igame.app_name, update=True, silent=True)
)
widget.update_button.setDisabled(True)
self.update_text.setVisible(False)
@ -148,7 +158,12 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
if game.app_name in self.update_widgets.keys():
igame = self.core.get_installed_game(game.app_name)
if self.core.get_asset(game.app_name, igame.platform, False).build_version == igame.version:
if (
self.core.get_asset(
game.app_name, igame.platform, False
).build_version
== igame.version
):
self.remove_update(game.app_name)
self.signals.send_notification.emit(game.app_title)
@ -188,12 +203,20 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
self.analysis = None
def statistics(self, ui_update: UIUpdate):
self.progress_bar.setValue(100 * ui_update.total_downloaded // self.analysis.dl_size)
self.progress_bar.setValue(
100 * ui_update.total_downloaded // self.analysis.dl_size
)
self.dl_speed.setText(f"{get_size(ui_update.download_speed)}/s")
self.cache_used.setText(f"{get_size(ui_update.cache_usage) if ui_update.cache_usage > 1023 else '0KB'}")
self.downloaded.setText(f"{get_size(ui_update.total_downloaded)} / {get_size(self.analysis.dl_size)}")
self.cache_used.setText(
f"{get_size(ui_update.cache_usage) if ui_update.cache_usage > 1023 else '0KB'}"
)
self.downloaded.setText(
f"{get_size(ui_update.total_downloaded)} / {get_size(self.analysis.dl_size)}"
)
self.time_left.setText(self.get_time(ui_update.estimated_time_left))
self.signals.dl_progress.emit(100 * ui_update.total_downloaded // self.analysis.dl_size)
self.signals.dl_progress.emit(
100 * ui_update.total_downloaded // self.analysis.dl_size
)
def get_time(self, seconds: int) -> str:
return str(datetime.timedelta(seconds=seconds))
@ -209,14 +232,24 @@ class DownloadsTab(QWidget, Ui_DownloadsTab):
def get_install_options(self, options: InstallOptionsModel):
install_dialog = InstallDialog(InstallQueueItemModel(options=options),
update=options.update, silent=options.silent, parent=self)
install_dialog = InstallDialog(
InstallQueueItemModel(options=options),
update=options.update,
silent=options.silent,
parent=self,
)
install_dialog.result_ready.connect(self.on_install_dialog_closed)
install_dialog.execute()
def start_download(self, download_item: InstallQueueItemModel):
downloads = len(self.downloadTab.dl_queue) + len(self.downloadTab.update_widgets.keys()) + 1
self.setTabText(1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else ""))
downloads = (
len(self.downloadTab.dl_queue)
+ len(self.downloadTab.update_widgets.keys())
+ 1
)
self.setTabText(
1, "Downloads" + ((" (" + str(downloads) + ")") if downloads != 0 else "")
)
self.setCurrentIndex(1)
self.downloadTab.install_game(download_item)
self.games_tab.start_download(download_item.options.app_name)
@ -244,13 +277,22 @@ class UpdateWidget(QWidget):
self.update_with_settings.clicked.connect(lambda: self.update_game(False))
self.layout.addWidget(self.update_button)
self.layout.addWidget(self.update_with_settings)
self.layout.addWidget(QLabel(
self.tr("Version: ") + self.game.version + " -> " +
self.core.get_asset(self.game.app_name, self.game.platform, True).build_version))
self.layout.addWidget(
QLabel(
self.tr("Version: ")
+ self.game.version
+ " -> "
+ self.core.get_asset(
self.game.app_name, self.game.platform, True
).build_version
)
)
self.setLayout(self.layout)
def update_game(self, auto: bool):
self.update_button.setDisabled(True)
self.update_with_settings.setDisabled(True)
self.update_signal.emit(InstallOptionsModel(app_name=self.game.app_name, silent=auto)) # True if settings
self.update_signal.emit(
InstallOptionsModel(app_name=self.game.app_name, silent=auto)
) # True if settings

View file

@ -1,7 +1,14 @@
from logging import getLogger
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QGroupBox, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QWidget
from PyQt5.QtWidgets import (
QGroupBox,
QVBoxLayout,
QLabel,
QHBoxLayout,
QPushButton,
QWidget,
)
from qtawesome import icon
from rare.utils.models import InstallQueueItemModel
@ -28,7 +35,9 @@ class DlWidget(QWidget):
self.left_layout.addWidget(self.move_up_button)
self.move_down_buttton = QPushButton(icon("fa.arrow-down"), "")
self.move_down_buttton.clicked.connect(lambda: self.move_down.emit(self.app_name))
self.move_down_buttton.clicked.connect(
lambda: self.move_down.emit(self.app_name)
)
self.left_layout.addWidget(self.move_down_buttton)
self.move_down_buttton.setFixedWidth(20)
@ -43,8 +52,18 @@ class DlWidget(QWidget):
self.size = QHBoxLayout()
self.size.addWidget(QLabel(self.tr("Download size: {} GB").format(round(dl_size / 1024 ** 3, 2))))
self.size.addWidget(QLabel(self.tr("Install size: {} GB").format(round(install_size / 1024 ** 3, 2))))
self.size.addWidget(
QLabel(
self.tr("Download size: {} GB").format(round(dl_size / 1024 ** 3, 2))
)
)
self.size.addWidget(
QLabel(
self.tr("Install size: {} GB").format(
round(install_size / 1024 ** 3, 2)
)
)
)
self.right_layout.addLayout(self.size)
self.delete = QPushButton(self.tr("Remove Download"))
@ -69,7 +88,9 @@ class DlQueueWidget(QGroupBox):
self.layout().addWidget(self.text)
def update_queue(self, dl_queue: list):
logger.debug("Update Queue " + ", ".join(i.download.game.app_title for i in dl_queue))
logger.debug(
"Update Queue " + ", ".join(i.download.game.app_title for i in dl_queue)
)
self.dl_queue = dl_queue
for item in (self.layout().itemAt(i) for i in range(self.layout().count())):

View file

@ -59,12 +59,23 @@ class DownloadThread(QThread):
for t in self.dlm.threads:
t.join(timeout=5.0)
if t.is_alive():
logger.warning(f'Thread did not terminate! {repr(t)}')
logger.warning(f"Thread did not terminate! {repr(t)}")
# clean up all the queues, otherwise this process won't terminate properly
for name, q in zip(('Download jobs', 'Writer jobs', 'Download results', 'Writer results'),
(self.dlm.dl_worker_queue, self.dlm.writer_queue, self.dlm.dl_result_q,
self.dlm.writer_result_q)):
for name, q in zip(
(
"Download jobs",
"Writer jobs",
"Download results",
"Writer results",
),
(
self.dlm.dl_worker_queue,
self.dlm.writer_queue,
self.dlm.dl_result_q,
self.dlm.writer_result_q,
),
):
logger.debug(f'Cleaning up queue "{name}"')
try:
while True:
@ -73,11 +84,11 @@ class DownloadThread(QThread):
q.close()
q.join_thread()
except AttributeError:
logger.warning(f'Queue {name} did not close')
logger.warning(f"Queue {name} did not close")
if self.dlm.writer_queue:
# cancel installation
self.dlm.writer_queue.put_nowait(WriterTask('', kill=True))
self.dlm.writer_queue.put_nowait(WriterTask("", kill=True))
# forcibly kill DL workers that are not actually dead yet
for child in self.dlm.children:
@ -95,9 +106,16 @@ class DownloadThread(QThread):
# force kill any threads that are somehow still alive
for proc in psutil.process_iter():
# check whether the process name matches
if sys.platform in ['linux', 'darwin'] and proc.name() == 'DownloadThread':
if (
sys.platform in ["linux", "darwin"]
and proc.name() == "DownloadThread"
):
proc.kill()
elif sys.platform == 'win32' and proc.name() == 'python.exe' and proc.create_time() >= start_time:
elif (
sys.platform == "win32"
and proc.name() == "python.exe"
and proc.create_time() >= start_time
):
proc.kill()
logger.info("Download stopped. It can be continued later.")
@ -111,7 +129,9 @@ class DownloadThread(QThread):
self.dlm.join()
except Exception as e:
logger.error(f"Installation failed after {time.time() - start_time:.02f} seconds: {e}")
logger.error(
f"Installation failed after {time.time() - start_time:.02f} seconds: {e}"
)
self.status.emit("error " + str(e))
return
@ -130,48 +150,65 @@ class DownloadThread(QThread):
dlcs = self.core.get_dlc_for_game(self.igame.app_name)
if dlcs:
print('The following DLCs are available for this game:')
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.')
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}"')
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.')
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.logger.info("Deleting now untagged files.")
self.core.uninstall_tag(old_igame)
self.core.install_game(old_igame)
self.status.emit("finish")
def _handle_postinstall(self, postinstall, igame):
print('This game lists the following prequisites to be installed:')
print(f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}')
print("This game lists the following prequisites to be installed:")
print(
f'- {postinstall["name"]}: {" ".join((postinstall["path"], postinstall["args"]))}'
)
if platform.system() == "Windows":
if QMessageBox.question(self, "", "Do you want to install the prequisites",
QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
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'])
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)
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.')
logger.info("Automatic installation not available on Linux.")
def kill(self):
self._kill = True

View file

@ -58,7 +58,9 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
self.game_info_tabs.back_clicked.connect(lambda: self.setCurrentIndex(0))
self.addWidget(self.game_info_tabs)
self.game_info_tabs.info.verification_finished.connect(self.verification_finished)
self.game_info_tabs.info.verification_finished.connect(
self.verification_finished
)
self.game_info_tabs.info.uninstalled.connect(lambda x: self.setCurrentIndex(0))
self.import_sync_tabs = ImportSyncTabs(self)
@ -102,7 +104,9 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
self.signals.dl_progress.connect(self.installing_widget.set_status)
self.signals.installation_started.connect(self.installation_started)
self.signals.update_gamelist.connect(self.update_list)
self.signals.installation_finished.connect(lambda x: self.installing_widget.setVisible(False))
self.signals.installation_finished.connect(
lambda x: self.installing_widget.setVisible(False)
)
self.signals.game_uninstalled.connect(lambda name: self.update_list([name]))
self.game_utils.update_list.connect(self.update_list)
@ -174,9 +178,11 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
self.list_view.layout().addWidget(list_widget)
def update_count_games_label(self):
self.count_games_label.setText(self.tr("Installed Games: {} Available Games: {}").format(
len(self.core.get_installed_list()),
len(self.game_list)))
self.count_games_label.setText(
self.tr("Installed Games: {} Available Games: {}").format(
len(self.core.get_installed_list()), len(self.game_list)
)
)
def add_installed_widget(self, app_name):
pixmap = get_pixmap(app_name)
@ -249,7 +255,10 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
visible = True
else:
visible = True
if search_text.lower() not in w.game.app_name.lower() and search_text.lower() not in w.game.app_title.lower():
if (
search_text.lower() not in w.game.app_name.lower()
and search_text.lower() not in w.game.app_title.lower()
):
visible = False
w.setVisible(visible)
@ -261,16 +270,24 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
if widgets := self.widgets.get(app_name):
# from update
if self.core.is_installed(widgets[0].game.app_name) and isinstance(widgets[0], BaseInstalledWidget):
if self.core.is_installed(widgets[0].game.app_name) and isinstance(
widgets[0], BaseInstalledWidget
):
logger.debug("Update Gamelist: Updated: " + app_name)
igame = self.core.get_installed_game(app_name)
for w in widgets:
w.igame = igame
w.update_available = self.core.get_asset(w.game.app_name, w.igame.platform,
True).build_version != igame.version
w.update_available = (
self.core.get_asset(
w.game.app_name, w.igame.platform, True
).build_version
!= igame.version
)
widgets[0].leaveEvent(None)
# new installed
elif self.core.is_installed(app_name) and isinstance(widgets[0], BaseUninstalledWidget):
elif self.core.is_installed(app_name) and isinstance(
widgets[0], BaseUninstalledWidget
):
logger.debug("Update Gamelist: New installed " + app_name)
self.widgets[app_name][0].deleteLater()
self.widgets[app_name][1].deleteLater()
@ -280,8 +297,9 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
update_list = True
# uninstalled
elif not self.core.is_installed(widgets[0].game.app_name) and isinstance(widgets[0],
BaseInstalledWidget):
elif not self.core.is_installed(
widgets[0].game.app_name
) and isinstance(widgets[0], BaseInstalledWidget):
logger.debug("Update list: Uninstalled: " + app_name)
self.widgets[app_name][0].deleteLater()
self.widgets[app_name][1].deleteLater()
@ -305,8 +323,13 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
if not game.app_name in installed_names:
uninstalled_names.append(game.app_name)
new_installed_games = list(set(installed_names) - set([i.app_name for i in self.installed]))
new_uninstalled_games = list(set(uninstalled_names) - set([i.app_name for i in self.uninstalled_games]))
new_installed_games = list(
set(installed_names) - set([i.app_name for i in self.installed])
)
new_uninstalled_games = list(
set(uninstalled_names)
- set([i.app_name for i in self.uninstalled_games])
)
if (not new_uninstalled_games) and (not new_installed_games):
return
@ -332,7 +355,9 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
game = self.core.get_game(name, False)
self.add_uninstalled_widget(game)
for igame in sorted(self.core.get_installed_list(), key=lambda x: x.title):
for igame in sorted(
self.core.get_installed_list(), key=lambda x: x.title
):
i_widget, list_widget = self.widgets[igame.app_name]
self.icon_view.layout().addWidget(i_widget)
@ -348,7 +373,10 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
self.uninstalled_games = []
games, self.dlcs = self.core.get_game_and_dlc_list()
for game in sorted(games, key=lambda x: x.app_title):
if not self.core.is_installed(game.app_name) and game.app_name not in self.no_asset_names:
if (
not self.core.is_installed(game.app_name)
and game.app_name not in self.no_asset_names
):
i_widget, list_widget = self.widgets[game.app_name]
self.icon_view.layout().addWidget(i_widget)
self.list_view.layout().addWidget(list_widget)
@ -391,8 +419,9 @@ class GamesTab(QStackedWidget, Ui_GamesTab):
self.list_view.setParent(None)
# insert widget in layout
self.scroll_widget.layout().insertWidget(1,
self.icon_view if self.head_bar.view.isChecked() else self.list_view)
self.scroll_widget.layout().insertWidget(
1, self.icon_view if self.head_bar.view.isChecked() else self.list_view
)
def toggle_view(self):
self.settings.setValue("icon_view", not self.head_bar.view.isChecked())

View file

@ -44,9 +44,15 @@ class SaveWorker(QRunnable):
def run(self) -> None:
try:
if isinstance(self.model, DownloadModel):
shared.core.download_saves(self.model.app_name, self.model.latest_save.manifest_name, self.model.path)
shared.core.download_saves(
self.model.app_name,
self.model.latest_save.manifest_name,
self.model.path,
)
else:
shared.core.upload_save(self.model.app_name, self.model.path, self.model.date_time)
shared.core.upload_save(
self.model.app_name, self.model.path, self.model.date_time
)
except Exception as e:
self.signals.finished.emit(str(e), self.model.app_name)
logger.error(str(e))
@ -69,7 +75,13 @@ class CloudSaveDialog(QDialog, Ui_SyncSaveDialog):
UPLOAD = 1
CANCEL = 0
def __init__(self, igame: InstalledGame, dt_local: datetime.datetime, dt_remote: datetime.datetime, newer: str):
def __init__(
self,
igame: InstalledGame,
dt_local: datetime.datetime,
dt_remote: datetime.datetime,
newer: str,
):
super(CloudSaveDialog, self).__init__()
self.setupUi(self)
@ -155,18 +167,34 @@ class CloudSaveUtils(QObject):
self.core.lgd.set_installed_game(app_name, igame)
logger.info(f"Set save path of {igame.title} to {savepath}")
elif not ignore_settings: # sync on startup
if QMessageBox.question(None, "Warning", self.tr(
"Could not compute cloud save path. Please set it in Game settings manually. \nDo you want to launch {} anyway?").format(
igame.title), QMessageBox.Yes | QMessageBox.No, QMessageBox.No) == QMessageBox.Yes:
if (
QMessageBox.question(
None,
"Warning",
self.tr(
"Could not compute cloud save path. Please set it in Game settings manually. \nDo you want to launch {} anyway?"
).format(igame.title),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
== QMessageBox.Yes
):
return False
else:
raise ValueError("No savepath detected")
else:
QMessageBox.warning(None, "Warning",
self.tr("No savepath found. Please set it in Game Settings manually"))
QMessageBox.warning(
None,
"Warning",
self.tr(
"No savepath found. Please set it in Game Settings manually"
),
)
return False
res, (dt_local, dt_remote) = self.core.check_savegame_state(igame.save_path, self.latest_saves.get(app_name))
res, (dt_local, dt_remote) = self.core.check_savegame_state(
igame.save_path, self.latest_saves.get(app_name)
)
if res == SaveGameStatus.NO_SAVE:
return False
@ -217,18 +245,27 @@ class CloudSaveUtils(QObject):
self.core.lgd.set_installed_game(app_name, igame)
logger.info(f"Set save path of {igame.title} to {savepath}")
else:
QMessageBox.warning(None, "Warning", self.tr("No savepath set. Skip syncing with cloud"))
QMessageBox.warning(
None, "Warning", self.tr("No savepath set. Skip syncing with cloud")
)
return False
res, (dt_local, dt_remote) = self.core.check_savegame_state(igame.save_path, self.latest_saves.get(app_name))
res, (dt_local, dt_remote) = self.core.check_savegame_state(
igame.save_path, self.latest_saves.get(app_name)
)
if res == SaveGameStatus.LOCAL_NEWER and not always_ask:
self.upload_saves(igame, dt_local)
return
elif res == SaveGameStatus.NO_SAVE and not always_ask:
QMessageBox.warning(None, "No saves", self.tr(
"There are no saves local and online. Maybe you have to change save path of {}").format(igame.title))
QMessageBox.warning(
None,
"No saves",
self.tr(
"There are no saves local and online. Maybe you have to change save path of {}"
).format(igame.title),
)
self.sync_finished.emit(app_name)
return
@ -256,7 +293,11 @@ class CloudSaveUtils(QObject):
def download_saves(self, igame):
logger.info("Downloading saves for " + igame.title)
w = SaveWorker(DownloadModel(igame.app_name, self.latest_saves.get(igame.app_name), igame.save_path))
w = SaveWorker(
DownloadModel(
igame.app_name, self.latest_saves.get(igame.app_name), igame.save_path
)
)
w.signals.finished.connect(self.worker_finished)
self.thread_pool.start(w)
@ -266,5 +307,9 @@ class CloudSaveUtils(QObject):
self.sync_finished.emit(app_name)
self.latest_saves = self.get_latest_saves(shared.api_results.saves)
else:
QMessageBox.warning(None, "Warning", self.tr("Syncing with cloud failed: \n ") + error_message)
QMessageBox.warning(
None,
"Warning",
self.tr("Syncing with cloud failed: \n ") + error_message,
)
self.sync_finished.emit(app_name)

View file

@ -33,7 +33,10 @@ class GameInfoTabs(SideTabWidget):
self.settings.update_game(app_name)
# DLC Tab: Disable if no dlcs available
if len(self.dlc_list.get(self.core.get_game(app_name).catalog_item_id, [])) == 0:
if (
len(self.dlc_list.get(self.core.get_game(app_name).catalog_item_id, []))
== 0
):
self.setTabEnabled(3, False)
else:
self.setTabEnabled(3, True)

View file

@ -69,11 +69,18 @@ class GameDlc(QWidget, Ui_GameDlc):
def install(self, app_name):
if not self.core.is_installed(self.game.app_name):
QMessageBox.warning(self, "Error", self.tr("Base Game is not installed. Please install {} first").format(
self.game.app_title))
QMessageBox.warning(
self,
"Error",
self.tr("Base Game is not installed. Please install {} first").format(
self.game.app_title
),
)
return
self.signals.install_game.emit(InstallOptionsModel(app_name=app_name, update=True))
self.signals.install_game.emit(
InstallOptionsModel(app_name=app_name, update=True)
)
class GameDlcWidget(QFrame, Ui_GameDlcWidget):
@ -109,10 +116,15 @@ class GameDlcWidget(QFrame, Ui_GameDlcWidget):
self.pixmap = a0
self.image.setPixmap(
self.pixmap.scaledToHeight(
self.dlc_info.size().height() - (self.image.contentsMargins().top() +
self.image.contentsMargins().bottom() +
self.image.lineWidth()*2),
Qt.SmoothTransformation))
self.dlc_info.size().height()
- (
self.image.contentsMargins().top()
+ self.image.contentsMargins().bottom()
+ self.image.lineWidth() * 2
),
Qt.SmoothTransformation,
)
)
self.image.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
def uninstall_dlc(self):

View file

@ -58,20 +58,32 @@ class GameInfo(QWidget, Ui_GameInfo):
self.uninstalled.emit(self.game.app_name)
def repair(self):
repair_file = os.path.join(self.core.lgd.get_tmp_path(), f'{self.game.app_name}.repair')
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"))
QMessageBox.warning(
self,
"Warning",
self.tr(
"Repair file does not exist or game does not need a repair. Please verify game first"
),
)
return
self.signals.install_game.emit(InstallOptionsModel(app_name=self.game.app_name, repair=True,
update=True))
self.signals.install_game.emit(
InstallOptionsModel(app_name=self.game.app_name, repair=True, update=True)
)
def verify(self):
if not os.path.exists(self.igame.install_path):
logger.error("Path does not exist")
QMessageBox.warning(self, "Warning",
self.tr("Installation path of {} does not exist. Cannot verify").format(
self.igame.title))
QMessageBox.warning(
self,
"Warning",
self.tr("Installation path of {} does not exist. Cannot verify").format(
self.igame.title
),
)
return
self.verify_widget.setCurrentIndex(1)
verify_worker = VerifyWorker(self.core, self.game.app_name)
@ -88,8 +100,11 @@ class GameInfo(QWidget, Ui_GameInfo):
def finish_verify(self, failed, missing, app_name):
if failed == missing == 0:
QMessageBox.information(self, "Summary",
"Game was verified successfully. No missing or corrupt files found")
QMessageBox.information(
self,
"Summary",
"Game was verified successfully. No missing or corrupt files found",
)
igame = self.core.get_installed_game(app_name)
if igame.needs_verification:
igame.needs_verification = False
@ -99,19 +114,28 @@ class GameInfo(QWidget, Ui_GameInfo):
QMessageBox.warning(self, "Warning", self.tr("Something went wrong"))
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)
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.signals.install_game.emit(InstallOptionsModel(app_name=self.game.app_name, repair=True,
update=True))
self.signals.install_game.emit(
InstallOptionsModel(
app_name=self.game.app_name, repair=True, update=True
)
)
self.verify_widget.setCurrentIndex(0)
self.verify_threads.pop(app_name)
def update_game(self, app_name: str):
self.game = self.core.get_game(app_name)
self.igame = self.core.get_installed_game(self.game.app_name)
self.game_title.setText(f'<h2>{self.game.app_title}</h2>')
self.game_title.setText(f"<h2>{self.game.app_title}</h2>")
pixmap = get_pixmap(self.game.app_name)
w = 200
@ -122,7 +146,9 @@ class GameInfo(QWidget, Ui_GameInfo):
if self.igame:
self.version.setText(self.igame.version)
else:
self.version.setText(self.game.app_version(self.igame.platform if self.igame else "Windows"))
self.version.setText(
self.game.app_version(self.igame.platform if self.igame else "Windows")
)
self.dev.setText(self.game.metadata["developer"])
if self.igame:
@ -154,10 +180,14 @@ class GameInfo(QWidget, Ui_GameInfo):
self.steam_worker.set_app_name(self.game.app_name)
QThreadPool.globalInstance().start(self.steam_worker)
if len(self.verify_threads.keys()) == 0 or not self.verify_threads.get(self.game.app_name):
if len(self.verify_threads.keys()) == 0 or not self.verify_threads.get(
self.game.app_name
):
self.verify_widget.setCurrentIndex(0)
elif self.verify_threads.get(self.game.app_name):
self.verify_widget.setCurrentIndex(1)
self.verify_progress.setValue(
self.verify_threads[self.game.app_name].num / self.verify_threads[self.game.app_name].total * 100
self.verify_threads[self.game.app_name].num
/ self.verify_threads[self.game.app_name].total
* 100
)

View file

@ -4,7 +4,14 @@ from logging import getLogger
from typing import Tuple
from PyQt5.QtCore import QSettings, QThreadPool, Qt
from PyQt5.QtWidgets import QWidget, QFileDialog, QMessageBox, QLabel, QPushButton, QSizePolicy
from PyQt5.QtWidgets import (
QWidget,
QFileDialog,
QMessageBox,
QLabel,
QPushButton,
QSizePolicy,
)
from qtawesome import icon
from legendary.core import LegendaryCore
@ -23,7 +30,7 @@ def find_proton_wrappers():
os.path.expanduser("~/.steam/steam/steamapps/common"),
"/usr/share/steam/compatibilitytools.d",
os.path.expanduser("~/.steam/compatibilitytools.d"),
os.path.expanduser("~/.steam/root/compatibilitytools.d")
os.path.expanduser("~/.steam/root/compatibilitytools.d"),
]
for c in compatibilitytools_dirs:
if os.path.exists(c):
@ -31,7 +38,9 @@ def find_proton_wrappers():
proton = os.path.join(c, i, "proton")
compatibilitytool = os.path.join(c, i, "compatibilitytool.vdf")
toolmanifest = os.path.join(c, i, "toolmanifest.vdf")
if os.path.exists(proton) and (os.path.exists(compatibilitytool) or os.path.exists(toolmanifest)):
if os.path.exists(proton) and (
os.path.exists(compatibilitytool) or os.path.exists(toolmanifest)
):
wrapper = '"' + proton + '" run'
possible_proton_wrappers.append(wrapper)
if not possible_proton_wrappers:
@ -53,14 +62,23 @@ class GameSettings(QWidget, Ui_GameSettings):
self.core = core
self.settings = QSettings()
self.cloud_save_path_edit = PathEdit("", file_type=QFileDialog.DirectoryOnly,
ph_text=self.tr("Cloud save path"),
edit_func=lambda text: (os.path.exists(text), text),
save_func=self.save_save_path)
self.cloud_gb.layout().addRow(QLabel(self.tr("Save path")), self.cloud_save_path_edit)
self.cloud_save_path_edit = PathEdit(
"",
file_type=QFileDialog.DirectoryOnly,
ph_text=self.tr("Cloud save path"),
edit_func=lambda text: (os.path.exists(text), text),
save_func=self.save_save_path,
)
self.cloud_gb.layout().addRow(
QLabel(self.tr("Save path")), self.cloud_save_path_edit
)
self.compute_save_path_button = QPushButton(icon("fa.magic"), self.tr("Auto compute save path"))
self.compute_save_path_button.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
self.compute_save_path_button = QPushButton(
icon("fa.magic"), self.tr("Auto compute save path")
)
self.compute_save_path_button.setSizePolicy(
QSizePolicy.Maximum, QSizePolicy.Fixed
)
self.compute_save_path_button.clicked.connect(self.compute_save_path)
self.cloud_gb.layout().addRow(None, self.compute_save_path_button)
@ -71,14 +89,14 @@ class GameSettings(QWidget, Ui_GameSettings):
lambda x: self.update_combobox(x, "skip_update_check")
)
self.cloud_sync.stateChanged.connect(
lambda: self.settings.setValue(f"{self.game.app_name}/auto_sync_cloud", self.cloud_sync.isChecked())
lambda: self.settings.setValue(
f"{self.game.app_name}/auto_sync_cloud", self.cloud_sync.isChecked()
)
)
self.launch_params.textChanged.connect(
lambda x: self.save_line_edit("start_params", x)
)
self.wrapper.textChanged.connect(
lambda x: self.save_line_edit("wrapper", x)
)
self.wrapper.textChanged.connect(lambda x: self.save_line_edit("wrapper", x))
self.override_exe_edit.textChanged.connect(
lambda x: self.save_line_edit("override_exe", x)
)
@ -92,7 +110,7 @@ class GameSettings(QWidget, Ui_GameSettings):
self.proton_prefix = PathEdit(
file_type=QFileDialog.DirectoryOnly,
edit_func=self.proton_prefix_edit,
save_func=self.proton_prefix_save
save_func=self.proton_prefix_save,
)
self.proton_prefix_layout.addWidget(self.proton_prefix)
@ -100,8 +118,10 @@ class GameSettings(QWidget, Ui_GameSettings):
# FIXME: Remove the spacerItem and margins from the linux settings
# FIXME: This should be handled differently at soem point in the future
self.linux_settings.layout().setContentsMargins(0, 0, 0, 0)
for item in [self.linux_settings.layout().itemAt(idx) for idx in
range(self.linux_settings.layout().count())]:
for item in [
self.linux_settings.layout().itemAt(idx)
for idx in range(self.linux_settings.layout().count())
]:
if item.spacerItem():
self.linux_settings.layout().removeItem(item)
del item
@ -113,7 +133,10 @@ class GameSettings(QWidget, Ui_GameSettings):
self.game_settings_layout.setAlignment(Qt.AlignTop)
def compute_save_path(self):
if self.core.is_installed(self.game.app_name) and self.game.supports_cloud_saves:
if (
self.core.is_installed(self.game.app_name)
and self.game.supports_cloud_saves
):
try:
new_path = self.core.get_save_path(self.game.app_name)
except Exception as e:
@ -121,16 +144,22 @@ class GameSettings(QWidget, Ui_GameSettings):
self.cloud_save_path_edit.setText(self.tr("Loading"))
self.cloud_save_path_edit.setDisabled(True)
self.compute_save_path_button.setDisabled(True)
resolver = WineResolver(get_raw_save_path(self.game), self.game.app_name, self.core)
resolver = WineResolver(
get_raw_save_path(self.game), self.game.app_name, self.core
)
app_name = self.game.app_name[:]
resolver.signals.result_ready.connect(lambda x: self.wine_resolver_finished(x, app_name))
resolver.signals.result_ready.connect(
lambda x: self.wine_resolver_finished(x, app_name)
)
QThreadPool.globalInstance().start(resolver)
return
else:
self.cloud_save_path_edit.setText(new_path)
def wine_resolver_finished(self, path, app_name):
logger.info(f"Wine resolver finished for {app_name}. Computed save path: {path}")
logger.info(
f"Wine resolver finished for {app_name}. Computed save path: {path}"
)
if app_name == self.game.app_name:
self.cloud_save_path_edit.setDisabled(False)
self.compute_save_path_button.setDisabled(False)
@ -139,9 +168,13 @@ class GameSettings(QWidget, Ui_GameSettings):
os.makedirs(path)
except PermissionError:
self.cloud_save_path_edit.setText("")
QMessageBox.warning(None, "Error", self.tr(
"Error while launching {}. No permission to create {}").format(
self.game.app_title, path))
QMessageBox.warning(
None,
"Error",
self.tr(
"Error while launching {}. No permission to create {}"
).format(self.game.app_title, path),
)
return
if not path:
self.cloud_save_path_edit.setText("")
@ -163,8 +196,13 @@ class GameSettings(QWidget, Ui_GameSettings):
self.core.lgd.config.add_section(self.game.app_name)
self.core.lgd.config.set(self.game.app_name, option, value)
else:
if self.core.lgd.config.has_section(self.game.app_name) and self.core.lgd.config.get(
f"{self.game.app_name}", option, fallback=None) is not None:
if (
self.core.lgd.config.has_section(self.game.app_name)
and self.core.lgd.config.get(
f"{self.game.app_name}", option, fallback=None
)
is not None
):
self.core.lgd.config.remove_option(self.game.app_name, option)
if not self.core.lgd.config[self.game.app_name]:
self.core.lgd.config.remove_section(self.game.app_name)
@ -186,7 +224,9 @@ class GameSettings(QWidget, Ui_GameSettings):
self.core.lgd.config.set(self.game.app_name, option, "false")
else:
if self.game.app_name in self.core.lgd.config.sections():
if self.core.lgd.config.get(f"{self.game.app_name}", option, fallback=False):
if self.core.lgd.config.get(
f"{self.game.app_name}", option, fallback=False
):
self.core.lgd.config.remove_option(self.game.app_name, option)
if not self.core.lgd.config[self.game.app_name]:
self.core.lgd.config.remove_section(self.game.app_name)
@ -197,21 +237,39 @@ class GameSettings(QWidget, Ui_GameSettings):
# Dont use Proton
if i == 0:
if f"{self.game.app_name}" in self.core.lgd.config.sections():
if self.core.lgd.config.get(f"{self.game.app_name}", "wrapper", fallback=False):
self.core.lgd.config.remove_option(self.game.app_name, "wrapper")
if self.core.lgd.config.get(f"{self.game.app_name}", "no_wine", fallback=False):
self.core.lgd.config.remove_option(self.game.app_name, "no_wine")
if self.core.lgd.config.get(
f"{self.game.app_name}", "wrapper", fallback=False
):
self.core.lgd.config.remove_option(
self.game.app_name, "wrapper"
)
if self.core.lgd.config.get(
f"{self.game.app_name}", "no_wine", fallback=False
):
self.core.lgd.config.remove_option(
self.game.app_name, "no_wine"
)
if not self.core.lgd.config[self.game.app_name]:
self.core.lgd.config.remove_section(self.game.app_name)
if f"{self.game.app_name}.env" in self.core.lgd.config.sections():
if self.core.lgd.config.get(f"{self.game.app_name}.env", "STEAM_COMPAT_DATA_PATH", fallback=False):
self.core.lgd.config.remove_option(f"{self.game.app_name}.env", "STEAM_COMPAT_DATA_PATH")
if self.core.lgd.config.get(
f"{self.game.app_name}.env",
"STEAM_COMPAT_DATA_PATH",
fallback=False,
):
self.core.lgd.config.remove_option(
f"{self.game.app_name}.env", "STEAM_COMPAT_DATA_PATH"
)
if not self.core.lgd.config[self.game.app_name + ".env"]:
self.core.lgd.config.remove_section(self.game.app_name + ".env")
self.proton_prefix.setEnabled(False)
# lk: TODO: This has to be fixed properly.
# lk: It happens because of the widget update. Mask it for now behind disabling the save button
self.wrapper.setText(self.core.lgd.config.get(f"{self.game.app_name}", "wrapper", fallback=""))
self.wrapper.setText(
self.core.lgd.config.get(
f"{self.game.app_name}", "wrapper", fallback=""
)
)
self.wrapper.setEnabled(True)
self.linux_settings.wine_groupbox.setEnabled(True)
else:
@ -225,8 +283,11 @@ class GameSettings(QWidget, Ui_GameSettings):
self.core.lgd.config[self.game.app_name + ".env"] = {}
self.core.lgd.config.set(self.game.app_name, "wrapper", wrapper)
self.core.lgd.config.set(self.game.app_name, "no_wine", "true")
self.core.lgd.config.set(self.game.app_name + ".env", "STEAM_COMPAT_DATA_PATH",
os.path.expanduser("~/.proton"))
self.core.lgd.config.set(
self.game.app_name + ".env",
"STEAM_COMPAT_DATA_PATH",
os.path.expanduser("~/.proton"),
)
self.proton_prefix.setText(os.path.expanduser("~/.proton"))
# Dont use Wine
@ -243,7 +304,9 @@ class GameSettings(QWidget, Ui_GameSettings):
return os.path.exists(parent), text
def proton_prefix_save(self, text: str):
self.core.lgd.config.set(self.game.app_name + ".env", "STEAM_COMPAT_DATA_PATH", text)
self.core.lgd.config.set(
self.game.app_name + ".env", "STEAM_COMPAT_DATA_PATH", text
)
self.core.lgd.save_config()
def update_game(self, app_name: str):
@ -252,7 +315,9 @@ class GameSettings(QWidget, Ui_GameSettings):
self.igame = self.core.get_installed_game(self.game.app_name)
if self.igame:
if self.igame.can_run_offline:
offline = self.core.lgd.config.get(self.game.app_name, "offline", fallback="unset")
offline = self.core.lgd.config.get(
self.game.app_name, "offline", fallback="unset"
)
if offline == "true":
self.offline.setCurrentIndex(1)
elif offline == "false":
@ -266,7 +331,9 @@ class GameSettings(QWidget, Ui_GameSettings):
else:
self.offline.setEnabled(False)
skip_update = self.core.lgd.config.get(self.game.app_name, "skip_update_check", fallback="unset")
skip_update = self.core.lgd.config.get(
self.game.app_name, "skip_update_check", fallback="unset"
)
if skip_update == "true":
self.skip_update.setCurrentIndex(1)
elif skip_update == "false":
@ -286,13 +353,19 @@ class GameSettings(QWidget, Ui_GameSettings):
else:
self.linux_settings_scroll.setVisible(True)
proton = self.core.lgd.config.get(f"{app_name}", "wrapper", fallback="").replace('"', "")
proton = self.core.lgd.config.get(
f"{app_name}", "wrapper", fallback=""
).replace('"', "")
if proton and "proton" in proton:
self.proton_prefix.setEnabled(True)
self.proton_wrapper.setCurrentText(f'"{proton.replace(" run", "")}" run')
proton_prefix = self.core.lgd.config.get(f"{app_name}.env", "STEAM_COMPAT_DATA_PATH",
fallback=self.tr(
"Please select path for proton prefix"))
self.proton_wrapper.setCurrentText(
f'"{proton.replace(" run", "")}" run'
)
proton_prefix = self.core.lgd.config.get(
f"{app_name}.env",
"STEAM_COMPAT_DATA_PATH",
fallback=self.tr("Please select path for proton prefix"),
)
self.proton_prefix.setText(proton_prefix)
self.wrapper.setEnabled(False)
else:
@ -305,15 +378,21 @@ class GameSettings(QWidget, Ui_GameSettings):
self.cloud_save_path_edit.setText("")
else:
self.cloud_gb.setEnabled(True)
sync_cloud = self.settings.value(f"{self.game.app_name}/auto_sync_cloud", True, bool)
sync_cloud = self.settings.value(
f"{self.game.app_name}/auto_sync_cloud", True, bool
)
self.cloud_sync.setChecked(sync_cloud)
if self.igame.save_path:
self.cloud_save_path_edit.setText(self.igame.save_path)
else:
self.cloud_save_path_edit.setText("")
self.launch_params.setText(self.core.lgd.config.get(self.game.app_name, "start_params", fallback=""))
self.override_exe_edit.setText(self.core.lgd.config.get(self.game.app_name, "override_exe", fallback=""))
self.launch_params.setText(
self.core.lgd.config.get(self.game.app_name, "start_params", fallback="")
)
self.override_exe_edit.setText(
self.core.lgd.config.get(self.game.app_name, "override_exe", fallback="")
)
self.change = True

View file

@ -56,10 +56,15 @@ class GameUtils(QObject):
game = self.core.get_game(app_name)
igame = self.core.get_installed_game(app_name)
if not os.path.exists(igame.install_path):
if QMessageBox.Yes == QMessageBox.question(None, "Uninstall", self.tr(
"Game files of {} do not exist. Remove it from installed games?").format(igame.title),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes):
if QMessageBox.Yes == QMessageBox.question(
None,
"Uninstall",
self.tr(
"Game files of {} do not exist. Remove it from installed games?"
).format(igame.title),
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
):
self.core.lgd.remove_installed_game(app_name)
return True
else:
@ -72,7 +77,9 @@ class GameUtils(QObject):
shared.signals.game_uninstalled.emit(app_name)
return True
def prepare_launch(self, app_name, offline: bool = False, skip_update_check: bool = False):
def prepare_launch(
self, app_name, offline: bool = False, skip_update_check: bool = False
):
game = self.core.get_game(app_name)
dont_sync_after_finish = False
@ -90,10 +97,19 @@ class GameUtils(QObject):
return
self.sync_finished(app_name)
self.launch_game(app_name, offline, skip_update_check, ask_always_sync=dont_sync_after_finish)
self.launch_game(
app_name, offline, skip_update_check, ask_always_sync=dont_sync_after_finish
)
def launch_game(self, app_name: str, offline: bool = False, skip_update_check: bool = False, wine_bin: str = None,
wine_pfx: str = None, ask_always_sync: bool = False):
def launch_game(
self,
app_name: str,
offline: bool = False,
skip_update_check: bool = False,
wine_bin: str = None,
wine_pfx: str = None,
ask_always_sync: bool = False,
):
if shared.args.offline:
offline = True
game = self.core.get_game(app_name)
@ -105,8 +121,15 @@ class GameUtils(QObject):
return
if QSettings().value("confirm_start", False, bool):
if not QMessageBox.question(None, "Launch", self.tr("Do you want to launch {}").format(game.app_title),
QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
if (
not QMessageBox.question(
None,
"Launch",
self.tr("Do you want to launch {}").format(game.app_title),
QMessageBox.Yes | QMessageBox.No,
)
== QMessageBox.Yes
):
logger.info("Cancel Startup")
self.finished.emit(app_name, "")
return
@ -119,12 +142,18 @@ class GameUtils(QObject):
logger.error(f"{app_name} is not installed")
if game.is_dlc:
logger.error("Game is dlc")
self.finished.emit(app_name, self.tr("Game is a DLC. Please launch base game instead"))
self.finished.emit(
app_name, self.tr("Game is a DLC. Please launch base game instead")
)
return
if not os.path.exists(igame.install_path):
logger.error("Game doesn't exist")
self.finished.emit(app_name,
self.tr("Game files of {} do not exist. Please install game").format(game.app_title))
self.finished.emit(
app_name,
self.tr(
"Game files of {} do not exist. Please install game"
).format(game.app_title),
)
return
process = GameProcess(app_name)
@ -135,7 +164,9 @@ class GameUtils(QObject):
if not skip_update_check and not self.core.is_noupdate_game(app_name):
# check updates
try:
latest = self.core.get_asset(app_name, igame.platform, update=False)
latest = self.core.get_asset(
app_name, igame.platform, update=False
)
except ValueError:
self.finished.emit(app_name, self.tr("Metadata doesn't exist"))
return
@ -144,12 +175,15 @@ class GameUtils(QObject):
self.finished.emit(app_name, self.tr("Please update game"))
return
params: LaunchParameters = self.core.get_launch_parameters(app_name=app_name, offline=offline,
wine_bin=wine_bin, wine_pfx=wine_pfx)
params: LaunchParameters = self.core.get_launch_parameters(
app_name=app_name, offline=offline, wine_bin=wine_bin, wine_pfx=wine_pfx
)
full_params = list()
full_params.extend(params.launch_command)
full_params.append(os.path.join(params.game_directory, params.game_executable))
full_params.append(
os.path.join(params.game_directory, params.game_executable)
)
full_params.extend(params.game_parameters)
full_params.extend(params.egl_parameters)
full_params.extend(params.user_parameters)
@ -170,23 +204,33 @@ class GameUtils(QObject):
os.makedirs(val)
except PermissionError as e:
logger.error(str(e))
QMessageBox.warning(None, "Error",
self.tr(
"Error while launching {}. No permission to create {} for {}").format(
game.app_title, val, env))
QMessageBox.warning(
None,
"Error",
self.tr(
"Error while launching {}. No permission to create {} for {}"
).format(game.app_title, val, env),
)
process.deleteLater()
return
# check wine executable
if shutil.which(full_params[0]) is None:
# wine binary does not exist
QMessageBox.warning(None, "Warning", self.tr(
"Wine executable '{}' does not exist. Please change it in Settings").format(full_params[0]))
QMessageBox.warning(
None,
"Warning",
self.tr(
"Wine executable '{}' does not exist. Please change it in Settings"
).format(full_params[0]),
)
process.deleteLater()
return
process.setProcessEnvironment(environment)
process.game_finished.connect(self.game_finished)
running_game = RunningGameModel(process=process, app_name=app_name, always_ask_sync=ask_always_sync)
running_game = RunningGameModel(
process=process, app_name=app_name, always_ask_sync=ask_always_sync
)
process.start(full_params[0], full_params[1:])
self.game_launched.emit(app_name)
@ -201,25 +245,37 @@ class GameUtils(QObject):
webbrowser.open(origin_uri)
self.finished.emit(app_name, "")
return
wine_pfx = self.core.lgd.config.get(game.app_name, 'wine_prefix',
fallback=os.path.expanduser("~/.wine"))
wine_pfx = self.core.lgd.config.get(
game.app_name, "wine_prefix", fallback=os.path.expanduser("~/.wine")
)
if not wine_bin:
wine_bin = self.core.lgd.config.get(game.app_name, 'wine_executable', fallback="/usr/bin/wine")
wine_bin = self.core.lgd.config.get(
game.app_name, "wine_executable", fallback="/usr/bin/wine"
)
if shutil.which(wine_bin) is None:
# wine binary does not exist
QMessageBox.warning(None, "Warning",
self.tr("Wine executable '{}' does not exist. Please change it in Settings").format(
wine_bin))
QMessageBox.warning(
None,
"Warning",
self.tr(
"Wine executable '{}' does not exist. Please change it in Settings"
).format(wine_bin),
)
process.deleteLater()
return
env = self.core.get_app_environment(game.app_name, wine_pfx=wine_pfx)
if not env.get('WINEPREFIX') and not os.path.exists("/usr/bin/wine"):
logger.error(f'In order to launch Origin correctly you must specify the wine binary and prefix '
f'to use in the configuration file or command line. See the README for details.')
self.finished.emit(app_name, self.tr("No wine executable selected. Please set it in settings"))
if not env.get("WINEPREFIX") and not os.path.exists("/usr/bin/wine"):
logger.error(
f"In order to launch Origin correctly you must specify the wine binary and prefix "
f"to use in the configuration file or command line. See the README for details."
)
self.finished.emit(
app_name,
self.tr("No wine executable selected. Please set it in settings"),
)
return
environment = QProcessEnvironment()
@ -231,17 +287,27 @@ class GameUtils(QObject):
if QSettings().value("show_console", False, bool):
self.console.show()
process.readyReadStandardOutput.connect(lambda: self.console.log(
str(process.readAllStandardOutput().data(), "utf-8", "ignore")))
process.readyReadStandardError.connect(lambda: self.console.error(
str(process.readAllStandardError().data(), "utf-8", "ignore")))
process.readyReadStandardOutput.connect(
lambda: self.console.log(
str(process.readAllStandardOutput().data(), "utf-8", "ignore")
)
)
process.readyReadStandardError.connect(
lambda: self.console.error(
str(process.readAllStandardError().data(), "utf-8", "ignore")
)
)
def game_finished(self, exit_code, app_name):
logger.info("Game exited with exit code: " + str(exit_code))
is_origin = self.core.get_game(app_name).third_party_store == "Origin"
if exit_code == 53 and is_origin:
msg_box = QMessageBox()
msg_box.setText(self.tr("Origin is not installed. Do you want to download installer file? "))
msg_box.setText(
self.tr(
"Origin is not installed. Do you want to download installer file? "
)
)
msg_box.addButton(QPushButton("Download"), QMessageBox.YesRole)
msg_box.addButton(QPushButton("Cancel"), QMessageBox.RejectRole)
resp = msg_box.exec()
@ -249,8 +315,13 @@ class GameUtils(QObject):
if resp == 0:
webbrowser.open("https://www.dm.origin.com/download")
if is_origin and exit_code == 1:
QMessageBox.warning(None, "Warning",
self.tr("Failed to launch {}").format(self.core.get_game(app_name).app_title))
QMessageBox.warning(
None,
"Warning",
self.tr("Failed to launch {}").format(
self.core.get_game(app_name).app_title
),
)
game: RunningGameModel = self.running_games.get(app_name, None)
if app_name in self.running_games.keys():
@ -262,10 +333,15 @@ class GameUtils(QObject):
if self.core.get_game(app_name).supports_cloud_saves:
if exit_code != 0:
r = QMessageBox.question(None, "Question", self.tr(
"Game exited with code {}, which is not a normal code. It could be caused by a crash. Do you want to sync cloud saves").format(
exit_code),
buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.Yes)
r = QMessageBox.question(
None,
"Question",
self.tr(
"Game exited with code {}, which is not a normal code. It could be caused by a crash. Do you want to sync cloud saves"
).format(exit_code),
buttons=QMessageBox.Yes | QMessageBox.No,
defaultButton=QMessageBox.Yes,
)
if r != QMessageBox.Yes:
return
self.cloud_save_utils.game_finished(app_name, game.always_ask_sync)

View file

@ -33,27 +33,30 @@ class BaseInstalledWidget(QGroupBox):
"update_available": self.tr("Start game without version check"),
"launch": self.tr("Launch Game"),
"launch_origin": self.tr("Launch/Link"),
"running": self.tr("Game running")
"running": self.tr("Game running"),
},
"default": {
"running": self.tr("Game running"),
"syncing": self.tr("Syncing cloud saves"),
"update_available": self.tr("Update available")
}
"update_available": self.tr("Update available"),
},
}
self.game = self.core.get_game(app_name)
self.igame = self.core.get_installed_game(app_name) # None if origin
self.image = QLabel()
self.image.setPixmap(pixmap.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation))
self.image.setPixmap(
pixmap.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
)
self.game_running = False
self.offline = shared.args.offline
self.update_available = False
if self.igame and self.core.lgd.assets:
try:
remote_version = self.core.get_asset(self.game.app_name, platform=self.igame.platform,
update=False).build_version
remote_version = self.core.get_asset(
self.game.app_name, platform=self.igame.platform, update=False
).build_version
except ValueError:
logger.error("Asset error for " + self.game.app_title)
self.update_available = False
@ -75,19 +78,26 @@ class BaseInstalledWidget(QGroupBox):
sync.triggered.connect(self.sync_game)
self.addAction(sync)
if os.path.exists(os.path.expanduser(f"~/Desktop/{self.game.app_title}.desktop")) \
or os.path.exists(os.path.expanduser(f"~/Desktop/{self.game.app_title}.lnk")):
if os.path.exists(
os.path.expanduser(f"~/Desktop/{self.game.app_title}.desktop")
) or os.path.exists(os.path.expanduser(f"~/Desktop/{self.game.app_title}.lnk")):
self.create_desktop = QAction(self.tr("Remove Desktop link"))
else:
self.create_desktop = QAction(self.tr("Create Desktop link"))
if self.igame:
self.create_desktop.triggered.connect(lambda: self.create_desktop_link("desktop"))
self.create_desktop.triggered.connect(
lambda: self.create_desktop_link("desktop")
)
self.addAction(self.create_desktop)
if platform.system() == "Linux":
start_menu_file = os.path.expanduser(f"~/.local/share/applications/{self.game.app_title}.desktop")
start_menu_file = os.path.expanduser(
f"~/.local/share/applications/{self.game.app_title}.desktop"
)
elif platform.system() == "Windows":
start_menu_file = os.path.expandvars("%appdata%/Microsoft/Windows/Start Menu")
start_menu_file = os.path.expandvars(
"%appdata%/Microsoft/Windows/Start Menu"
)
else:
start_menu_file = ""
if platform.system() in ["Windows", "Linux"]:
@ -96,7 +106,9 @@ class BaseInstalledWidget(QGroupBox):
else:
self.create_start_menu = QAction(self.tr("Create start menu link"))
if self.igame:
self.create_start_menu.triggered.connect(lambda: self.create_desktop_link("start_menu"))
self.create_start_menu.triggered.connect(
lambda: self.create_desktop_link("start_menu")
)
self.addAction(self.create_start_menu)
reload_image = QAction(self.tr("Reload Image"), self)
@ -105,19 +117,26 @@ class BaseInstalledWidget(QGroupBox):
uninstall = QAction(self.tr("Uninstall"), self)
uninstall.triggered.connect(
lambda: shared.signals.update_gamelist.emit([self.game.app_name]) if self.game_utils.uninstall_game(
self.game.app_name) else None)
lambda: shared.signals.update_gamelist.emit([self.game.app_name])
if self.game_utils.uninstall_game(self.game.app_name)
else None
)
self.addAction(uninstall)
def reload_image(self):
utils.download_image(self.game, True)
pm = utils.get_pixmap(self.game.app_name)
self.image.setPixmap(pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation))
self.image.setPixmap(
pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
)
def create_desktop_link(self, type_of_link):
if platform.system() not in ["Windows", "Linux"]:
QMessageBox.warning(self, "Warning",
f"Create a Desktop link is currently not supported on {platform.system()}")
QMessageBox.warning(
self,
"Warning",
f"Create a Desktop link is currently not supported on {platform.system()}",
)
return
if type_of_link == "desktop":
path = os.path.expanduser(f"~/Desktop/")
@ -125,19 +144,25 @@ class BaseInstalledWidget(QGroupBox):
path = os.path.expanduser("~/.local/share/applications/")
else:
return
if not (os.path.exists(os.path.expanduser(f"{path}{self.game.app_title}.desktop"))
or os.path.exists(os.path.expanduser(f"{path}{self.game.app_title}.lnk"))):
if not (
os.path.exists(os.path.expanduser(f"{path}{self.game.app_title}.desktop"))
or os.path.exists(os.path.expanduser(f"{path}{self.game.app_title}.lnk"))
):
try:
if not create_desktop_link(self.game.app_name, self.core, type_of_link):
return
except PermissionError:
QMessageBox.warning(self, "Error", "Permission error. Cannot create Desktop Link")
QMessageBox.warning(
self, "Error", "Permission error. Cannot create Desktop Link"
)
if type_of_link == "desktop":
self.create_desktop.setText(self.tr("Remove Desktop link"))
elif type_of_link == "start_menu":
self.create_start_menu.setText(self.tr("Remove Start menu link"))
else:
if os.path.exists(os.path.expanduser(f"{path}{self.game.app_title}.desktop")):
if os.path.exists(
os.path.expanduser(f"{path}{self.game.app_title}.desktop")
):
os.remove(os.path.expanduser(f"{path}{self.game.app_title}.desktop"))
elif os.path.exists(os.path.expanduser(f"{path}{self.game.app_title}.lnk")):
os.remove(os.path.expanduser(f"{path}{self.game.app_title}.lnk"))
@ -151,14 +176,18 @@ class BaseInstalledWidget(QGroupBox):
if not self.game_running:
if self.game.supports_cloud_saves:
self.syncing_cloud_saves = True
self.game_utils.prepare_launch(self.game.app_name, offline, skip_version_check)
self.game_utils.prepare_launch(
self.game.app_name, offline, skip_version_check
)
def sync_finished(self, app_name):
self.syncing_cloud_saves = False
def sync_game(self):
if self.game_utils.cloud_save_utils.sync_before_launch_game(self.game.app_name, True):
if self.game_utils.cloud_save_utils.sync_before_launch_game(
self.game.app_name, True
):
self.syncing_cloud_saves = True
def game_finished(self, app_name, error):

View file

@ -17,7 +17,9 @@ class BaseUninstalledWidget(QGroupBox):
self.game = game
self.core = core
self.image = QLabel()
self.image.setPixmap(pixmap.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation))
self.image.setPixmap(
pixmap.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
)
self.installing = False
self.setContextMenuPolicy(Qt.ActionsContextMenu)
self.setContentsMargins(0, 0, 0, 0)
@ -29,7 +31,9 @@ class BaseUninstalledWidget(QGroupBox):
def reload_image(self):
utils.download_image(self.game, True)
pm = utils.get_uninstalled_pixmap(self.game.app_name)
self.image.setPixmap(pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation))
self.image.setPixmap(
pm.scaled(200, int(200 * 4 / 3), transformMode=Qt.SmoothTransformation)
)
def install(self):
self.show_uninstalled_info.emit(self.game)

View file

@ -6,7 +6,9 @@ from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QPushButton, QLabel
from qtawesome import icon
from rare import shared
from rare.components.tabs.games.game_widgets.base_installed_widget import BaseInstalledWidget
from rare.components.tabs.games.game_widgets.base_installed_widget import (
BaseInstalledWidget,
)
logger = getLogger("GameWidgetInstalled")
@ -43,7 +45,9 @@ class InstalledIconWidget(BaseInstalledWidget):
self.menu_btn.setIcon(icon("ei.info-circle"))
# self.menu_btn.setObjectName("installed_menu_button")
self.menu_btn.setIconSize(QSize(18, 18))
self.menu_btn.enterEvent = lambda x: self.info_label.setText(self.tr("Information"))
self.menu_btn.enterEvent = lambda x: self.info_label.setText(
self.tr("Information")
)
self.menu_btn.leaveEvent = lambda x: self.enterEvent(None)
# remove Border
@ -77,7 +81,9 @@ class InstalledIconWidget(BaseInstalledWidget):
elif self.update_available:
self.info_label.setText(self.texts["hover"]["update_available"])
else:
self.info_label.setText(self.texts["hover"]["launch" if self.igame else "launch_origin"])
self.info_label.setText(
self.texts["hover"]["launch" if self.igame else "launch_origin"]
)
def leaveEvent(self, a0: QEvent = None) -> None:
if self.game_running:

View file

@ -4,7 +4,9 @@ from PyQt5.QtCore import QProcess, pyqtSignal, Qt
from PyQt5.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout
from qtawesome import icon
from rare.components.tabs.games.game_widgets.base_installed_widget import BaseInstalledWidget
from rare.components.tabs.games.game_widgets.base_installed_widget import (
BaseInstalledWidget,
)
from rare.utils.utils import get_size
logger = getLogger("GameWidget")
@ -36,7 +38,9 @@ class InstalledListWidget(BaseInstalledWidget):
self.title_label.setWordWrap(True)
self.childLayout.addWidget(self.title_label)
self.app_name_label = QLabel(self.game.app_name)
self.launch_button = QPushButton(play_icon, self.tr("Launch") if self.igame else self.tr("Link/Play"))
self.launch_button = QPushButton(
play_icon, self.tr("Launch") if self.igame else self.tr("Link/Play")
)
self.launch_button.setObjectName("launch_game_button")
self.launch_button.setFixedWidth(150)
@ -57,7 +61,9 @@ class InstalledListWidget(BaseInstalledWidget):
self.childLayout.addWidget(self.developer_label)
if self.igame:
self.version_label = QLabel("Version: " + str(self.igame.version))
self.size_label = QLabel(f"{self.tr('Installed size')}: {get_size(self.size)}")
self.size_label = QLabel(
f"{self.tr('Installed size')}: {get_size(self.size)}"
)
self.childLayout.addWidget(self.version_label)
self.childLayout.addWidget(self.size_label)

View file

@ -3,7 +3,12 @@ from PyQt5.QtGui import QPaintEvent, QPainter, QPixmap, QPen, QFont, QColor
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QHBoxLayout, QWidget
from rare import shared
from rare.utils.utils import get_pixmap, optimal_text_background, text_color_for_background, get_uninstalled_pixmap
from rare.utils.utils import (
get_pixmap,
optimal_text_background,
text_color_for_background,
get_uninstalled_pixmap,
)
class InstallingGameWidget(QWidget):
@ -53,17 +58,23 @@ class PaintWidget(QWidget):
game = shared.core.get_game(app_name, False)
self.color_image = get_pixmap(game.app_name)
w = 200
self.color_image = self.color_image.scaled(w, int(w * 4 // 3), transformMode=Qt.SmoothTransformation)
self.color_image = self.color_image.scaled(
w, int(w * 4 // 3), transformMode=Qt.SmoothTransformation
)
self.setFixedSize(self.color_image.size())
self.bw_image = get_uninstalled_pixmap(app_name)
self.bw_image = self.bw_image.scaled(w, int(w * 4 // 3), transformMode=Qt.SmoothTransformation)
self.bw_image = self.bw_image.scaled(
w, int(w * 4 // 3), transformMode=Qt.SmoothTransformation
)
self.progress = 0
pixel_list = []
for x in range(self.color_image.width()):
for y in range(self.color_image.height()):
# convert pixmap to qimage, get pixel and remove alpha channel
pixel_list.append(self.color_image.toImage().pixelColor(x, y).getRgb()[:-1])
pixel_list.append(
self.color_image.toImage().pixelColor(x, y).getRgb()[:-1]
)
self.rgb_tuple = optimal_text_background(pixel_list)
@ -74,14 +85,21 @@ class PaintWidget(QWidget):
w = self.bw_image.width() * self.progress // 100
painter.drawPixmap(0, 0, w, self.color_image.height(),
self.color_image.copy(QRect(0, 0, w, self.color_image.height())))
painter.drawPixmap(
0,
0,
w,
self.color_image.height(),
self.color_image.copy(QRect(0, 0, w, self.color_image.height())),
)
# Draw Circle
pen = QPen(QColor(*self.rgb_tuple), 3)
painter.setPen(pen)
painter.setBrush(QColor(*self.rgb_tuple))
painter.drawEllipse(int(self.width() / 2) - 20, int(self.height() / 2) - 20, 40, 40)
painter.drawEllipse(
int(self.width() / 2) - 20, int(self.height() / 2) - 20, 40, 40
)
# draw text
painter.setPen(QColor(*text_color_for_background(self.rgb_tuple)))

View file

@ -4,13 +4,14 @@ from PyQt5.QtWidgets import QVBoxLayout, QLabel
from legendary.core import LegendaryCore
from legendary.models.game import Game
from rare.components.tabs.games.game_widgets.base_uninstalled_widget import BaseUninstalledWidget
from rare.components.tabs.games.game_widgets.base_uninstalled_widget import (
BaseUninstalledWidget,
)
logger = getLogger("Uninstalled")
class IconWidgetUninstalled(BaseUninstalledWidget):
def __init__(self, game: Game, core: LegendaryCore, pixmap):
super(IconWidgetUninstalled, self).__init__(game, core, pixmap)
self.layout = QVBoxLayout()

View file

@ -4,13 +4,14 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QPushButton
from legendary.core import LegendaryCore
from rare.components.tabs.games.game_widgets.base_uninstalled_widget import BaseUninstalledWidget
from rare.components.tabs.games.game_widgets.base_uninstalled_widget import (
BaseUninstalledWidget,
)
logger = getLogger("Game")
class ListWidgetUninstalled(BaseUninstalledWidget):
def __init__(self, core: LegendaryCore, game, pixmap):
super(ListWidgetUninstalled, self).__init__(game, core, pixmap)
self.layout = QHBoxLayout()

View file

@ -1,5 +1,12 @@
from PyQt5.QtCore import QSize, QSettings, pyqtSignal
from PyQt5.QtWidgets import QLineEdit, QLabel, QPushButton, QWidget, QHBoxLayout, QComboBox
from PyQt5.QtWidgets import (
QLineEdit,
QLabel,
QPushButton,
QWidget,
QHBoxLayout,
QComboBox,
)
from qtawesome import icon
from rare.utils.extra_widgets import SelectViewWidget
@ -17,15 +24,26 @@ class GameListHeadBar(QWidget):
# self.layout.addWidget(self.installed_only)
self.filter = QComboBox()
self.filter.addItems([self.tr("All"),
self.tr("Installed only"),
self.tr("Offline Games"),
self.tr("32 Bit Games"),
self.tr("Mac games"),
self.tr("Exclude Origin")])
self.filter.addItems(
[
self.tr("All"),
self.tr("Installed only"),
self.tr("Offline Games"),
self.tr("32 Bit Games"),
self.tr("Mac games"),
self.tr("Exclude Origin"),
]
)
self.layout().addWidget(self.filter)
self.available_filters = ["all", "installed", "offline", "32bit", "mac", "installable"]
self.available_filters = [
"all",
"installed",
"offline",
"32bit",
"mac",
"installable",
]
try:
self.filter.setCurrentIndex(self.settings.value("filter", 0, int))

View file

@ -6,22 +6,21 @@ from .import_group import ImportGroup
class ImportSyncTabs(SideTabWidget):
def __init__(self, parent=None):
super(ImportSyncTabs, self).__init__(show_back=True, parent=parent)
self.import_widget = ImportSyncWidget(
ImportGroup(self),
self.tr('Import Game'),
self.tr('To import games from Epic Games Store, please enable EGL Sync.'),
self
self.tr("Import Game"),
self.tr("To import games from Epic Games Store, please enable EGL Sync."),
self,
)
self.addTab(self.import_widget, self.tr("Import Games"))
self.egl_sync_widget = ImportSyncWidget(
EGLSyncGroup(self),
self.tr('Sync with EGL'),
self.tr('To import EGL games from directories, please use Import Game.'),
self
self.tr("Sync with EGL"),
self.tr("To import EGL games from directories, please use Import Game."),
self,
)
self.addTab(self.egl_sync_widget, self.tr("Sync with EGL"))
# FIXME: Until it is ready
@ -37,7 +36,6 @@ class ImportSyncTabs(SideTabWidget):
class ImportSyncWidget(QWidget):
def __init__(self, widget: QWidget, title: str, info: str, parent=None):
super(ImportSyncWidget, self).__init__(parent=parent)
self.layout = QVBoxLayout()
@ -48,5 +46,7 @@ class ImportSyncWidget(QWidget):
self.layout.addWidget(self.group)
self.info = QLabel(f"<h4>{info}</h4>")
self.layout.addWidget(self.info)
self.layout.addItem(QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))
self.layout.addItem(
QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
)
self.setLayout(self.layout)

View file

@ -8,7 +8,9 @@ from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox
import rare.shared as shared
from rare.ui.components.tabs.games.import_sync.egl_sync_group import Ui_EGLSyncGroup
from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import Ui_EGLSyncListGroup
from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import (
Ui_EGLSyncListGroup,
)
from rare.utils.extra_widgets import PathEdit
from rare.utils.models import PathSpec
from rare.utils.utils import WineResolver
@ -17,33 +19,36 @@ logger = getLogger("EGLSync")
class EGLSyncGroup(QGroupBox, Ui_EGLSyncGroup):
def __init__(self, parent=None):
super(EGLSyncGroup, self).__init__(parent=parent)
self.setupUi(self)
self.egl_path_info.setProperty('infoLabel', 1)
self.egl_path_info.setProperty("infoLabel", 1)
self.thread_pool = QThreadPool.globalInstance()
if platform.system() == 'Windows':
if platform.system() == "Windows":
self.egl_path_edit_label.setVisible(False)
self.egl_path_info_label.setVisible(False)
self.egl_path_info.setVisible(False)
else:
self.egl_path_edit = PathEdit(
path=shared.core.egl.programdata_path,
ph_text=self.tr('Path to the Wine prefix where EGL is installed, or the Manifests folder'),
ph_text=self.tr(
"Path to the Wine prefix where EGL is installed, or the Manifests folder"
),
file_type=QFileDialog.DirectoryOnly,
edit_func=self.egl_path_edit_edit_cb,
save_func=self.egl_path_edit_save_cb,
parent=self
parent=self,
)
self.egl_path_edit.textChanged.connect(self.egl_path_changed)
self.egl_path_edit_layout.addWidget(self.egl_path_edit)
if not shared.core.egl.programdata_path:
self.egl_path_info.setText(self.tr('Updating...'))
wine_resolver = WineResolver(PathSpec.egl_programdata, 'default', shared.core)
self.egl_path_info.setText(self.tr("Updating..."))
wine_resolver = WineResolver(
PathSpec.egl_programdata, "default", shared.core
)
wine_resolver.signals.result_ready.connect(self.wine_resolver_cb)
self.thread_pool.start(wine_resolver)
else:
@ -67,12 +72,18 @@ class EGLSyncGroup(QGroupBox, Ui_EGLSyncGroup):
self.egl_path_info.setText(path)
if not path:
self.egl_path_info.setText(
self.tr('Default Wine prefix is unset, or path does not exist. '
'Create it or configure it in Settings -> Linux.'))
self.tr(
"Default Wine prefix is unset, or path does not exist. "
"Create it or configure it in Settings -> Linux."
)
)
elif not os.path.exists(path):
self.egl_path_info.setText(
self.tr('Default Wine prefix is set but EGL manifests path does not exist. '
'Your configured default Wine prefix might not be where EGL is installed.'))
self.tr(
"Default Wine prefix is set but EGL manifests path does not exist. "
"Your configured default Wine prefix might not be where EGL is installed."
)
)
else:
self.egl_path_edit.setText(path)
@ -80,10 +91,18 @@ class EGLSyncGroup(QGroupBox, Ui_EGLSyncGroup):
def egl_path_edit_edit_cb(path) -> Tuple[bool, str]:
if not path:
return True, path
if os.path.exists(os.path.join(path, 'system.reg')) and os.path.exists(os.path.join(path, 'dosdevices/c:')):
if os.path.exists(os.path.join(path, "system.reg")) and os.path.exists(
os.path.join(path, "dosdevices/c:")
):
# path is a wine prefix
path = os.path.join(path, 'dosdevices/c:', 'ProgramData/Epic/EpicGamesLauncher/Data/Manifests')
elif not path.rstrip('/').endswith('ProgramData/Epic/EpicGamesLauncher/Data/Manifests'):
path = os.path.join(
path,
"dosdevices/c:",
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests",
)
elif not path.rstrip("/").endswith(
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
):
# lower() might or might not be needed in the check
return False, path
if os.path.exists(path):
@ -95,11 +114,11 @@ class EGLSyncGroup(QGroupBox, Ui_EGLSyncGroup):
if not path or not os.path.exists(path):
# This is the same as "--unlink"
shared.core.egl.programdata_path = None
shared.core.lgd.config.remove_option('Legendary', 'egl_programdata')
shared.core.lgd.config.remove_option('Legendary', 'egl_sync')
shared.core.lgd.config.remove_option("Legendary", "egl_programdata")
shared.core.lgd.config.remove_option("Legendary", "egl_sync")
# remove EGL GUIDs from all games, DO NOT remove .egstore folders because that would fuck things up.
for igame in shared.core.get_installed_list():
igame.egl_guid = ''
igame.egl_guid = ""
shared.core.install_game(igame)
else:
shared.core.egl.programdata_path = path
@ -119,9 +138,9 @@ class EGLSyncGroup(QGroupBox, Ui_EGLSyncGroup):
if state == Qt.Unchecked:
self.import_list.setEnabled(bool(self.import_list.items))
self.export_list.setEnabled(bool(self.export_list.items))
shared.core.lgd.config.remove_option('Legendary', 'egl_sync')
shared.core.lgd.config.remove_option("Legendary", "egl_sync")
else:
shared.core.lgd.config.set('Legendary', 'egl_sync', str(True))
shared.core.lgd.config.set("Legendary", "egl_sync", str(True))
# lk: do import/export here since automatic sync was selected
self.import_list.mark(Qt.Checked)
self.export_list.mark(Qt.Checked)
@ -134,7 +153,9 @@ class EGLSyncGroup(QGroupBox, Ui_EGLSyncGroup):
def update_lists(self):
# self.egl_watcher.blockSignals(True)
if have_path := bool(shared.core.egl.programdata_path) and os.path.exists(shared.core.egl.programdata_path):
if have_path := bool(shared.core.egl.programdata_path) and os.path.exists(
shared.core.egl.programdata_path
):
# NOTE: need to clear known manifests to force refresh
shared.core.egl.manifests.clear()
self.egl_sync_check_label.setEnabled(have_path)
@ -178,7 +199,6 @@ class EGLSyncListItem(QListWidgetItem):
class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup):
def __init__(self, export: bool, parent=None):
super(EGLSyncListGroup, self).__init__(parent=parent)
self.setupUi(self)
@ -187,19 +207,20 @@ class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup):
self.export = export
if export:
self.setTitle(self.tr('Exportable games'))
self.label.setText(self.tr('No games to export to EGL'))
self.action_button.setText(self.tr('Export'))
self.setTitle(self.tr("Exportable games"))
self.label.setText(self.tr("No games to export to EGL"))
self.action_button.setText(self.tr("Export"))
self.list_func = shared.core.egl_get_exportable
else:
self.setTitle(self.tr('Importable games'))
self.label.setText(self.tr('No games to import from EGL'))
self.action_button.setText(self.tr('Import'))
self.setTitle(self.tr("Importable games"))
self.label.setText(self.tr("No games to import from EGL"))
self.action_button.setText(self.tr("Import"))
self.list_func = shared.core.egl_get_importable
self.list.itemDoubleClicked.connect(
lambda item:
item.setCheckState(Qt.Unchecked) if item.checkState() != Qt.Unchecked else item.setCheckState(Qt.Checked)
lambda item: item.setCheckState(Qt.Unchecked)
if item.checkState() != Qt.Unchecked
else item.setCheckState(Qt.Checked)
)
self.list.itemChanged.connect(self.has_selected)
@ -244,9 +265,10 @@ class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup):
if errors:
QMessageBox.warning(
self.parent(),
self.tr('The following errors occurred while {}.').format(
self.tr('exporting') if self.export else self.tr('importing')),
'\n'.join(errors)
self.tr("The following errors occurred while {}.").format(
self.tr("exporting") if self.export else self.tr("importing")
),
"\n".join(errors),
)
@property
@ -269,7 +291,7 @@ class EGLSyncWorker(QRunnable):
self.export_list.action()
'''
"""
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QCheckBox, QPushButton, QDialog
@ -348,4 +370,4 @@ class EGLSyncItemWidget(QGroupBox):
# FIXME: on update_egl_widget this is going to crash because
# FIXME: the item is not removed from the list in the python's side
self.deleteLater()
'''
"""

View file

@ -21,7 +21,9 @@ class AppNameCompleter(QCompleter):
def __init__(self, app_names: List, parent=None):
super(AppNameCompleter, self).__init__(parent)
# pylint: disable=E1136
super(AppNameCompleter, self).activated[QModelIndex].connect(self.__activated_idx)
super(AppNameCompleter, self).activated[QModelIndex].connect(
self.__activated_idx
)
model = QStandardItemModel(len(app_names), 2)
for idx, game in enumerate(app_names):
@ -51,9 +53,7 @@ class AppNameCompleter(QCompleter):
# lk: Getting the index from the popup and trying to use it in the completer will return invalid results
if isinstance(idx, QModelIndex):
self.activated.emit(
self.popup().model().data(
self.popup().model().index(idx.row(), 1)
)
self.popup().model().data(self.popup().model().index(idx.row(), 1))
)
# TODO: implement conversion from app_name to app_title (signal loop here)
# if isinstance(idx_str, str):
@ -67,23 +67,31 @@ class ImportGroup(QGroupBox, Ui_ImportGroup):
self.core = shared.core
self.app_name_list = [game.app_name for game in shared.api_results.game_list]
self.install_dir_list = [
game.metadata.get('customAttributes', {}).get('FolderName', {}).get('value', game.app_name)
for game in shared.api_results.game_list if not game.is_dlc]
game.metadata.get("customAttributes", {})
.get("FolderName", {})
.get("value", game.app_name)
for game in shared.api_results.game_list
if not game.is_dlc
]
self.path_edit = PathEdit(
self.core.get_default_install_dir(),
QFileDialog.DirectoryOnly,
edit_func=self.path_edit_cb,
parent=self
parent=self,
)
self.path_edit.textChanged.connect(self.path_changed)
self.path_edit_layout.addWidget(self.path_edit)
self.app_name = IndicatorLineEdit(
ph_text=self.tr("Use in case the app name was not found automatically"),
completer=AppNameCompleter(app_names=[(i.app_name, i.app_title) for i in shared.api_results.game_list]),
completer=AppNameCompleter(
app_names=[
(i.app_name, i.app_title) for i in shared.api_results.game_list
]
),
edit_func=self.app_name_edit_cb,
parent=self
parent=self,
)
self.app_name.textChanged.connect(self.app_name_changed)
self.app_name_layout.addWidget(self.app_name)
@ -144,18 +152,27 @@ class ImportGroup(QGroupBox, Ui_ImportGroup):
self.info_label.setText(self.tr("Could not find app name"))
return
if not (err := legendary_utils.import_game(self.core, app_name=app_name, path=path)):
if not (
err := legendary_utils.import_game(self.core, app_name=app_name, path=path)
):
igame = self.core.get_installed_game(app_name)
self.info_label.setText(self.tr("Successfully imported {}").format(igame.title))
self.info_label.setText(
self.tr("Successfully imported {}").format(igame.title)
)
self.app_name.setText(str())
shared.signals.update_gamelist.emit([app_name])
if igame.version != self.core.get_asset(app_name, igame.platform, False).build_version:
if (
igame.version
!= self.core.get_asset(app_name, igame.platform, False).build_version
):
# update available
shared.signals.add_download.emit(igame.app_name)
shared.signals.update_download_tab_text.emit()
else:
logger.warning(f'Failed to import "{app_name}"')
self.info_label.setText(self.tr("Could not import {}: ").format(app_name) + err)
self.info_label.setText(
self.tr("Could not import {}: ").format(app_name) + err
)
return

View file

@ -22,6 +22,8 @@ class SettingsTab(SideTabWidget):
self.about = About()
self.addTab(self.about, "About")
self.about.update_available_ready.connect(lambda: self.tabBar().setTabText(about_tab, "About (!)"))
self.about.update_available_ready.connect(
lambda: self.tabBar().setTabText(about_tab, "About (!)")
)
self.setCurrentIndex(0)

View file

@ -33,10 +33,14 @@ class About(QWidget, Ui_About):
self.open_browser.setVisible(False)
self.manager = QtRequestManager("json")
self.manager.get("https://api.github.com/repos/Dummerle/Rare/releases/latest", self.update_available_finished)
self.manager.get(
"https://api.github.com/repos/Dummerle/Rare/releases/latest",
self.update_available_finished,
)
self.open_browser.clicked.connect(
lambda: webbrowser.open("https://github.com/Dummerle/Rare/releases/latest"))
lambda: webbrowser.open("https://github.com/Dummerle/Rare/releases/latest")
)
def update_available_finished(self, data: dict):
if latest_tag := data.get("tag_name"):
@ -46,7 +50,9 @@ class About(QWidget, Ui_About):
if self.update_available:
logger.info(f"Update available: {__version__} -> {latest_tag}")
self.update_lbl.setText(self.tr("Update available: ") + f"{__version__} -> {latest_tag}")
self.update_lbl.setText(
self.tr("Update available: ") + f"{__version__} -> {latest_tag}"
)
self.update_label.setVisible(True)
self.update_lbl.setVisible(True)
self.open_browser.setVisible(True)

View file

@ -9,7 +9,6 @@ logger = getLogger("DXVK Settings")
class DxvkSettings(QGroupBox, Ui_DxvkSettings):
def __init__(self, name=None):
super(DxvkSettings, self).__init__()
self.setupUi(self)
@ -39,7 +38,9 @@ class DxvkSettings(QGroupBox, Ui_DxvkSettings):
# Custom Options, index 3, adds DXVK_HUD=devinfo,fps and enables the customization panel
def load_settings(self):
dxvk_options = self.core.lgd.config.get(f"{self.name}.env", "DXVK_HUD", fallback=None)
dxvk_options = self.core.lgd.config.get(
f"{self.name}.env", "DXVK_HUD", fallback=None
)
self.gb_dxvk_options.setDisabled(True)
if dxvk_options is not None:
if dxvk_options == "0":
@ -74,16 +75,23 @@ class DxvkSettings(QGroupBox, Ui_DxvkSettings):
dxvk_options.append(opt)
if not dxvk_options:
# Check if this is the first activation
stored = self.core.lgd.config.get(f"{self.name}.env", "DXVK_HUD", fallback=None)
stored = self.core.lgd.config.get(
f"{self.name}.env", "DXVK_HUD", fallback=None
)
if stored not in [None, "0", "1"]:
self.core.lgd.config[f"{self.name}.env"]["DXVK_HUD"] = "0"
else:
dxvk_options = ["devinfo", "fps"]
# Check again if dxvk_options changed due to first activation
if dxvk_options:
self.core.lgd.config[f"{self.name}.env"]["DXVK_HUD"] = ",".join(dxvk_options)
self.core.lgd.config[f"{self.name}.env"]["DXVK_HUD"] = ",".join(
dxvk_options
)
else:
if self.core.lgd.config.get(f"{self.name}.env", "DXVK_HUD", fallback=None) is not None:
if (
self.core.lgd.config.get(f"{self.name}.env", "DXVK_HUD", fallback=None)
is not None
):
self.core.lgd.config.remove_option(f"{self.name}.env", "DXVK_HUD")
if not self.core.lgd.config[f"{self.name}.env"]:
self.core.lgd.config.remove_section(f"{self.name}.env")

View file

@ -21,42 +21,46 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
self.core = shared.core
# Default installation directory
self.install_dir = PathEdit(self.core.get_default_install_dir(),
file_type=QFileDialog.DirectoryOnly,
save_func=self.path_save)
self.install_dir = PathEdit(
self.core.get_default_install_dir(),
file_type=QFileDialog.DirectoryOnly,
save_func=self.path_save,
)
self.install_dir_layout.addWidget(self.install_dir)
# Max Workers
max_workers = self.core.lgd.config['Legendary'].getint('max_workers', fallback=0)
max_workers = self.core.lgd.config["Legendary"].getint(
"max_workers", fallback=0
)
self.max_worker_spin.setValue(max_workers)
self.max_worker_spin.valueChanged.connect(self.max_worker_save)
# Max memory
max_memory = self.core.lgd.config['Legendary'].getint('max_memory', fallback=0)
max_memory = self.core.lgd.config["Legendary"].getint("max_memory", fallback=0)
self.max_memory_spin.setValue(max_memory)
self.max_memory_spin.valueChanged.connect(self.max_memory_save)
# Preferred CDN
preferred_cdn = self.core.lgd.config['Legendary'].get('preferred_cdn', fallback="")
preferred_cdn = self.core.lgd.config["Legendary"].get(
"preferred_cdn", fallback=""
)
self.preferred_cdn_line.setText(preferred_cdn)
self.preferred_cdn_line.textChanged.connect(self.preferred_cdn_save)
# Disable HTTPS
disable_https = self.core.lgd.config['Legendary'].getboolean('disable_https', fallback=False)
disable_https = self.core.lgd.config["Legendary"].getboolean(
"disable_https", fallback=False
)
self.disable_https_check.setChecked(disable_https)
self.disable_https_check.stateChanged.connect(self.disable_https_save)
# Cleanup
self.clean_button.clicked.connect(
lambda: self.cleanup(False)
)
self.clean_keep_manifests_button.clicked.connect(
lambda: self.cleanup(True)
)
self.clean_button.clicked.connect(lambda: self.cleanup(False))
self.clean_keep_manifests_button.clicked.connect(lambda: self.cleanup(True))
self.locale_edit = IndicatorLineEdit(
f"{self.core.language_code}-{self.core.country_code}",
edit_func=self.locale_edit_cb,
save_func=self.locale_save_cb,
horiz_policy=QSizePolicy.Minimum,
parent=self
parent=self,
)
self.locale_layout.addWidget(self.locale_edit)
@ -111,28 +115,41 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
self.core.lgd.save_config()
def disable_https_save(self, checked: int):
self.core.lgd.config.set("Legendary", "disable_https", str(bool(checked)).lower())
self.core.lgd.config.set(
"Legendary", "disable_https", str(bool(checked)).lower()
)
self.core.lgd.save_config()
def cleanup(self, keep_manifests: bool):
before = self.core.lgd.get_dir_size()
logger.debug('Removing app metadata...')
logger.debug("Removing app metadata...")
app_names = set(g.app_name for g in self.core.get_assets(update_assets=False))
self.core.lgd.clean_metadata(app_names)
if not keep_manifests:
logger.debug('Removing manifests...')
installed = [(ig.app_name, ig.version) for ig in self.core.get_installed_list()]
installed.extend((ig.app_name, ig.version) for ig in self.core.get_installed_dlc_list())
logger.debug("Removing manifests...")
installed = [
(ig.app_name, ig.version) for ig in self.core.get_installed_list()
]
installed.extend(
(ig.app_name, ig.version) for ig in self.core.get_installed_dlc_list()
)
self.core.lgd.clean_manifests(installed)
logger.debug('Removing tmp data')
logger.debug("Removing tmp data")
self.core.lgd.clean_tmp_data()
after = self.core.lgd.get_dir_size()
logger.info(f'Cleanup complete! Removed {(before - after) / 1024 / 1024:.02f} MiB.')
logger.info(
f"Cleanup complete! Removed {(before - after) / 1024 / 1024:.02f} MiB."
)
if (before - after) > 0:
QMessageBox.information(self, "Cleanup", self.tr("Cleanup complete! Successfully removed {}").format(
get_size(before - after)))
QMessageBox.information(
self,
"Cleanup",
self.tr("Cleanup complete! Successfully removed {}").format(
get_size(before - after)
),
)
else:
QMessageBox.information(self, "Cleanup", "Nothing to clean")

View file

@ -21,7 +21,8 @@ class LinuxSettings(QWidget, Ui_LinuxSettings):
self.wine_prefix = PathEdit(
self.load_prefix(),
file_type=QFileDialog.DirectoryOnly,
save_func=self.save_prefix)
save_func=self.save_prefix,
)
self.prefix_layout.addWidget(self.wine_prefix)
# Wine executable
@ -29,7 +30,10 @@ class LinuxSettings(QWidget, Ui_LinuxSettings):
self.load_setting(self.name, "wine_executable"),
file_type=QFileDialog.ExistingFile,
name_filter="Wine executable (wine wine64)",
save_func=lambda text: self.save_setting(text, section=self.name, setting="wine_executable"))
save_func=lambda text: self.save_setting(
text, section=self.name, setting="wine_executable"
),
)
self.exec_layout.addWidget(self.wine_exec)
# dxvk
@ -37,13 +41,15 @@ class LinuxSettings(QWidget, Ui_LinuxSettings):
self.dxvk_layout.addWidget(self.dxvk)
def load_prefix(self) -> str:
return self.load_setting(f'{self.name}.env',
'WINEPREFIX',
fallback=self.load_setting(self.name, 'wine_prefix'))
return self.load_setting(
f"{self.name}.env",
"WINEPREFIX",
fallback=self.load_setting(self.name, "wine_prefix"),
)
def save_prefix(self, text: str):
self.save_setting(text, f'{self.name}.env', 'WINEPREFIX')
self.save_setting(text, self.name, 'wine_prefix')
self.save_setting(text, f"{self.name}.env", "WINEPREFIX")
self.save_setting(text, self.name, "wine_prefix")
@staticmethod
def load_setting(section: str, setting: str, fallback: str = str()):
@ -59,7 +65,9 @@ class LinuxSettings(QWidget, Ui_LinuxSettings):
logger.debug(f"Set {setting} in {f'[{section}]'} to {text}")
else:
if shared.core.lgd.config.has_section(section) and shared.core.lgd.config.has_option(section, setting):
if shared.core.lgd.config.has_section(
section
) and shared.core.lgd.config.has_option(section, setting):
shared.core.lgd.config.remove_option(section, setting)
logger.debug(f"Unset {setting} from {f'[{section}]'}")
if not shared.core.lgd.config[section]:

View file

@ -11,15 +11,17 @@ from rare import cache_dir, shared
from rare.components.tabs.settings.rpc import RPCSettings
from rare.ui.components.tabs.settings.rare import Ui_RareSettings
from rare.utils import utils
from rare.utils.utils import get_translations, get_color_schemes, set_color_pallete, get_style_sheets, set_style_sheet
from rare.utils.utils import (
get_translations,
get_color_schemes,
set_color_pallete,
get_style_sheets,
set_style_sheet,
)
logger = getLogger("RareSettings")
languages = [
("en", "English"),
("de", "Deutsch"),
("fr", "Français")
]
languages = [("en", "English"), ("de", "Deutsch"), ("fr", "Français")]
class RareSettings(QWidget, Ui_RareSettings):
@ -84,27 +86,35 @@ class RareSettings(QWidget, Ui_RareSettings):
lambda: self.settings.setValue("auto_update", self.auto_update.isChecked())
)
self.confirm_start.stateChanged.connect(
lambda: self.settings.setValue("confirm_start", self.confirm_start.isChecked())
lambda: self.settings.setValue(
"confirm_start", self.confirm_start.isChecked()
)
)
self.auto_sync_cloud.stateChanged.connect(
lambda: self.settings.setValue("auto_sync_cloud", self.auto_sync_cloud.isChecked())
lambda: self.settings.setValue(
"auto_sync_cloud", self.auto_sync_cloud.isChecked()
)
)
self.notification.stateChanged.connect(
lambda: self.settings.setValue("notification", self.notification.isChecked())
)
self.save_size.stateChanged.connect(
self.save_window_size
lambda: self.settings.setValue(
"notification", self.notification.isChecked()
)
)
self.save_size.stateChanged.connect(self.save_window_size)
self.log_games.stateChanged.connect(
lambda: self.settings.setValue("show_console", self.log_games.isChecked())
)
if platform.system() == "Linux":
self.desktop_file = os.path.expanduser("~/Desktop/Rare.desktop")
self.start_menu_link = os.path.expanduser("~/.local/share/applications/Rare.desktop")
self.start_menu_link = os.path.expanduser(
"~/.local/share/applications/Rare.desktop"
)
elif platform.system() == "Windows":
self.desktop_file = os.path.expanduser("~/Desktop/Rare.lnk")
self.start_menu_link = os.path.expandvars("%appdata%\\Microsoft\\Windows\\Start Menu\\Rare.lnk")
self.start_menu_link = os.path.expandvars(
"%appdata%\\Microsoft\\Windows\\Start Menu\\Rare.lnk"
)
else:
self.desktop_link_btn.setText(self.tr("Not supported"))
self.desktop_link_btn.setDisabled(True)
@ -151,7 +161,11 @@ class RareSettings(QWidget, Ui_RareSettings):
self.startmenu_link_btn.setText(self.tr("Create start menu link"))
except PermissionError as e:
logger.error(str(e))
QMessageBox.warning(self, "Error", "Permission error, cannot remove " + str(self.start_menu_link))
QMessageBox.warning(
self,
"Error",
"Permission error, cannot remove " + str(self.start_menu_link),
)
def create_desktop_link(self):
try:
@ -162,7 +176,11 @@ class RareSettings(QWidget, Ui_RareSettings):
os.remove(self.desktop_file)
self.desktop_link_btn.setText(self.tr("Create desktop link"))
except PermissionError as e:
logger.warning(self, "Error", "Permission error, cannot remove " + str(self.desktop_file))
logger.warning(
self,
"Error",
"Permission error, cannot remove " + str(self.desktop_file),
)
def on_color_select_changed(self, color):
if color:

View file

@ -27,20 +27,20 @@ class UbiGetInfoWorker(QRunnable):
try:
external_auths = shared.core.egs.get_external_auths()
for ext_auth in external_auths:
if ext_auth['type'] != 'ubisoft':
if ext_auth["type"] != "ubisoft":
continue
ubi_account_id = ext_auth['externalAuthId']
ubi_account_id = ext_auth["externalAuthId"]
break
else:
self.signals.worker_finished.emit(set(), set(), "")
return
uplay_keys = shared.core.egs.store_get_uplay_codes()
key_list = uplay_keys['data']['PartnerIntegration']['accountUplayCodes']
redeemed = {k['gameId'] for k in key_list if k['redeemedOnUplay']}
key_list = uplay_keys["data"]["PartnerIntegration"]["accountUplayCodes"]
redeemed = {k["gameId"] for k in key_list if k["redeemedOnUplay"]}
entitlements = shared.core.egs.get_user_entitlements()
entitlements = {i['entitlementName'] for i in entitlements}
entitlements = {i["entitlementName"] for i in entitlements}
self.signals.worker_finished.emit(redeemed, entitlements, ubi_account_id)
except Exception as e:
logger.error(str(e))
@ -61,7 +61,9 @@ class UbiConnectWorker(QRunnable):
self.signals.linked.emit("")
return
try:
shared.core.egs.store_claim_uplay_code(self.ubi_account_id, self.partner_link_id)
shared.core.egs.store_claim_uplay_code(
self.ubi_account_id, self.partner_link_id
)
shared.core.egs.store_redeem_uplay_codes(self.ubi_account_id)
except Exception as e:
self.signals.linked.emit(str(e))
@ -85,14 +87,18 @@ class UbiLinkWidget(QWidget):
self.ok_indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
self.layout().addWidget(self.ok_indicator)
self.link_button = QPushButton(self.tr("Redeem to Ubisoft") + ": Test" if shared.args.debug else "")
self.link_button = QPushButton(
self.tr("Redeem to Ubisoft") + ": Test" if shared.args.debug else ""
)
self.layout().addWidget(self.link_button)
self.link_button.clicked.connect(self.activate)
def activate(self):
self.link_button.setDisabled(True)
# self.ok_indicator.setPixmap(icon("mdi.loading", color="grey").pixmap(20, 20))
self.ok_indicator.setPixmap(icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20))
self.ok_indicator.setPixmap(
icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20)
)
if shared.args.debug:
worker = UbiConnectWorker(None, None)
@ -103,11 +109,15 @@ class UbiLinkWidget(QWidget):
def worker_finished(self, error):
if not error:
self.ok_indicator.setPixmap(icon("ei.ok-circle", color="green").pixmap(QSize(20, 20)))
self.ok_indicator.setPixmap(
icon("ei.ok-circle", color="green").pixmap(QSize(20, 20))
)
self.link_button.setDisabled(True)
self.link_button.setText(self.tr("Already activated"))
else:
self.ok_indicator.setPixmap(icon("fa.info-circle", color="red").pixmap(QSize(20, 20)))
self.ok_indicator.setPixmap(
icon("fa.info-circle", color="red").pixmap(QSize(20, 20))
)
self.ok_indicator.setToolTip(error)
self.link_button.setText(self.tr("Try again"))
self.link_button.setDisabled(False)
@ -126,11 +136,20 @@ class UbiActivationHelper(QObject):
def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
if not redeemed and ubi_account_id != "error":
logger.error('No linked ubisoft account found! Link your accounts via your browser and try again.')
logger.error(
"No linked ubisoft account found! Link your accounts via your browser and try again."
)
self.widget.layout().addWidget(
QLabel(self.tr("Your account is not linked with Ubisoft. Please link your account first")))
QLabel(
self.tr(
"Your account is not linked with Ubisoft. Please link your account first"
)
)
)
open_browser_button = QPushButton(self.tr("Open link page"))
open_browser_button.clicked.connect(lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft"))
open_browser_button.clicked.connect(
lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft")
)
self.widget.layout().addWidget(open_browser_button)
return
elif ubi_account_id == "error":
@ -141,17 +160,19 @@ class UbiActivationHelper(QObject):
uplay_games = []
activated = 0
for game in games:
for dlc_data in game.metadata.get('dlcItemList', []):
if dlc_data['entitlementName'] not in entitlements:
for dlc_data in game.metadata.get("dlcItemList", []):
if dlc_data["entitlementName"] not in entitlements:
continue
try:
app_name = dlc_data['releaseInfo'][0]['appId']
app_name = dlc_data["releaseInfo"][0]["appId"]
except (IndexError, KeyError):
app_name = 'unknown'
app_name = "unknown"
dlc_game = Game(app_name=app_name, app_title=dlc_data['title'], metadata=dlc_data)
if dlc_game.partner_link_type != 'ubisoft':
dlc_game = Game(
app_name=app_name, app_title=dlc_data["title"], metadata=dlc_data
)
if dlc_game.partner_link_type != "ubisoft":
continue
if dlc_game.partner_link_id in redeemed:
continue
@ -167,14 +188,22 @@ class UbiActivationHelper(QObject):
if not uplay_games:
if activated >= 1:
self.widget.layout().addWidget(
QLabel(self.tr("All your Ubisoft games have already been activated")))
QLabel(
self.tr("All your Ubisoft games have already been activated")
)
)
else:
self.widget.layout().addWidget(QLabel(self.tr("You don't own any Ubisoft games")))
self.widget.layout().addWidget(
QLabel(self.tr("You don't own any Ubisoft games"))
)
if shared.args.debug:
widget = UbiLinkWidget(Game(app_name="Test", app_title="This is a test game"), ubi_account_id)
widget = UbiLinkWidget(
Game(app_name="Test", app_title="This is a test game"),
ubi_account_id,
)
self.widget.layout().addWidget(widget)
return
logger.info(f'Found {len(uplay_games)} game(s) to redeem')
logger.info(f"Found {len(uplay_games)} game(s) to redeem")
for game in uplay_games:
widget = UbiLinkWidget(game, ubi_account_id)

View file

@ -15,8 +15,11 @@ class Shop(QStackedWidget):
def __init__(self, core: LegendaryCore):
super(Shop, self).__init__()
self.core = core
self.api_core = ShopApiCore(self.core.egs.session.headers["Authorization"], self.core.language_code,
self.core.country_code)
self.api_core = ShopApiCore(
self.core.egs.session.headers["Authorization"],
self.core.language_code,
self.core.country_code,
)
self.shop = ShopWidget(cache_dir, core, self.api_core)
self.wishlist_widget = Wishlist(self.api_core)
@ -30,7 +33,10 @@ class Shop(QStackedWidget):
self.search_results = SearchResults(self.api_core)
self.addWidget(self.search_results)
self.search_results.show_info.connect(self.show_game_info)
self.info = ShopGameInfo([i.asset_infos["Windows"].namespace for i in shared.api_results.game_list], self.api_core)
self.info = ShopGameInfo(
[i.asset_infos["Windows"].namespace for i in shared.api_results.game_list],
self.api_core,
)
self.addWidget(self.info)
self.info.back_button.clicked.connect(lambda: self.setCurrentIndex(0))

View file

@ -5,21 +5,24 @@ from PyQt5.QtCore import QObject
class Constants(QObject):
def __init__(self):
super(Constants, self).__init__()
self.categories = sorted([
(self.tr("Action"), "1216"),
(self.tr("Adventure"), "1117"),
(self.tr("Puzzle"), "1298"),
(self.tr("Open world"), "1307"),
(self.tr("Racing"), "1212"),
(self.tr("RPG"), "1367"),
(self.tr("Shooter"), "1210"),
(self.tr("Strategy"), "1115"),
(self.tr("Survival"), "1080"),
(self.tr("First Person"), "1294"),
(self.tr("Indie"), "1263"),
(self.tr("Simulation"), "1393"),
(self.tr("Sport"), "1283")
], key=lambda x: x[0])
self.categories = sorted(
[
(self.tr("Action"), "1216"),
(self.tr("Adventure"), "1117"),
(self.tr("Puzzle"), "1298"),
(self.tr("Open world"), "1307"),
(self.tr("Racing"), "1212"),
(self.tr("RPG"), "1367"),
(self.tr("Shooter"), "1210"),
(self.tr("Strategy"), "1115"),
(self.tr("Survival"), "1080"),
(self.tr("First Person"), "1294"),
(self.tr("Indie"), "1263"),
(self.tr("Simulation"), "1393"),
(self.tr("Sport"), "1283"),
],
key=lambda x: x[0],
)
self.platforms = [
("MacOS", "9548"),
@ -37,74 +40,78 @@ class Constants(QObject):
(self.tr("Game"), "games/edition/base"),
(self.tr("Bundle"), "bundles/games"),
(self.tr("Add-on"), "addons"),
(self.tr("Apps"), "software/edition/base")
(self.tr("Apps"), "software/edition/base"),
]
game_query = "query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, " \
"$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, " \
"$sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean " \
"= false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, " \
"$effectiveDate: String) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n " \
"category: $category\n count: $count\n country: $country\n keywords: $keywords\n " \
"locale: $locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n " \
"sortDir: $sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n " \
"priceRange: $priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: " \
"$effectiveDate\n ) {\n elements {\n title\n id\n namespace\n " \
"description\n effectiveDate\n keyImages {\n type\n url\n }\n " \
" currentPrice\n seller {\n id\n name\n }\n productSlug\n " \
" urlSlug\n url\n tags {\n id\n }\n items {\n id\n " \
" namespace\n }\n customAttributes {\n key\n value\n }\n " \
"categories {\n path\n }\n catalogNs @include(if: $withMapping) {\n " \
"mappings(pageType: \"productHome\") {\n pageSlug\n pageType\n }\n " \
"}\n offerMappings @include(if: $withMapping) {\n pageSlug\n pageType\n " \
"}\n price(country: $country) @include(if: $withPrice) {\n totalPrice {\n " \
"discountPrice\n originalPrice\n voucherDiscount\n discount\n " \
" currencyCode\n currencyInfo {\n decimals\n }\n fmtPrice(" \
"locale: $locale) {\n originalPrice\n discountPrice\n " \
"intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n " \
" id\n endDate\n discountSetting {\n discountType\n " \
" }\n }\n }\n }\n promotions(category: $category) @include(if: " \
"$withPromotions) {\n promotionalOffers {\n promotionalOffers {\n " \
"startDate\n endDate\n discountSetting {\n discountType\n " \
" discountPercentage\n }\n }\n }\n " \
"upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n " \
"endDate\n discountSetting {\n discountType\n " \
"discountPercentage\n }\n }\n }\n }\n }\n paging {\n " \
" count\n total\n }\n }\n }\n}\n "
game_query = (
"query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, "
"$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, "
"$sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean "
"= false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, "
"$effectiveDate: String) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n "
"category: $category\n count: $count\n country: $country\n keywords: $keywords\n "
"locale: $locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n "
"sortDir: $sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n "
"priceRange: $priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: "
"$effectiveDate\n ) {\n elements {\n title\n id\n namespace\n "
"description\n effectiveDate\n keyImages {\n type\n url\n }\n "
" currentPrice\n seller {\n id\n name\n }\n productSlug\n "
" urlSlug\n url\n tags {\n id\n }\n items {\n id\n "
" namespace\n }\n customAttributes {\n key\n value\n }\n "
"categories {\n path\n }\n catalogNs @include(if: $withMapping) {\n "
'mappings(pageType: "productHome") {\n pageSlug\n pageType\n }\n '
"}\n offerMappings @include(if: $withMapping) {\n pageSlug\n pageType\n "
"}\n price(country: $country) @include(if: $withPrice) {\n totalPrice {\n "
"discountPrice\n originalPrice\n voucherDiscount\n discount\n "
" currencyCode\n currencyInfo {\n decimals\n }\n fmtPrice("
"locale: $locale) {\n originalPrice\n discountPrice\n "
"intermediatePrice\n }\n }\n lineOffers {\n appliedRules {\n "
" id\n endDate\n discountSetting {\n discountType\n "
" }\n }\n }\n }\n promotions(category: $category) @include(if: "
"$withPromotions) {\n promotionalOffers {\n promotionalOffers {\n "
"startDate\n endDate\n discountSetting {\n discountType\n "
" discountPercentage\n }\n }\n }\n "
"upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n "
"endDate\n discountSetting {\n discountType\n "
"discountPercentage\n }\n }\n }\n }\n }\n paging {\n "
" count\n total\n }\n }\n }\n}\n "
)
search_query = "query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, " \
"$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, " \
"$sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = " \
"false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, " \
"$effectiveDate: String) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n " \
"category: $category\n count: $count\n country: $country\n keywords: $keywords\n locale: " \
"$locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n sortDir: " \
"$sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n priceRange: " \
"$priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: $effectiveDate\n ) {" \
"\n elements {\n title\n id\n namespace\n description\n " \
"effectiveDate\n keyImages {\n type\n url\n }\n currentPrice\n " \
"seller {\n id\n name\n }\n productSlug\n urlSlug\n url\n " \
" tags {\n id\n }\n items {\n id\n namespace\n }\n " \
"customAttributes {\n key\n value\n }\n categories {\n path\n " \
"}\n catalogNs @include(if: $withMapping) {\n mappings(pageType: \"productHome\") {\n " \
" pageSlug\n pageType\n }\n }\n offerMappings @include(if: $withMapping) " \
"{\n pageSlug\n pageType\n }\n price(country: $country) @include(if: " \
"$withPrice) {\n totalPrice {\n discountPrice\n originalPrice\n " \
"voucherDiscount\n discount\n currencyCode\n currencyInfo {\n " \
"decimals\n }\n fmtPrice(locale: $locale) {\n originalPrice\n " \
"discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n " \
" appliedRules {\n id\n endDate\n discountSetting {\n " \
"discountType\n }\n }\n }\n }\n promotions(category: " \
"$category) @include(if: $withPromotions) {\n promotionalOffers {\n promotionalOffers {\n " \
" startDate\n endDate\n discountSetting {\n " \
"discountType\n discountPercentage\n }\n }\n }\n " \
"upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n " \
"endDate\n discountSetting {\n discountType\n discountPercentage\n " \
" }\n }\n }\n }\n }\n paging {\n count\n " \
"total\n }\n }\n }\n}\n "
search_query = (
"query searchStoreQuery($allowCountries: String, $category: String, $count: Int, $country: String!, "
"$keywords: String, $locale: String, $namespace: String, $withMapping: Boolean = false, $itemNs: String, "
"$sortBy: String, $sortDir: String, $start: Int, $tag: String, $releaseDate: String, $withPrice: Boolean = "
"false, $withPromotions: Boolean = false, $priceRange: String, $freeGame: Boolean, $onSale: Boolean, "
"$effectiveDate: String) {\n Catalog {\n searchStore(\n allowCountries: $allowCountries\n "
"category: $category\n count: $count\n country: $country\n keywords: $keywords\n locale: "
"$locale\n namespace: $namespace\n itemNs: $itemNs\n sortBy: $sortBy\n sortDir: "
"$sortDir\n releaseDate: $releaseDate\n start: $start\n tag: $tag\n priceRange: "
"$priceRange\n freeGame: $freeGame\n onSale: $onSale\n effectiveDate: $effectiveDate\n ) {"
"\n elements {\n title\n id\n namespace\n description\n "
"effectiveDate\n keyImages {\n type\n url\n }\n currentPrice\n "
"seller {\n id\n name\n }\n productSlug\n urlSlug\n url\n "
" tags {\n id\n }\n items {\n id\n namespace\n }\n "
"customAttributes {\n key\n value\n }\n categories {\n path\n "
'}\n catalogNs @include(if: $withMapping) {\n mappings(pageType: "productHome") {\n '
" pageSlug\n pageType\n }\n }\n offerMappings @include(if: $withMapping) "
"{\n pageSlug\n pageType\n }\n price(country: $country) @include(if: "
"$withPrice) {\n totalPrice {\n discountPrice\n originalPrice\n "
"voucherDiscount\n discount\n currencyCode\n currencyInfo {\n "
"decimals\n }\n fmtPrice(locale: $locale) {\n originalPrice\n "
"discountPrice\n intermediatePrice\n }\n }\n lineOffers {\n "
" appliedRules {\n id\n endDate\n discountSetting {\n "
"discountType\n }\n }\n }\n }\n promotions(category: "
"$category) @include(if: $withPromotions) {\n promotionalOffers {\n promotionalOffers {\n "
" startDate\n endDate\n discountSetting {\n "
"discountType\n discountPercentage\n }\n }\n }\n "
"upcomingPromotionalOffers {\n promotionalOffers {\n startDate\n "
"endDate\n discountSetting {\n discountType\n discountPercentage\n "
" }\n }\n }\n }\n }\n paging {\n count\n "
"total\n }\n }\n }\n}\n "
)
wishlist_query = "\n query wishlistQuery($country:String!, $locale:String) {\n Wishlist {\n wishlistItems {\n elements {\n id\n order\n created\n offerId\n updated\n namespace\n \n offer {\n productSlug\n urlSlug\n title\n id\n namespace\n offerType\n expiryDate\n status\n isCodeRedemptionOnly\n description\n effectiveDate\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n productSlug\n urlSlug\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n catalogNs {\n mappings(pageType: \"productHome\") {\n pageSlug\n pageType\n }\n }\n offerMappings {\n pageSlug\n pageType\n }\n categories {\n path\n }\n price(country: $country) {\n totalPrice {\n discountPrice\n originalPrice\n voucherDiscount\n discount\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n currencyCode\n currencyInfo {\n decimals\n symbol\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n }\n }\n }\n }\n\n }\n }\n }\n }\n"
wishlist_query = '\n query wishlistQuery($country:String!, $locale:String) {\n Wishlist {\n wishlistItems {\n elements {\n id\n order\n created\n offerId\n updated\n namespace\n \n offer {\n productSlug\n urlSlug\n title\n id\n namespace\n offerType\n expiryDate\n status\n isCodeRedemptionOnly\n description\n effectiveDate\n keyImages {\n type\n url\n }\n seller {\n id\n name\n }\n productSlug\n urlSlug\n items {\n id\n namespace\n }\n customAttributes {\n key\n value\n }\n catalogNs {\n mappings(pageType: "productHome") {\n pageSlug\n pageType\n }\n }\n offerMappings {\n pageSlug\n pageType\n }\n categories {\n path\n }\n price(country: $country) {\n totalPrice {\n discountPrice\n originalPrice\n voucherDiscount\n discount\n fmtPrice(locale: $locale) {\n originalPrice\n discountPrice\n intermediatePrice\n }\n currencyCode\n currencyInfo {\n decimals\n symbol\n }\n }\n lineOffers {\n appliedRules {\n id\n endDate\n }\n }\n }\n }\n\n }\n }\n }\n }\n'
add_to_wishlist_query = "\n mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) {\n Wishlist {\n removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) {\n success\n }\n }\n }\n"
remove_from_wishlist_query = "\n mutation removeFromWishlistMutation($namespace: String!, $offerId: String!, $operation: RemoveOperation!) {\n Wishlist {\n removeFromWishlist(namespace: $namespace, offerId: $offerId, operation: $operation) {\n success\n }\n }\n }\n"
coupon_query = "\n query getCoupons($currencyCountry: String!, $identityId: String!, $locale: String) {\n CodeRedemption {\n coupons(currencyCountry: $currencyCountry, identityId: $identityId, includeSalesEventInfo: true) {\n code\n codeStatus\n codeType\n consumptionMetadata {\n amountDisplay {\n amount\n currency\n placement\n symbol\n }\n minSalesPriceDisplay {\n amount\n currency\n placement\n symbol\n }\n }\n endDate\n namespace\n salesEvent(locale: $locale) {\n eventName\n eventSlug\n voucherImages {\n type\n url\n }\n voucherLink\n }\n startDate\n }\n }\n }\n"

View file

@ -3,7 +3,16 @@ import webbrowser
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap, QFont
from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QHBoxLayout, QSpacerItem, QGroupBox, QTabWidget, QGridLayout
from PyQt5.QtWidgets import (
QWidget,
QLabel,
QPushButton,
QHBoxLayout,
QSpacerItem,
QGroupBox,
QTabWidget,
QGridLayout,
)
from qtawesome import icon
from rare import shared
@ -29,7 +38,9 @@ class ShopGameInfo(QWidget, Ui_shop_info):
self.image_stack.addWidget(self.image)
self.image_stack.addWidget(WaitingSpinner())
warn_label = QLabel()
warn_label.setPixmap(icon("fa.warning").pixmap(160, 160).scaled(240, 320, Qt.IgnoreAspectRatio))
warn_label.setPixmap(
icon("fa.warning").pixmap(160, 160).scaled(240, 320, Qt.IgnoreAspectRatio)
)
self.image_stack.addWidget(warn_label)
self.wishlist_button.clicked.connect(self.add_to_wishlist)
@ -101,9 +112,13 @@ class ShopGameInfo(QWidget, Ui_shop_info):
# lambda success: self.wishlist_button.setText(self.tr("Remove from wishlist"))
# if success else self.wishlist_button.setText("Something goes wrong"))
else:
self.api_core.remove_from_wishlist(self.game.namespace, self.game.offer_id,
lambda success: self.wishlist_button.setVisible(False)
if success else self.wishlist_button.setText("Something goes wrong"))
self.api_core.remove_from_wishlist(
self.game.namespace,
self.game.offer_id,
lambda success: self.wishlist_button.setVisible(False)
if success
else self.wishlist_button.setText("Something goes wrong"),
)
def data_received(self, game):
try:
@ -113,7 +128,12 @@ class ShopGameInfo(QWidget, Ui_shop_info):
self.price.setText("Error")
self.req_group_box.setVisible(False)
for img in self.data.get("keyImages"):
if img["type"] in ["DieselStoreFrontWide", "OfferImageTall", "VaultClosed", "ProductLogo"]:
if img["type"] in [
"DieselStoreFrontWide",
"OfferImageTall",
"VaultClosed",
"ProductLogo",
]:
self.image.update_image(img["url"], size=(240, 320))
self.image_stack.setCurrentIndex(0)
break
@ -137,7 +157,10 @@ class ShopGameInfo(QWidget, Ui_shop_info):
font.setStrikeOut(True)
self.price.setFont(font)
self.discount_price.setText(
self.game.discount_price if self.game.discount_price != "0" else self.tr("Free"))
self.game.discount_price
if self.game.discount_price != "0"
else self.tr("Free")
)
self.discount_price.setVisible(True)
else:
self.discount_price.setVisible(False)
@ -156,7 +179,9 @@ class ShopGameInfo(QWidget, Ui_shop_info):
req_widget.setLayout(QGridLayout())
req_widget.layout().addWidget(min_label, 0, 1)
req_widget.layout().addWidget(rec_label, 0, 2)
for i, (key, value) in enumerate(self.game.reqs.get(system, {}).items()):
for i, (key, value) in enumerate(
self.game.reqs.get(system, {}).items()
):
req_widget.layout().addWidget(QLabel(key), i + 1, 0)
min_label = QLabel(value[0])
min_label.setWordWrap(True)
@ -167,7 +192,9 @@ class ShopGameInfo(QWidget, Ui_shop_info):
req_tabs.addTab(req_widget, system)
self.req_group_box.layout().addWidget(req_tabs)
else:
self.req_group_box.layout().addWidget(QLabel(self.tr("Could not get requirements")))
self.req_group_box.layout().addWidget(
QLabel(self.tr("Could not get requirements"))
)
self.req_group_box.setVisible(True)
if self.game.image_urls.front_tall:
img_url = self.game.image_urls.front_tall
@ -190,7 +217,10 @@ class ShopGameInfo(QWidget, Ui_shop_info):
self.tags.setText(", ".join(self.game.tags))
# clear Layout
for widget in (self.social_link_gb.layout().itemAt(i) for i in range(self.social_link_gb.layout().count())):
for widget in (
self.social_link_gb.layout().itemAt(i)
for i in range(self.social_link_gb.layout().count())
):
if not isinstance(widget, QSpacerItem):
widget.widget().deleteLater()
self.social_link_gb.deleteLater()
@ -229,7 +259,10 @@ class ShopGameInfo(QWidget, Ui_shop_info):
self.wishlist.append(game["offer"]["title"])
def button_clicked(self):
webbrowser.open(f"https://www.epicgames.com/store/{shared.core.language_code}/p/" + self.slug)
webbrowser.open(
f"https://www.epicgames.com/store/{shared.core.language_code}/p/"
+ self.slug
)
class SocialButton(QPushButton):

View file

@ -42,14 +42,16 @@ class GameWidget(QWidget):
mini_layout.addWidget(self.title_label)
mini_layout.addStretch(1)
price = json_info['price']['totalPrice']['fmtPrice']['originalPrice']
discount_price = json_info['price']['totalPrice']['fmtPrice']['discountPrice']
price = json_info["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
discount_price = json_info["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
price_label = QLabel(price)
if price != discount_price:
font = QFont()
font.setStrikeOut(True)
price_label.setFont(font)
mini_layout.addWidget(QLabel(discount_price if discount_price != "0" else self.tr("Free")))
mini_layout.addWidget(
QLabel(discount_price if discount_price != "0" else self.tr("Free"))
)
mini_layout.addWidget(price_label)
else:
if price == "0":
@ -64,10 +66,19 @@ class GameWidget(QWidget):
self.title = json_info["title"]
for img in json_info["keyImages"]:
if img["type"] in ["DieselStoreFrontWide", "OfferImageWide", "VaultClosed", "ProductLogo"]:
if img["type"] in [
"DieselStoreFrontWide",
"OfferImageWide",
"VaultClosed",
"ProductLogo",
]:
if img["type"] == "VaultClosed" and self.title != "Mystery Game":
continue
self.image.update_image(img["url"], json_info["title"], (self.width, int(self.width * 9 / 16)))
self.image.update_image(
img["url"],
json_info["title"],
(self.width, int(self.width * 9 / 16)),
)
break
else:
logger.info(", ".join([img["type"] for img in json_info["keyImages"]]))
@ -95,8 +106,8 @@ class WishlistWidget(QWidget, Ui_WishlistWidget):
break
else:
self.developer.setText(game["seller"]["name"])
original_price = game['price']['totalPrice']['fmtPrice']['originalPrice']
discount_price = game['price']['totalPrice']['fmtPrice']['discountPrice']
original_price = game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
discount_price = game["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
self.price.setText(original_price if original_price != "0" else self.tr("Free"))
# if discount
@ -118,7 +129,9 @@ class WishlistWidget(QWidget, Ui_WishlistWidget):
url = image_model.offer_image_wide
self.image.update_image(url, game.get("title"), (240, 135))
self.delete_button.setIcon(icon("mdi.delete", color="white"))
self.delete_button.clicked.connect(lambda: self.delete_from_wishlist.emit(self.game))
self.delete_button.clicked.connect(
lambda: self.delete_from_wishlist.emit(self.game)
)
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
# left button

View file

@ -1,8 +1,16 @@
from PyQt5 import QtGui
from PyQt5.QtCore import pyqtSignal, Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QScrollArea, QGroupBox, QPushButton, \
QStackedWidget
from PyQt5.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QScrollArea,
QGroupBox,
QPushButton,
QStackedWidget,
)
from rare.utils.extra_widgets import ImageLabel, FlowLayout, WaitingSpinner
@ -79,8 +87,8 @@ class _SearchResultItem(QGroupBox):
self.title.setFont(title_font)
self.title.setWordWrap(True)
self.layout.addWidget(self.title)
price = result['price']['totalPrice']['fmtPrice']['originalPrice']
discount_price = result['price']['totalPrice']['fmtPrice']['discountPrice']
price = result["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
discount_price = result["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
price_layout = QHBoxLayout()
price_label = QLabel(price if price != "0" else self.tr("Free"))
price_layout.addWidget(price_label)

View file

@ -3,8 +3,12 @@ from logging import getLogger
from PyQt5.QtCore import pyqtSignal, QObject
from rare.components.tabs.shop.constants import wishlist_query, search_query, add_to_wishlist_query, \
remove_from_wishlist_query
from rare.components.tabs.shop.constants import (
wishlist_query,
search_query,
add_to_wishlist_query,
remove_from_wishlist_query,
)
from rare.components.tabs.shop.shop_models import BrowseModel
from rare.utils.qt_requests import QtRequestManager
@ -46,13 +50,17 @@ class ShopApiCore(QObject):
handle_func(results)
def get_wishlist(self, handle_func):
self.auth_manager.post(graphql_url, {
"query": wishlist_query,
"variables": {
"country": self.country_code,
"locale": self.language_code + "-" + self.country_code
}
}, lambda data: self._handle_wishlist(data, handle_func))
self.auth_manager.post(
graphql_url,
{
"query": wishlist_query,
"variables": {
"country": self.country_code,
"locale": self.language_code + "-" + self.country_code,
},
},
lambda data: self._handle_wishlist(data, handle_func),
)
def _handle_wishlist(self, data, handle_func):
try:
@ -71,13 +79,24 @@ class ShopApiCore(QObject):
def search_game(self, name, handle_func):
payload = {
"query": search_query,
"variables": {"category": "games/edition/base|bundles/games|editors|software/edition/base", "count": 10,
"country": self.country_code, "keywords": name, "locale": self.locale, "sortDir": "DESC",
"allowCountries": self.country_code,
"start": 0, "tag": "", "withMapping": False, "withPrice": True}
"variables": {
"category": "games/edition/base|bundles/games|editors|software/edition/base",
"count": 10,
"country": self.country_code,
"keywords": name,
"locale": self.locale,
"sortDir": "DESC",
"allowCountries": self.country_code,
"start": 0,
"tag": "",
"withMapping": False,
"withPrice": True,
},
}
self.manager.post(graphql_url, payload, lambda data: self._handle_search(data, handle_func))
self.manager.post(
graphql_url, payload, lambda data: self._handle_search(data, handle_func)
)
def _handle_search(self, data, handle_func):
try:
@ -98,13 +117,26 @@ class ShopApiCore(QObject):
url = "https://www.epicgames.com/graphql?operationName=searchStoreQuery&variables="
args = urllib.parse.quote_plus(str(browse_model.__dict__))
for old, new in [("%27", "%22"), ("+", ""), ("%3A", ":"), ("%2C", ","), ("%5B", "["), ("%5D", "]"),
("True", "true")]:
for old, new in [
("%27", "%22"),
("+", ""),
("%3A", ":"),
("%2C", ","),
("%5B", "["),
("%5D", "]"),
("True", "true"),
]:
args = args.replace(old, new)
url = url + args + "&extensions=%7B%22persistedQuery%22:%7B%22version%22:1,%22sha256Hash%22:%220304d711e653a2914f3213a6d9163cc17153c60aef0ef52279731b02779231d2%22%7D%7D"
url = (
url
+ args
+ "&extensions=%7B%22persistedQuery%22:%7B%22version%22:1,%22sha256Hash%22:%220304d711e653a2914f3213a6d9163cc17153c60aef0ef52279731b02779231d2%22%7D%7D"
)
self.auth_manager.get(url, lambda data: self._handle_browse_games(data, handle_func))
self.auth_manager.get(
url, lambda data: self._handle_browse_games(data, handle_func)
)
def _handle_browse_games(self, data, handle_func):
self.browse_active = False
@ -143,11 +175,15 @@ class ShopApiCore(QObject):
"offerId": offer_id,
"namespace": namespace,
"country": self.country_code,
"locale": self.locale
"locale": self.locale,
},
"query": add_to_wishlist_query
"query": add_to_wishlist_query,
}
self.auth_manager.post(graphql_url, payload, lambda data: self._handle_add_to_wishlist(data, handle_func))
self.auth_manager.post(
graphql_url,
payload,
lambda data: self._handle_add_to_wishlist(data, handle_func),
)
def _handle_add_to_wishlist(self, data, handle_func):
try:
@ -166,11 +202,15 @@ class ShopApiCore(QObject):
"variables": {
"offerId": offer_id,
"namespace": namespace,
"operation": "REMOVE"
"operation": "REMOVE",
},
"query": remove_from_wishlist_query
"query": remove_from_wishlist_query,
}
self.auth_manager.post(graphql_url, payload, lambda data: self._handle_remove_from_wishlist(data, handle_func))
self.auth_manager.post(
graphql_url,
payload,
lambda data: self._handle_remove_from_wishlist(data, handle_func),
)
def _handle_remove_from_wishlist(self, data, handle_func):
try:

View file

@ -3,9 +3,15 @@ from dataclasses import dataclass
class ImageUrlModel:
def __init__(self, front_tall: str = "", offer_image_tall: str = "",
thumbnail: str = "", front_wide: str = "", offer_image_wide: str = "",
product_logo: str = ""):
def __init__(
self,
front_tall: str = "",
offer_image_tall: str = "",
thumbnail: str = "",
front_wide: str = "",
offer_image_wide: str = "",
product_logo: str = "",
):
self.front_tall = front_tall
self.offer_image_tall = offer_image_tall
self.thumbnail = thumbnail
@ -34,17 +40,30 @@ class ImageUrlModel:
class ShopGame:
# TODO: Copyrights etc
def __init__(self, title: str = "", image_urls: ImageUrlModel = None, social_links: dict = None,
langs: list = None, reqs: dict = None, publisher: str = "", developer: str = "",
original_price: str = "", discount_price: str = "", tags: list = None, namespace: str = "",
offer_id: str = ""):
def __init__(
self,
title: str = "",
image_urls: ImageUrlModel = None,
social_links: dict = None,
langs: list = None,
reqs: dict = None,
publisher: str = "",
developer: str = "",
original_price: str = "",
discount_price: str = "",
tags: list = None,
namespace: str = "",
offer_id: str = "",
):
self.title = title
self.image_urls = image_urls
self.links = []
if social_links:
for item in social_links:
if item.startswith("link"):
self.links.append(tuple((item.replace("link", ""), social_links[item])))
self.links.append(
tuple((item.replace("link", ""), social_links[item]))
)
else:
self.links = []
self.languages = langs
@ -77,7 +96,9 @@ class ShopGame:
for item in links:
if item.startswith("link"):
tmp.links.append(tuple((item.replace("link", ""), links[item])))
tmp.available_voice_langs = api_data["data"]["requirements"].get("languages", "Failed")
tmp.available_voice_langs = api_data["data"]["requirements"].get(
"languages", "Failed"
)
tmp.reqs = {}
for i, system in enumerate(api_data["data"]["requirements"].get("systems", [])):
try:
@ -86,7 +107,10 @@ class ShopGame:
continue
for req in system["details"]:
try:
tmp.reqs[system["systemType"]][req["title"]] = (req["minimum"], req["recommended"])
tmp.reqs[system["systemType"]][req["title"]] = (
req["minimum"],
req["recommended"],
)
except KeyError:
pass
tmp.publisher = api_data["data"]["meta"].get("publisher", "")
@ -95,9 +119,14 @@ class ShopGame:
for i in search_data["customAttributes"]:
if i["key"] == "developerName":
tmp.developer = i["value"]
tmp.price = search_data['price']['totalPrice']['fmtPrice']['originalPrice']
tmp.discount_price = search_data['price']['totalPrice']['fmtPrice']['discountPrice']
tmp.tags = [i.replace("_", " ").capitalize() for i in api_data["data"]["meta"].get("tags", [])]
tmp.price = search_data["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
tmp.discount_price = search_data["price"]["totalPrice"]["fmtPrice"][
"discountPrice"
]
tmp.tags = [
i.replace("_", " ").capitalize()
for i in api_data["data"]["meta"].get("tags", [])
]
tmp.namespace = search_data["namespace"]
tmp.offer_id = search_data["id"]
@ -116,7 +145,9 @@ class BrowseModel:
tag: str = ""
withMapping: bool = True
withPrice: bool = True
date: str = f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%X')}.999Z]"
date: str = (
f"[,{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%dT%X')}.999Z]"
)
price: str = ""
onSale: bool = False

View file

@ -3,7 +3,14 @@ import logging
import random
from PyQt5.QtCore import pyqtSignal
from PyQt5.QtWidgets import QGroupBox, QScrollArea, QCheckBox, QLabel, QPushButton, QHBoxLayout
from PyQt5.QtWidgets import (
QGroupBox,
QScrollArea,
QCheckBox,
QLabel,
QPushButton,
QHBoxLayout,
)
from legendary.core import LegendaryCore
from rare.ui.components.tabs.store.store import Ui_ShopWidget
@ -49,7 +56,9 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
self.game_stack.addWidget(WaitingSpinner())
self.game_stack.setCurrentIndex(1)
self.search_bar = ButtonLineEdit("fa.search", placeholder_text=self.tr("Search Games"))
self.search_bar = ButtonLineEdit(
"fa.search", placeholder_text=self.tr("Search Games")
)
self.layout().insertWidget(0, self.search_bar)
# self.search_bar.textChanged.connect(self.search_games)
@ -78,10 +87,14 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
if item:
item.widget().deleteLater()
if wishlist and wishlist[0] == "error":
self.discount_widget.layout().addWidget(QLabel(self.tr("Failed to get wishlist: ") + wishlist[1]))
self.discount_widget.layout().addWidget(
QLabel(self.tr("Failed to get wishlist: ") + wishlist[1])
)
btn = QPushButton(self.tr("Reload"))
self.discount_widget.layout().addWidget(btn)
btn.clicked.connect(lambda: self.api_core.get_wishlist(self.add_wishlist_items))
btn.clicked.connect(
lambda: self.api_core.get_wishlist(self.add_wishlist_items)
)
self.discount_stack.setCurrentIndex(0)
return
@ -109,10 +122,14 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
item.widget().deleteLater()
if free_games and free_games[0] == "error":
self.free_widget.layout().addWidget(QLabel(self.tr("Failed to fetch free games: ") + free_games[1]))
self.free_widget.layout().addWidget(
QLabel(self.tr("Failed to fetch free games: ") + free_games[1])
)
btn = QPushButton(self.tr("Reload"))
self.free_widget.layout().addWidget(btn)
btn.clicked.connect(lambda: self.api_core.get_free_games(self.add_free_games))
btn.clicked.connect(
lambda: self.api_core.get_free_games(self.add_free_games)
)
self.free_stack.setCurrentIndex(0)
return
@ -129,9 +146,11 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
coming_free_games = []
for game in free_games:
try:
if game['price']['totalPrice']['fmtPrice']['discountPrice'] == "0" and \
game['price']['totalPrice']['fmtPrice']['originalPrice'] != \
game['price']['totalPrice']['fmtPrice']['discountPrice']:
if (
game["price"]["totalPrice"]["fmtPrice"]["discountPrice"] == "0"
and game["price"]["totalPrice"]["fmtPrice"]["originalPrice"]
!= game["price"]["totalPrice"]["fmtPrice"]["discountPrice"]
):
free_games_now.append(game)
continue
@ -145,13 +164,19 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
# parse datetime to check if game is next week or now
try:
start_date = datetime.datetime.strptime(
game["promotions"]["upcomingPromotionalOffers"][0]["promotionalOffers"][0]["startDate"],
'%Y-%m-%dT%H:%M:%S.%fZ')
game["promotions"]["upcomingPromotionalOffers"][0][
"promotionalOffers"
][0]["startDate"],
"%Y-%m-%dT%H:%M:%S.%fZ",
)
except Exception:
try:
start_date = datetime.datetime.strptime(
game["promotions"]["promotionalOffers"][0]["promotionalOffers"][0]["startDate"],
'%Y-%m-%dT%H:%M:%S.%fZ')
game["promotions"]["promotionalOffers"][0][
"promotionalOffers"
][0]["startDate"],
"%Y-%m-%dT%H:%M:%S.%fZ",
)
except Exception as e:
continue
@ -171,7 +196,9 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
self.free_game_widgets.append(w)
now_free += 1
if now_free == 0:
self.free_games_now.layout().addWidget(QLabel(self.tr("Could not find current free game")))
self.free_games_now.layout().addWidget(
QLabel(self.tr("Could not find current free game"))
)
# free games next week
for free_game in coming_free_games:
@ -187,30 +214,53 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
def init_filter(self):
self.none_price.toggled.connect(lambda: self.prepare_request("") if self.none_price.isChecked() else None)
self.free_button.toggled.connect(lambda: self.prepare_request("free") if self.free_button.isChecked() else None)
self.none_price.toggled.connect(
lambda: self.prepare_request("") if self.none_price.isChecked() else None
)
self.free_button.toggled.connect(
lambda: self.prepare_request("free")
if self.free_button.isChecked()
else None
)
self.under10.toggled.connect(
lambda: self.prepare_request("<price>[0, 1000)") if self.under10.isChecked() else None)
lambda: self.prepare_request("<price>[0, 1000)")
if self.under10.isChecked()
else None
)
self.under20.toggled.connect(
lambda: self.prepare_request("<price>[0, 2000)") if self.under20.isChecked() else None)
lambda: self.prepare_request("<price>[0, 2000)")
if self.under20.isChecked()
else None
)
self.under30.toggled.connect(
lambda: self.prepare_request("<price>[0, 3000)") if self.under30.isChecked() else None)
self.above.toggled.connect(lambda: self.prepare_request("<price>[1499,]") if self.above.isChecked() else None)
lambda: self.prepare_request("<price>[0, 3000)")
if self.under30.isChecked()
else None
)
self.above.toggled.connect(
lambda: self.prepare_request("<price>[1499,]")
if self.above.isChecked()
else None
)
# self.on_discount.toggled.connect(lambda: self.prepare_request("sale") if self.on_discount.isChecked() else None)
self.on_discount.toggled.connect(lambda: self.prepare_request())
constants = Constants()
self.checkboxes = []
for groupbox, variables in [(self.genre_gb, constants.categories),
(self.platform_gb, constants.platforms),
(self.others_gb, constants.others),
(self.type_gb, constants.types)]:
for groupbox, variables in [
(self.genre_gb, constants.categories),
(self.platform_gb, constants.platforms),
(self.others_gb, constants.others),
(self.type_gb, constants.types),
]:
for text, tag in variables:
checkbox = CheckBox(text, tag)
checkbox.activated.connect(lambda x: self.prepare_request(added_tag=x))
checkbox.deactivated.connect(lambda x: self.prepare_request(removed_tag=x))
checkbox.deactivated.connect(
lambda x: self.prepare_request(removed_tag=x)
)
groupbox.layout().addWidget(checkbox)
self.checkboxes.append(checkbox)
self.reset_button.clicked.connect(self.reset_filters)
@ -228,8 +278,14 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
self.on_discount.setChecked(False)
def prepare_request(self, price: str = None, added_tag: int = 0, removed_tag: int = 0,
added_type: str = "", removed_type: str = ""):
def prepare_request(
self,
price: str = None,
added_tag: int = 0,
removed_tag: int = 0,
added_type: str = "",
removed_type: str = "",
):
if not self.update_games_allowed:
return
if price is not None:
@ -255,9 +311,14 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
self.game_stack.setCurrentIndex(1)
date = f"[{datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d')},]"
browse_model = BrowseModel(language_code=self.core.language_code, country_code=self.core.country_code,
date=date, count=20, price=self.price,
onSale=self.on_discount.isChecked())
browse_model = BrowseModel(
language_code=self.core.language_code,
country_code=self.core.country_code,
date=date,
count=20,
price=self.price,
onSale=self.on_discount.isChecked(),
)
browse_model.tag = "|".join(self.tags)
if self.types:
@ -265,7 +326,10 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
self.api_core.browse_games(browse_model, self.show_games)
def show_games(self, data):
for item in (self.game_widget.layout().itemAt(i) for i in range(self.game_widget.layout().count())):
for item in (
self.game_widget.layout().itemAt(i)
for i in range(self.game_widget.layout().count())
):
item.widget().deleteLater()
if data:
for game in data:
@ -275,7 +339,8 @@ class ShopWidget(QScrollArea, Ui_ShopWidget):
else:
self.game_widget.layout().addWidget(
QLabel(self.tr("Could not get games matching the filter")))
QLabel(self.tr("Could not get games matching the filter"))
)
self.game_stack.setCurrentIndex(0)
self.game_widget.layout().update()

View file

@ -21,22 +21,31 @@ class Wishlist(QStackedWidget, Ui_Wishlist):
self.wishlist = []
self.widgets = []
self.sort_cb.currentIndexChanged.connect(lambda i: self.set_wishlist(self.wishlist, i))
self.sort_cb.currentIndexChanged.connect(
lambda i: self.set_wishlist(self.wishlist, i)
)
self.filter_cb.currentIndexChanged.connect(self.set_filter)
self.reload_button.clicked.connect(self.update_wishlist)
self.reload_button.setIcon(icon("fa.refresh", color="white"))
self.reverse.stateChanged.connect(lambda: self.set_wishlist(sort=self.sort_cb.currentIndex()))
self.reverse.stateChanged.connect(
lambda: self.set_wishlist(sort=self.sort_cb.currentIndex())
)
def update_wishlist(self):
self.setCurrentIndex(1)
self.api_core.get_wishlist(self.set_wishlist)
def delete_from_wishlist(self, game):
self.api_core.remove_from_wishlist(game["namespace"], game["id"],
lambda success: self.update_wishlist() if success else
QMessageBox.warning(self, "Error",
self.tr("Could not remove game from wishlist")))
self.api_core.remove_from_wishlist(
game["namespace"],
game["id"],
lambda success: self.update_wishlist()
if success
else QMessageBox.warning(
self, "Error", self.tr("Could not remove game from wishlist")
),
)
self.update_wishlist_signal.emit()
def set_filter(self, i):
@ -69,14 +78,26 @@ class Wishlist(QStackedWidget, Ui_Wishlist):
if sort == 0:
sorted_list = sorted(self.wishlist, key=lambda x: x["offer"]["title"])
elif sort == 1:
sorted_list = sorted(self.wishlist,
key=lambda x: x["offer"]['price']['totalPrice']['fmtPrice']['discountPrice'])
sorted_list = sorted(
self.wishlist,
key=lambda x: x["offer"]["price"]["totalPrice"]["fmtPrice"][
"discountPrice"
],
)
elif sort == 2:
sorted_list = sorted(self.wishlist, key=lambda x: x["offer"]["seller"]["name"])
sorted_list = sorted(
self.wishlist, key=lambda x: x["offer"]["seller"]["name"]
)
elif sort == 3:
sorted_list = sorted(self.wishlist, reverse=True, key=lambda x: 1 - (
x["offer"]["price"]["totalPrice"]["discountPrice"] / x["offer"]["price"]["totalPrice"][
"originalPrice"]))
sorted_list = sorted(
self.wishlist,
reverse=True,
key=lambda x: 1
- (
x["offer"]["price"]["totalPrice"]["discountPrice"]
/ x["offer"]["price"]["totalPrice"]["originalPrice"]
),
)
else:
sorted_list = self.wishlist
self.widgets.clear()

View file

@ -9,7 +9,7 @@ class MainTabBar(QTabBar):
self._expanded = expanded
self.setObjectName("MainTabBar")
font = self.font()
font.setPointSize(font.pointSize()+2)
font.setPointSize(font.pointSize() + 2)
font.setBold(True)
self.setFont(font)
# self.setContentsMargins(0,10,0,10)

View file

@ -2,11 +2,36 @@ import os
from logging import getLogger
from typing import Callable, Tuple
from PyQt5.QtCore import Qt, QCoreApplication, QRect, QSize, QPoint, pyqtSignal, QFileInfo
from PyQt5.QtCore import (
Qt,
QCoreApplication,
QRect,
QSize,
QPoint,
pyqtSignal,
QFileInfo,
)
from PyQt5.QtGui import QMovie, QPixmap, QFontMetrics, QImage
from PyQt5.QtWidgets import QLayout, QStyle, QSizePolicy, QLabel, QFileDialog, QHBoxLayout, QWidget, QPushButton, \
QStyleOptionTab, QStylePainter, QTabBar, QLineEdit, QToolButton, QTabWidget, QCompleter, QFileSystemModel, \
QStyledItemDelegate, QFileIconProvider
from PyQt5.QtWidgets import (
QLayout,
QStyle,
QSizePolicy,
QLabel,
QFileDialog,
QHBoxLayout,
QWidget,
QPushButton,
QStyleOptionTab,
QStylePainter,
QTabBar,
QLineEdit,
QToolButton,
QTabWidget,
QCompleter,
QFileSystemModel,
QStyledItemDelegate,
QFileIconProvider,
)
from qtawesome import icon as qta_icon
from rare import cache_dir
@ -33,15 +58,13 @@ class FlowLayout(QLayout):
if self._hspacing >= 0:
return self._hspacing
else:
return self.smartSpacing(
QStyle.PM_LayoutHorizontalSpacing)
return self.smartSpacing(QStyle.PM_LayoutHorizontalSpacing)
def verticalSpacing(self):
if self._vspacing >= 0:
return self._vspacing
else:
return self.smartSpacing(
QStyle.PM_LayoutVerticalSpacing)
return self.smartSpacing(QStyle.PM_LayoutVerticalSpacing)
def count(self):
return len(self._items)
@ -91,13 +114,13 @@ class FlowLayout(QLayout):
hspace = self.horizontalSpacing()
if hspace == -1:
hspace = widget.style().layoutSpacing(
QSizePolicy.PushButton,
QSizePolicy.PushButton, Qt.Horizontal)
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal
)
vspace = self.verticalSpacing()
if vspace == -1:
vspace = widget.style().layoutSpacing(
QSizePolicy.PushButton,
QSizePolicy.PushButton, Qt.Vertical)
QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical
)
nextX = x + item.sizeHint().width() + hspace
if nextX - hspace > effective.right() and lineheight > 0:
x = effective.x()
@ -105,8 +128,7 @@ class FlowLayout(QLayout):
nextX = x + item.sizeHint().width() + hspace
lineheight = 0
if not testonly:
item.setGeometry(
QRect(QPoint(x, y), item.sizeHint()))
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
x = nextX
lineheight = max(lineheight, item.sizeHint().height())
return y + lineheight - rect.y() + bottom
@ -125,14 +147,16 @@ class IndicatorLineEdit(QWidget):
textChanged = pyqtSignal(str)
is_valid = False
def __init__(self,
text: str = "",
ph_text: str = "",
completer: QCompleter = None,
edit_func: Callable[[str], Tuple[bool, str]] = None,
save_func: Callable[[str], None] = None,
horiz_policy: QSizePolicy = QSizePolicy.Expanding,
parent=None):
def __init__(
self,
text: str = "",
ph_text: str = "",
completer: QCompleter = None,
edit_func: Callable[[str], Tuple[bool, str]] = None,
save_func: Callable[[str], None] = None,
horiz_policy: QSizePolicy = QSizePolicy.Expanding,
parent=None,
):
super(IndicatorLineEdit, self).__init__(parent=parent)
self.setObjectName("IndicatorLineEdit")
self.layout = QHBoxLayout(self)
@ -146,7 +170,7 @@ class IndicatorLineEdit(QWidget):
# Add hint_label to line_edit
self.line_edit.setLayout(QHBoxLayout())
self.hint_label = QLabel()
self.hint_label.setObjectName('HintLabel')
self.hint_label.setObjectName("HintLabel")
self.hint_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.line_edit.layout().setContentsMargins(0, 0, 10, 0)
self.line_edit.layout().addWidget(self.hint_label)
@ -158,13 +182,17 @@ class IndicatorLineEdit(QWidget):
self.layout.addWidget(self.line_edit)
if edit_func is not None:
self.indicator_label = QLabel()
self.indicator_label.setPixmap(qta_icon("ei.info-circle", color="gray").pixmap(16, 16))
self.indicator_label.setPixmap(
qta_icon("ei.info-circle", color="gray").pixmap(16, 16)
)
self.indicator_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self.layout.addWidget(self.indicator_label)
if not ph_text:
_translate = QCoreApplication.translate
self.line_edit.setPlaceholderText(_translate(self.__class__.__name__, "Default"))
self.line_edit.setPlaceholderText(
_translate(self.__class__.__name__, "Default")
)
self.edit_func = edit_func
self.save_func = save_func
@ -193,7 +221,9 @@ class IndicatorLineEdit(QWidget):
def __indicator(self, res):
color = "green" if res else "red"
self.indicator_label.setPixmap(qta_icon("ei.info-circle", color=color).pixmap(16, 16))
self.indicator_label.setPixmap(
qta_icon("ei.info-circle", color=color).pixmap(16, 16)
)
def __edit(self, text):
if self.edit_func is not None:
@ -214,22 +244,22 @@ class IndicatorLineEdit(QWidget):
class PathEditIconProvider(QFileIconProvider):
icons = [
'mdi.file-cancel', # Unknown
'mdi.desktop-classic', # Computer
'mdi.desktop-mac', # Desktop
'mdi.trash-can', # Trashcan
'mdi.server-network', # Network
'mdi.harddisk', # Drive
'mdi.folder', # Folder
'mdi.file', # File
'mdi.cog', # Executable
"mdi.file-cancel", # Unknown
"mdi.desktop-classic", # Computer
"mdi.desktop-mac", # Desktop
"mdi.trash-can", # Trashcan
"mdi.server-network", # Network
"mdi.harddisk", # Drive
"mdi.folder", # Folder
"mdi.file", # File
"mdi.cog", # Executable
]
def __init__(self):
super(PathEditIconProvider, self).__init__()
self.icon_types = dict()
for idx, icn in enumerate(self.icons):
self.icon_types.update({idx-1: qta_icon(icn, color='#eeeeee')})
self.icon_types.update({idx - 1: qta_icon(icn, color="#eeeeee")})
def icon(self, info_type):
if isinstance(info_type, QFileInfo):
@ -249,25 +279,35 @@ class PathEdit(IndicatorLineEdit):
completer = QCompleter()
compl_model = QFileSystemModel()
def __init__(self,
path: str = "",
file_type: QFileDialog.FileType = QFileDialog.AnyFile,
type_filter: str = "",
name_filter: str = "",
ph_text: str = "",
edit_func: Callable[[str], Tuple[bool, str]] = None,
save_func: Callable[[str], None] = None,
horiz_policy: QSizePolicy = QSizePolicy.Expanding,
parent=None):
self.compl_model.setOptions(QFileSystemModel.DontWatchForChanges |
QFileSystemModel.DontResolveSymlinks |
QFileSystemModel.DontUseCustomDirectoryIcons)
def __init__(
self,
path: str = "",
file_type: QFileDialog.FileType = QFileDialog.AnyFile,
type_filter: str = "",
name_filter: str = "",
ph_text: str = "",
edit_func: Callable[[str], Tuple[bool, str]] = None,
save_func: Callable[[str], None] = None,
horiz_policy: QSizePolicy = QSizePolicy.Expanding,
parent=None,
):
self.compl_model.setOptions(
QFileSystemModel.DontWatchForChanges
| QFileSystemModel.DontResolveSymlinks
| QFileSystemModel.DontUseCustomDirectoryIcons
)
self.compl_model.setIconProvider(PathEditIconProvider())
self.compl_model.setRootPath(path)
self.completer.setModel(self.compl_model)
super(PathEdit, self).__init__(text=path, ph_text=ph_text, completer=self.completer,
edit_func=edit_func, save_func=save_func,
horiz_policy=horiz_policy, parent=parent)
super(PathEdit, self).__init__(
text=path,
ph_text=ph_text,
completer=self.completer,
edit_func=edit_func,
save_func=save_func,
horiz_policy=horiz_policy,
parent=parent,
)
self.setObjectName("PathEdit")
self.line_edit.setMinimumSize(QSize(300, 0))
self.path_select = QToolButton(self)
@ -353,10 +393,12 @@ class SideTabWidget(QTabWidget):
class WaitingSpinner(QLabel):
def __init__(self):
super(WaitingSpinner, self).__init__()
self.setStyleSheet("""
self.setStyleSheet(
"""
margin-left: auto;
margin-right: auto;
""")
"""
)
self.movie = QMovie(":/images/loader.gif")
self.setMovie(self.movie)
self.movie.start()
@ -368,11 +410,15 @@ class SelectViewWidget(QWidget):
def __init__(self, icon_view: bool):
super(SelectViewWidget, self).__init__()
self.icon_view = icon_view
self.setStyleSheet("""QPushButton{border: none; background-color: transparent}""")
self.setStyleSheet(
"""QPushButton{border: none; background-color: transparent}"""
)
self.icon_view_button = QPushButton()
self.list_view = QPushButton()
if icon_view:
self.icon_view_button.setIcon(qta_icon("mdi.view-grid-outline", color="orange"))
self.icon_view_button.setIcon(
qta_icon("mdi.view-grid-outline", color="orange")
)
self.list_view.setIcon(qta_icon("fa5s.list"))
else:
self.icon_view_button.setIcon(qta_icon("mdi.view-grid-outline"))
@ -438,14 +484,19 @@ class ImageLabel(QLabel):
return
image = QImage()
image.loadFromData(data)
image = image.scaled(*self.img_size[:2], Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
image = image.scaled(
*self.img_size[:2],
Qt.KeepAspectRatio,
transformMode=Qt.SmoothTransformation,
)
pixmap = QPixmap().fromImage(image)
self.setPixmap(pixmap)
def show_image(self):
self.image = QPixmap(os.path.join(self.path, self.name)).scaled(*self.img_size,
transformMode=Qt.SmoothTransformation)
self.image = QPixmap(os.path.join(self.path, self.name)).scaled(
*self.img_size, transformMode=Qt.SmoothTransformation
)
self.setPixmap(self.image)
@ -457,20 +508,31 @@ class ButtonLineEdit(QLineEdit):
self.button = QToolButton(self)
self.button.setIcon(qta_icon(icon_name, color="white"))
self.button.setStyleSheet('border: 0px; padding: 0px;')
self.button.setStyleSheet("border: 0px; padding: 0px;")
self.button.setCursor(Qt.ArrowCursor)
self.button.clicked.connect(self.buttonClicked.emit)
self.setPlaceholderText(placeholder_text)
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
buttonSize = self.button.sizeHint()
self.setStyleSheet('QLineEdit {padding-right: %dpx; }' % (buttonSize.width() + frameWidth + 1))
self.setMinimumSize(max(self.minimumSizeHint().width(), buttonSize.width() + frameWidth * 2 + 2),
max(self.minimumSizeHint().height(), buttonSize.height() + frameWidth * 2 + 2))
self.setStyleSheet(
"QLineEdit {padding-right: %dpx; }" % (buttonSize.width() + frameWidth + 1)
)
self.setMinimumSize(
max(
self.minimumSizeHint().width(), buttonSize.width() + frameWidth * 2 + 2
),
max(
self.minimumSizeHint().height(),
buttonSize.height() + frameWidth * 2 + 2,
),
)
def resizeEvent(self, event):
buttonSize = self.button.sizeHint()
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
self.button.move(self.rect().right() - frameWidth - buttonSize.width(),
(self.rect().bottom() - buttonSize.height() + 1) // 2)
self.button.move(
self.rect().right() - frameWidth - buttonSize.width(),
(self.rect().bottom() - buttonSize.height() + 1) // 2,
)
super(ButtonLineEdit, self).resizeEvent(event)

View file

@ -64,10 +64,7 @@ class QJsonTreeItem(object):
return len(self._children)
def row(self):
return (
self._parent._children.index(self)
if self._parent else 0
)
return self._parent._children.index(self) if self._parent else 0
@property
def key(self):
@ -99,10 +96,7 @@ class QJsonTreeItem(object):
rootItem.key = "root"
if isinstance(value, dict):
items = (
sorted(value.items())
if sort else value.items()
)
items = sorted(value.items()) if sort else value.items()
for key, value in items:
child = self.load(value, rootItem)
@ -142,10 +136,9 @@ class QJsonModel(QtCore.QAbstractItemModel):
"""
assert isinstance(document, (dict, list, tuple)), (
"`document` must be of dict, list or tuple, "
"not %s" % type(document)
)
assert isinstance(
document, (dict, list, tuple)
), "`document` must be of dict, list or tuple, " "not %s" % type(document)
self.beginResetModel()
@ -277,7 +270,7 @@ class QJsonModel(QtCore.QAbstractItemModel):
return item.value
if __name__ == '__main__':
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
@ -286,7 +279,8 @@ if __name__ == '__main__':
view.setModel(model)
document = json.loads("""\
document = json.loads(
"""\
{
"firstName": "John",
"lastName": "Smith",
@ -308,16 +302,16 @@ if __name__ == '__main__':
}
]
}
""")
"""
)
model.load(document)
model.clear()
model.load(document)
# Sanity check
assert (
json.dumps(model.json(), sort_keys=True) ==
json.dumps(document, sort_keys=True)
assert json.dumps(model.json(), sort_keys=True) == json.dumps(
document, sort_keys=True
)
view.show()

View file

@ -22,15 +22,28 @@ def uninstall(app_name: str, core: LegendaryCore, options=None):
if platform.system() == "Linux":
if os.path.exists(os.path.expanduser(f"~/Desktop/{igame.title}.desktop")):
os.remove(os.path.expanduser(f"~/Desktop/{igame.title}.desktop"))
if os.path.exists(os.path.expanduser(f"~/.local/share/applications/{igame.title}.desktop")):
os.remove(os.path.expanduser(f"~/.local/share/applications/{igame.title}.desktop"))
if os.path.exists(
os.path.expanduser(f"~/.local/share/applications/{igame.title}.desktop")
):
os.remove(
os.path.expanduser(f"~/.local/share/applications/{igame.title}.desktop")
)
elif platform.system() == "Windows":
if os.path.exists(os.path.expanduser(f"~/Desktop/{igame.title.split(':')[0]}.lnk")):
if os.path.exists(
os.path.expanduser(f"~/Desktop/{igame.title.split(':')[0]}.lnk")
):
os.remove(os.path.expanduser(f"~/Desktop/{igame.title.split(':')[0]}.lnk"))
elif os.path.exists(
os.path.expandvars(f"%appdata%/Microsoft/Windows/Start Menu/{igame.title.split(':')[0]}.lnk")):
os.remove(os.path.expandvars(f"%appdata%/Microsoft/Windows/Start Menu/{igame.title.split(':')[0]}.lnk"))
os.path.expandvars(
f"%appdata%/Microsoft/Windows/Start Menu/{igame.title.split(':')[0]}.lnk"
)
):
os.remove(
os.path.expandvars(
f"%appdata%/Microsoft/Windows/Start Menu/{igame.title.split(':')[0]}.lnk"
)
)
try:
# Remove DLC first so directory is empty when game uninstall runs
@ -41,13 +54,17 @@ def uninstall(app_name: str, core: LegendaryCore, options=None):
core.uninstall_game(idlc, delete_files=not options["keep_files"])
logger.info(f'Removing "{igame.title}" from "{igame.install_path}"...')
core.uninstall_game(igame, delete_files=not options["keep_files"], delete_root_directory=True)
logger.info('Game has been uninstalled.')
core.uninstall_game(
igame, delete_files=not options["keep_files"], delete_root_directory=True
)
logger.info("Game has been uninstalled.")
if not options["keep_files"]:
shutil.rmtree(igame.install_path)
except Exception as e:
logger.warning(f'Removing game failed: {e!r}, please remove {igame.install_path} manually.')
logger.warning(
f"Removing game failed: {e!r}, please remove {igame.install_path} manually."
)
logger.info("Removing sections in config file")
if core.lgd.config.has_section(app_name):
@ -59,7 +76,7 @@ def uninstall(app_name: str, core: LegendaryCore, options=None):
def update_manifest(app_name: str, core: LegendaryCore):
game = core.get_game(app_name)
logger.info('Reloading game manifest of ' + game.app_title)
logger.info("Reloading game manifest of " + game.app_title)
new_manifest_data, base_urls = core.get_cdn_manifest(game)
# overwrite base urls in metadata with current ones to avoid using old/dead CDNs
game.base_urls = base_urls
@ -67,10 +84,11 @@ def update_manifest(app_name: str, core: LegendaryCore):
core.lgd.set_game_meta(game.app_name, game)
new_manifest = core.load_manifest(new_manifest_data)
logger.debug(f'Base urls: {base_urls}')
logger.debug(f"Base urls: {base_urls}")
# save manifest with version name as well for testing/downgrading/etc.
core.lgd.save_manifest(game.app_name, new_manifest_data,
version=new_manifest.meta.build_version)
core.lgd.save_manifest(
game.app_name, new_manifest_data, version=new_manifest.meta.build_version
)
class VerifySignals(QObject):
@ -105,8 +123,9 @@ class VerifyWorker(QRunnable):
manifest = self.core.load_manifest(manifest_data)
files = sorted(manifest.file_manifest_list.elements,
key=lambda a: a.filename.lower())
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]
@ -117,46 +136,60 @@ class VerifyWorker(QRunnable):
_translate = QCoreApplication.translate
logger.info(f'Verifying "{igame.title}" version "{manifest.meta.build_version}"')
logger.info(
f'Verifying "{igame.title}" version "{manifest.meta.build_version}"'
)
repair_file = []
try:
for result, path, result_hash in validate_files(igame.install_path, file_list):
for result, path, result_hash in validate_files(
igame.install_path, file_list
):
self.signals.status.emit((self.num, self.total, self.app_name))
self.num += 1
if result == VerifyResult.HASH_MATCH:
repair_file.append(f'{result_hash}:{path}')
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}')
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}"')
logger.error(
f'Other failure (see log), treating file as missing: "{path}"'
)
missing.append(path)
except OSError as e:
QMessageBox.warning(None, "Error", _translate("VerifyWorker", "Path does not exist"))
QMessageBox.warning(
None, "Error", _translate("VerifyWorker", "Path does not exist")
)
logger.error(str(e))
except ValueError as e:
QMessageBox.warning(None, "Error", _translate("VerifyWorker", "No files to validate"))
QMessageBox.warning(
None, "Error", _translate("VerifyWorker", "No files to validate")
)
logger.error(str(e))
# 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))
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.')
logger.info("Verification finished successfully.")
self.signals.summary.emit(0, 0, self.app_name)
else:
logger.error(f'Verification finished, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing.')
logger.error(
f"Verification finished, {len(failed)} file(s) corrupted, {len(missing)} file(s) are missing."
)
self.signals.summary.emit(len(failed), len(missing), self.app_name)
@ -176,30 +209,37 @@ def import_game(core: LegendaryCore, app_name: str, path: str) -> str:
return _tr("LgdUtils", "Path does not exist")
manifest, igame = core.import_game(game, path)
exe_path = os.path.join(path, manifest.meta.launch_exe.lstrip('/'))
exe_path = os.path.join(path, manifest.meta.launch_exe.lstrip("/"))
if not os.path.exists(exe_path):
logger.error(f"Launch Executable of {game.app_title} does not exist")
return _tr("LgdUtils", "Launch executable of {} does not exist").format(game.app_title)
return _tr("LgdUtils", "Launch executable of {} does not exist").format(
game.app_title
)
if game.is_dlc:
release_info = game.metadata.get('mainGameItem', {}).get('releaseInfo')
release_info = game.metadata.get("mainGameItem", {}).get("releaseInfo")
if release_info:
main_game_appname = release_info[0]['appId']
main_game_title = game.metadata['mainGameItem']['title']
main_game_appname = release_info[0]["appId"]
main_game_title = game.metadata["mainGameItem"]["title"]
if not core.is_installed(main_game_appname):
return _tr("LgdUtils", "Game is a DLC, but {} is not installed").format(main_game_title)
return _tr("LgdUtils", "Game is a DLC, but {} is not installed").format(
main_game_title
)
else:
return _tr("LgdUtils", "Unable to get base game information for DLC")
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)
found = sum(
os.path.exists(os.path.join(path, f.filename))
for f in manifest.file_manifest_list.elements
)
ratio = found / total
if ratio < 0.9:
logger.warning(
"Game files are missing. It may be not the latest version or it is corrupt")
"Game files are missing. It may be not the latest version or it is corrupt"
)
# return False
core.install_game(igame)
if igame.needs_verification:

View file

@ -20,7 +20,7 @@ class InstallOptionsModel:
no_install: bool = False
ignore_space_req: bool = False
force: bool = False
sdl_list: list = field(default_factory=lambda: [''])
sdl_list: list = field(default_factory=lambda: [""])
update: bool = False
silent: bool = False
platform: str = ""
@ -47,32 +47,38 @@ class InstallQueueItemModel:
options: InstallOptionsModel = None
def __bool__(self):
return (self.status_q is not None) and (self.download is not None) and (self.options is not None)
return (
(self.status_q is not None)
and (self.download is not None)
and (self.options is not None)
)
class PathSpec:
__egl_path_vars = {
'{appdata}': os.path.expandvars('%LOCALAPPDATA%'),
'{userdir}': os.path.expandvars('%USERPROFILE%/Documents'),
"{appdata}": os.path.expandvars("%LOCALAPPDATA%"),
"{userdir}": os.path.expandvars("%USERPROFILE%/Documents"),
# '{userprofile}': os.path.expandvars('%userprofile%'), # possibly wrong
'{usersavedgames}': os.path.expandvars('%USERPROFILE%/Saved Games'),
"{usersavedgames}": os.path.expandvars("%USERPROFILE%/Saved Games"),
}
egl_appdata: str = r'%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows'
egl_programdata: str = r'%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests'
wine_programdata: str = r'dosdevices/c:/ProgramData'
egl_appdata: str = r"%LOCALAPPDATA%\EpicGamesLauncher\Saved\Config\Windows"
egl_programdata: str = r"%PROGRAMDATA%\Epic\EpicGamesLauncher\Data\Manifests"
wine_programdata: str = r"dosdevices/c:/ProgramData"
def __init__(self, core: LegendaryCore = None, app_name: str = 'default'):
def __init__(self, core: LegendaryCore = None, app_name: str = "default"):
if core is not None:
self.__egl_path_vars.update({'{epicid}': core.lgd.userdata['account_id']})
self.__egl_path_vars.update({"{epicid}": core.lgd.userdata["account_id"]})
self.app_name = app_name
def cook(self, path: str) -> str:
cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split('/')]
cooked_path = [self.__egl_path_vars.get(p.lower(), p) for p in path.split("/")]
return os.path.join(*cooked_path)
@property
def wine_egl_programdata(self):
return self.egl_programdata.replace('\\', '/').replace('%PROGRAMDATA%', self.wine_programdata)
return self.egl_programdata.replace("\\", "/").replace(
"%PROGRAMDATA%", self.wine_programdata
)
def wine_egl_prefixes(self, results: int = 0) -> Union[List[str], str]:
possible_prefixes = [
@ -103,12 +109,14 @@ class ApiResults:
saves: list = None
def __bool__(self):
return self.game_list is not None \
and self.dlcs is not None \
and self.bit32_games is not None \
and self.mac_games is not None \
and self.no_asset_games is not None \
and self.saves is not None
return (
self.game_list is not None
and self.dlcs is not None
and self.bit32_games is not None
and self.mac_games is not None
and self.no_asset_games is not None
and self.saves is not None
)
class Signals(QObject):

View file

@ -29,30 +29,41 @@ class QtRequestManager(QObject):
self.request_active = RequestQueueItem(handle_func=handle_func)
payload = json.dumps(payload).encode("utf-8")
request.setHeader(QNetworkRequest.UserAgentHeader,
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36")
request.setHeader(
QNetworkRequest.UserAgentHeader,
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
)
if self.authorization_token is not None:
request.setRawHeader(b"Authorization", self.authorization_token.encode())
request.setRawHeader(
b"Authorization", self.authorization_token.encode()
)
self.request = self.manager.post(request, payload)
self.request.finished.connect(self.prepare_data)
else:
self.request_queue.append(
RequestQueueItem(method="post", url=url, payload=payload, handle_func=handle_func))
RequestQueueItem(
method="post", url=url, payload=payload, handle_func=handle_func
)
)
def get(self, url: str, handle_func: Callable[[Union[dict, bytes]], None]):
if not self.request_active:
request = QNetworkRequest(QUrl(url))
request.setHeader(QNetworkRequest.UserAgentHeader,
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36")
request.setHeader(
QNetworkRequest.UserAgentHeader,
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36",
)
self.request_active = RequestQueueItem(handle_func=handle_func)
self.request = self.manager.get(request)
self.request.finished.connect(self.prepare_data)
else:
self.request_queue.append(RequestQueueItem(method="get", url=url, handle_func=handle_func))
self.request_queue.append(
RequestQueueItem(method="get", url=url, handle_func=handle_func)
)
def prepare_data(self):
# self.request_active = False
@ -62,7 +73,9 @@ class QtRequestManager(QObject):
if self.request.error() == QNetworkReply.NoError:
if self.type == "json":
error = QJsonParseError()
json_data = QJsonDocument.fromJson(self.request.readAll().data(), error)
json_data = QJsonDocument.fromJson(
self.request.readAll().data(), error
)
if QJsonParseError.NoError == error.error:
data = json.loads(json_data.toJson().data().decode())
else:
@ -78,7 +91,11 @@ class QtRequestManager(QObject):
if self.request_queue:
if self.request_queue[0].method == "post":
self.post(self.request_queue[0].url, self.request_queue[0].payload, self.request_queue[0].handle_func)
self.post(
self.request_queue[0].url,
self.request_queue[0].payload,
self.request_queue[0].handle_func,
)
else:
self.get(self.request_queue[0].url, self.request_queue[0].handle_func)
self.request_queue.pop(0)

View file

@ -61,7 +61,9 @@ class DiscordRPC(QObject):
def set_discord_rpc(self, app_name=None):
if not self.RPC:
try:
self.RPC = Presence(client_id) # Rare app: https://discord.com/developers/applications
self.RPC = Presence(
client_id
) # Rare app: https://discord.com/developers/applications
self.RPC.connect()
except ConnectionRefusedError as e:
logger.warning("Discord is not active\n" + str(e))
@ -83,13 +85,15 @@ class DiscordRPC(QObject):
def update_rpc(self, app_name=None):
if self.settings.value("rpc_enable", 0, int) == 2 or (
app_name is None and self.settings.value("rpc_enable", 0, int) == 0):
app_name is None and self.settings.value("rpc_enable", 0, int) == 0
):
self.remove_rpc()
return
title = None
if not app_name:
self.RPC.update(large_image="logo",
details="https://github.com/Dummerle/Rare")
self.RPC.update(
large_image="logo", details="https://github.com/Dummerle/Rare"
)
return
if self.settings.value("rpc_name", True, bool):
try:
@ -104,9 +108,7 @@ class DiscordRPC(QObject):
if self.settings.value("rpc_os", True, bool):
os = "via Rare on " + platform.system()
self.RPC.update(large_image="logo",
details=title,
large_text=title,
state=os,
start=start)
self.RPC.update(
large_image="logo", details=title, large_text=title, state=os, start=start
)
self.state = 0

View file

@ -34,36 +34,38 @@ class SingleInstance(object):
if lockfile:
self.lockfile = lockfile
else:
basename = os.path.splitext(os.path.abspath(sys.argv[0]))[0].replace(
"/", "-").replace(":", "").replace("\\", "-") + '-%s' % flavor_id + '.lock'
self.lockfile = os.path.normpath(
tempfile.gettempdir() + '/' + basename)
basename = (
os.path.splitext(os.path.abspath(sys.argv[0]))[0]
.replace("/", "-")
.replace(":", "")
.replace("\\", "-")
+ "-%s" % flavor_id
+ ".lock"
)
self.lockfile = os.path.normpath(tempfile.gettempdir() + "/" + basename)
logger.debug("SingleInstance lockfile: " + self.lockfile)
if sys.platform == 'win32':
if sys.platform == "win32":
try:
# file already exists, we try to remove (in case previous
# execution was interrupted)
if os.path.exists(self.lockfile):
os.unlink(self.lockfile)
self.fd = os.open(
self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
self.fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
except OSError:
type, e, tb = sys.exc_info()
if e.errno == 13:
logger.error(
"Another instance is already running, quitting.")
logger.error("Another instance is already running, quitting.")
raise SingleInstanceException()
print(e.errno)
raise
else: # non Windows
self.fp = open(self.lockfile, 'w')
self.fp = open(self.lockfile, "w")
self.fp.flush()
try:
fcntl.lockf(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
logger.warning(
"Another instance is already running, quitting.")
logger.warning("Another instance is already running, quitting.")
raise SingleInstanceException()
self.initialized = True
@ -71,8 +73,8 @@ class SingleInstance(object):
if not self.initialized:
return
try:
if sys.platform == 'win32':
if hasattr(self, 'fd'):
if sys.platform == "win32":
if hasattr(self, "fd"):
os.close(self.fd)
os.unlink(self.lockfile)
else:

View file

@ -34,14 +34,16 @@ class SteamWorker(QRunnable):
"bronze": _tr("SteamWorker", "Bronze"),
"fail": _tr("SteamWorker", "Could not get grade"),
"borked": _tr("SteamWorker", "unplayable"),
"pending": _tr("SteamWorker", "Could not get grade")
"pending": _tr("SteamWorker", "Could not get grade"),
}
def set_app_name(self, app_name: str):
self.app_name = app_name
def run(self) -> None:
self.signals.rating_signal.emit(self.ratings.get(get_rating(self.app_name), self.ratings["fail"]))
self.signals.rating_signal.emit(
self.ratings.get(get_rating(self.app_name), self.ratings["fail"])
)
def get_rating(app_name: str):
@ -57,10 +59,7 @@ def get_rating(app_name: str):
steam_id = get_steam_id(game.app_title)
grade = get_grade(steam_id)
grades[app_name] = {
"steam_id": steam_id,
"grade": grade
}
grades[app_name] = {"steam_id": steam_id, "grade": grade}
with open(os.path.join(data_dir, "steam_ids.json"), "w") as f:
f.write(json.dumps(grades))
f.close()
@ -74,14 +73,14 @@ def get_grade(steam_code):
if steam_code == 0:
return "fail"
steam_code = str(steam_code)
url = 'https://www.protondb.com/api/v1/reports/summaries/'
res = requests.get(url + steam_code + '.json')
url = "https://www.protondb.com/api/v1/reports/summaries/"
res = requests.get(url + steam_code + ".json")
try:
lista = json.loads(res.text)
except json.decoder.JSONDecodeError:
return "fail"
return lista['tier']
return lista["tier"]
def load_json() -> dict:
@ -129,18 +128,18 @@ def get_steam_id(title: str):
def check_time(): # this function check if it's time to update
global file
text = open(file, 'r')
text = open(file, "r")
json_table = json.load(text)
text.close()
today = date.today()
day = 0 # it controls how many days it's necessary for an update
for i in 'ymd':
if i == 'd':
for i in "ymd":
if i == "d":
day = 7
else:
day = 0
if int(today.strftime('%' + i)) > int(json_table['data'][i]) + day:
if int(today.strftime("%" + i)) > int(json_table["data"][i]) + day:
return 1
else:
return 0

View file

@ -10,7 +10,16 @@ from typing import Tuple, List
import qtawesome
import requests
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QRunnable, QSettings, Qt, QFile, QDir
from PyQt5.QtCore import (
pyqtSignal,
pyqtSlot,
QObject,
QRunnable,
QSettings,
Qt,
QFile,
QDir,
)
from PyQt5.QtGui import QPalette, QColor, QPixmap, QImage
from PyQt5.QtWidgets import QApplication, QStyleFactory
from requests.exceptions import HTTPError
@ -25,6 +34,7 @@ if platform.system() == "Windows":
from win32com.client import Dispatch # pylint: disable=E0401
from rare import image_dir, shared, resources_path
# Mac not supported
from legendary.core import LegendaryCore
@ -66,27 +76,46 @@ def download_image(game, force=False):
# to get picture updates
if not os.path.isfile(f"{image_dir}/{game.app_name}/image.json"):
json_data = {"DieselGameBoxTall": None, "DieselGameBoxLogo": None, "Thumbnail": None}
json_data = {
"DieselGameBoxTall": None,
"DieselGameBoxLogo": None,
"Thumbnail": None,
}
else:
json_data = json.load(open(f"{image_dir}/{game.app_name}/image.json", "r"))
# Download
for image in game.metadata["keyImages"]:
if image["type"] == "DieselGameBoxTall" or image["type"] == "DieselGameBoxLogo" or image["type"] == "Thumbnail":
if (
image["type"] == "DieselGameBoxTall"
or image["type"] == "DieselGameBoxLogo"
or image["type"] == "Thumbnail"
):
if image["type"] not in json_data.keys():
json_data[image["type"]] = None
if json_data[image["type"]] != image["md5"] or not os.path.isfile(
f"{image_dir}/{game.app_name}/{image['type']}.png"):
f"{image_dir}/{game.app_name}/{image['type']}.png"
):
# Download
json_data[image["type"]] = image["md5"]
# os.remove(f"{image_dir}/{game.app_name}/{image['type']}.png")
json.dump(json_data, open(f"{image_dir}/{game.app_name}/image.json", "w"))
json.dump(
json_data, open(f"{image_dir}/{game.app_name}/image.json", "w")
)
logger.info(f"Download Image for Game: {game.app_title}")
url = image["url"]
resp = requests.get(url)
img = QImage()
img.loadFromData(resp.content)
img = img.scaled(200, 200 * 4 // 3, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
img.save(os.path.join(image_dir, game.app_name, image["type"] + ".png"), format="PNG")
img = img.scaled(
200,
200 * 4 // 3,
Qt.KeepAspectRatio,
transformMode=Qt.SmoothTransformation,
)
img.save(
os.path.join(image_dir, game.app_name, image["type"] + ".png"),
format="PNG",
)
color_role_map = {
@ -145,9 +174,13 @@ def load_color_scheme(path: str) -> QPalette:
def set_color_pallete(color_scheme: str):
if not color_scheme:
QApplication.instance().setStyle(QStyleFactory.create(QApplication.instance().property('rareDefaultQtStyle')))
QApplication.instance().setStyle(
QStyleFactory.create(QApplication.instance().property("rareDefaultQtStyle"))
)
QApplication.instance().setStyleSheet("")
QApplication.instance().setPalette(QApplication.instance().style().standardPalette())
QApplication.instance().setPalette(
QApplication.instance().style().standardPalette()
)
return
QApplication.instance().setStyle(QStyleFactory.create("Fusion"))
custom_palette = load_color_scheme(f":/schemes/{color_scheme}")
@ -166,7 +199,9 @@ def get_color_schemes() -> List[str]:
def set_style_sheet(style_sheet: str):
if not style_sheet:
QApplication.instance().setStyle(QStyleFactory.create(QApplication.instance().property('rareDefaultQtStyle')))
QApplication.instance().setStyle(
QStyleFactory.create(QApplication.instance().property("rareDefaultQtStyle"))
)
QApplication.instance().setStyleSheet("")
return
QApplication.instance().setStyle(QStyleFactory.create("Fusion"))
@ -195,7 +230,9 @@ def get_translations():
def get_latest_version():
try:
resp = requests.get("https://api.github.com/repos/Dummerle/Rare/releases/latest")
resp = requests.get(
"https://api.github.com/repos/Dummerle/Rare/releases/latest"
)
tag = resp.json()["tag_name"]
return tag
except requests.exceptions.ConnectionError:
@ -203,7 +240,7 @@ def get_latest_version():
def get_size(b: int) -> str:
for i in ['', 'K', 'M', 'G', 'T', 'P', 'E']:
for i in ["", "K", "M", "G", "T", "P", "E"]:
if b < 1024:
return f"{b:.2f}{i}B"
b /= 1024
@ -226,21 +263,22 @@ def create_rare_desktop_link(type_of_link):
else:
executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}"
with open(os.path.join(path, "Rare.desktop"), "w") as desktop_file:
desktop_file.write("[Desktop Entry]\n"
f"Name=Rare\n"
f"Type=Application\n"
f"Icon={os.path.join(resources_path, 'images', 'Rare.png')}\n"
f"Exec={executable}\n"
"Terminal=false\n"
"StartupWMClass=rare\n"
)
desktop_file.write(
"[Desktop Entry]\n"
f"Name=Rare\n"
f"Type=Application\n"
f"Icon={os.path.join(resources_path, 'images', 'Rare.png')}\n"
f"Exec={executable}\n"
"Terminal=false\n"
"StartupWMClass=rare\n"
)
desktop_file.close()
os.chmod(os.path.expanduser(os.path.join(path, "Rare.desktop")), 0o755)
elif platform.system() == "Windows":
# Target of shortcut
if type_of_link == "desktop":
target_folder = os.path.expanduser('~/Desktop/')
target_folder = os.path.expanduser("~/Desktop/")
elif type_of_link == "start_menu":
target_folder = os.path.expandvars("%appdata%/Microsoft/Windows/Start Menu")
else:
@ -255,7 +293,7 @@ def create_rare_desktop_link(type_of_link):
executable = executable.replace("python.exe", "pythonw.exe")
logger.debug(executable)
# Add shortcut
shell = Dispatch('WScript.Shell')
shell = Dispatch("WScript.Shell")
shortcut = shell.CreateShortCut(pathLink)
shortcut.Targetpath = executable
if not sys.executable.endswith("Rare.exe"):
@ -270,12 +308,16 @@ def create_rare_desktop_link(type_of_link):
def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop") -> bool:
igame = core.get_installed_game(app_name)
if os.path.exists(p := os.path.join(image_dir, igame.app_name, 'Thumbnail.png')):
if os.path.exists(p := os.path.join(image_dir, igame.app_name, "Thumbnail.png")):
icon = p
elif os.path.exists(p := os.path.join(image_dir, igame.app_name, "DieselGameBoxLogo.png")):
elif os.path.exists(
p := os.path.join(image_dir, igame.app_name, "DieselGameBoxLogo.png")
):
icon = p
else:
icon = os.path.join(os.path.join(image_dir, igame.app_name, "DieselGameBoxTall.png"))
icon = os.path.join(
os.path.join(image_dir, igame.app_name, "DieselGameBoxTall.png")
)
icon = icon.replace(".png", "")
# Linux
@ -293,14 +335,15 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop") -
else:
executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}"
with open(f"{path}{igame.title}.desktop", "w") as desktop_file:
desktop_file.write("[Desktop Entry]\n"
f"Name={igame.title}\n"
f"Type=Application\n"
f"Icon={icon}.png\n"
f"Exec={executable} launch {app_name}\n"
"Terminal=false\n"
"StartupWMClass=rare-game\n"
)
desktop_file.write(
"[Desktop Entry]\n"
f"Name={igame.title}\n"
f"Type=Application\n"
f"Icon={icon}.png\n"
f"Exec={executable} launch {app_name}\n"
"Terminal=false\n"
"StartupWMClass=rare-game\n"
)
desktop_file.close()
os.chmod(os.path.expanduser(f"{path}{igame.title}.desktop"), 0o755)
@ -308,7 +351,7 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop") -
elif platform.system() == "Windows":
# Target of shortcut
if type_of_link == "desktop":
target_folder = os.path.expanduser('~/Desktop/')
target_folder = os.path.expanduser("~/Desktop/")
elif type_of_link == "start_menu":
target_folder = os.path.expandvars("%appdata%/Microsoft/Windows/Start Menu")
else:
@ -322,20 +365,20 @@ def create_desktop_link(app_name, core: LegendaryCore, type_of_link="desktop") -
for c in r'<>?":|\/*':
linkName.replace(c, "")
linkName = linkName.strip() + '.lnk'
linkName = linkName.strip() + ".lnk"
# Path to location of link file
pathLink = os.path.join(target_folder, linkName)
# Add shortcut
shell = Dispatch('WScript.Shell')
shell = Dispatch("WScript.Shell")
shortcut = shell.CreateShortCut(pathLink)
if sys.executable.endswith("Rare.exe"):
executable = sys.executable
else:
executable = f"{sys.executable} {os.path.abspath(sys.argv[0])}"
shortcut.Targetpath = executable
shortcut.Arguments = f'launch {app_name}'
shortcut.Arguments = f"launch {app_name}"
shortcut.WorkingDirectory = os.getcwd()
# Icon
@ -383,9 +426,7 @@ def optimal_text_background(image: list) -> Tuple[int, int, int]:
return tuple(inverted)
def text_color_for_background(background: Tuple[int, int, int]) -> Tuple[int,
int,
int]:
def text_color_for_background(background: Tuple[int, int, int]) -> Tuple[int, int, int]:
"""
Calculates whether a black or white text color would fit better for the
given background, and returns that color. This is done by calculating the
@ -393,10 +434,7 @@ def text_color_for_background(background: Tuple[int, int, int]) -> Tuple[int,
"""
# see https://alienryderflex.com/hsp.html
(red, green, blue) = background
luminance = math.sqrt(
0.299 * red ** 2 +
0.587 * green ** 2 +
0.114 * blue ** 2)
luminance = math.sqrt(0.299 * red ** 2 + 0.587 * green ** 2 + 0.114 * blue ** 2)
if luminance < 127:
return 255, 255, 255
else:
@ -408,45 +446,62 @@ class WineResolverSignals(QObject):
class WineResolver(QRunnable):
def __init__(self, path: str, app_name: str, core: LegendaryCore):
super(WineResolver, self).__init__()
self.setAutoDelete(True)
self.signals = WineResolverSignals()
self.wine_env = os.environ.copy()
self.wine_env.update(core.get_app_environment(app_name))
self.wine_env['WINEDLLOVERRIDES'] = 'winemenubuilder=d;mscoree=d;mshtml=d;'
self.wine_env['DISPLAY'] = ''
self.wine_env["WINEDLLOVERRIDES"] = "winemenubuilder=d;mscoree=d;mshtml=d;"
self.wine_env["DISPLAY"] = ""
self.wine_binary = core.lgd.config.get(
app_name, 'wine_executable',
fallback=core.lgd.config.get('default', 'wine_executable', fallback='wine'))
self.winepath_binary = os.path.join(os.path.dirname(self.wine_binary), 'winepath')
app_name,
"wine_executable",
fallback=core.lgd.config.get("default", "wine_executable", fallback="wine"),
)
self.winepath_binary = os.path.join(
os.path.dirname(self.wine_binary), "winepath"
)
self.path = PathSpec(core, app_name).cook(path)
@pyqtSlot()
def run(self):
if 'WINEPREFIX' not in self.wine_env or not os.path.exists(self.wine_env['WINEPREFIX']):
if "WINEPREFIX" not in self.wine_env or not os.path.exists(
self.wine_env["WINEPREFIX"]
):
# pylint: disable=E1136
self.signals.result_ready[str].emit(str())
return
if not os.path.exists(self.wine_binary) or not os.path.exists(self.winepath_binary):
if not os.path.exists(self.wine_binary) or not os.path.exists(
self.winepath_binary
):
# pylint: disable=E1136
self.signals.result_ready[str].emit(str())
return
path = self.path.strip().replace('/', '\\')
path = self.path.strip().replace("/", "\\")
# lk: if path does not exist form
cmd = [self.wine_binary, 'cmd', '/c', 'echo', path]
cmd = [self.wine_binary, "cmd", "/c", "echo", path]
# lk: if path exists and needs a case sensitive interpretation form
# cmd = [self.wine_binary, 'cmd', '/c', f'cd {path} & cd']
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=self.wine_env, shell=False, text=True)
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=self.wine_env,
shell=False,
text=True,
)
out, err = proc.communicate()
# Clean wine output
out = out.strip().strip('"')
proc = subprocess.Popen([self.winepath_binary, '-u', out],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=self.wine_env, shell=False, text=True)
proc = subprocess.Popen(
[self.winepath_binary, "-u", out],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=self.wine_env,
shell=False,
text=True,
)
out, err = proc.communicate()
real_path = os.path.realpath(out.strip())
# pylint: disable=E1136
@ -474,7 +529,11 @@ class CloudWorker(QRunnable):
def get_raw_save_path(game: Game):
if game.supports_cloud_saves:
return game.metadata.get("customAttributes", {}).get("CloudSaveFolder", {}).get("value")
return (
game.metadata.get("customAttributes", {})
.get("CloudSaveFolder", {})
.get("value")
)
def get_default_platform(app_name):