2021-10-12 10:19:32 +13:00
|
|
|
import os
|
|
|
|
import platform
|
|
|
|
from logging import getLogger
|
2022-07-02 20:48:33 +12:00
|
|
|
from typing import Tuple, Iterable, List
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2022-07-02 20:48:33 +12:00
|
|
|
from PyQt5.QtCore import Qt, QThreadPool, QRunnable, pyqtSlot, pyqtSignal
|
2021-11-11 10:07:28 +13:00
|
|
|
from PyQt5.QtWidgets import QGroupBox, QListWidgetItem, QFileDialog, QMessageBox
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2022-07-11 04:32:42 +12:00
|
|
|
from rare.lgndr.api_exception import LgndrException
|
2022-02-26 06:43:27 +13:00
|
|
|
from rare.shared import LegendaryCoreSingleton, GlobalSignalsSingleton
|
2021-10-17 12:17:54 +13:00
|
|
|
from rare.ui.components.tabs.games.import_sync.egl_sync_group import Ui_EGLSyncGroup
|
2021-12-24 22:09:50 +13:00
|
|
|
from rare.ui.components.tabs.games.import_sync.egl_sync_list_group import (
|
|
|
|
Ui_EGLSyncListGroup,
|
|
|
|
)
|
2021-10-12 10:19:32 +13:00
|
|
|
from rare.utils.extra_widgets import PathEdit
|
2021-10-15 11:08:18 +13:00
|
|
|
from rare.utils.models import PathSpec
|
2022-07-27 02:58:17 +12:00
|
|
|
from rare.utils.misc import WineResolver
|
2021-10-12 10:19:32 +13:00
|
|
|
|
|
|
|
logger = getLogger("EGLSync")
|
|
|
|
|
|
|
|
|
|
|
|
class EGLSyncGroup(QGroupBox, Ui_EGLSyncGroup):
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
super(EGLSyncGroup, self).__init__(parent=parent)
|
|
|
|
self.setupUi(self)
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core = LegendaryCoreSingleton()
|
2021-12-24 22:09:50 +13:00
|
|
|
self.egl_path_info.setProperty("infoLabel", 1)
|
2021-10-13 08:11:52 +13:00
|
|
|
|
2021-11-22 03:33:16 +13:00
|
|
|
self.thread_pool = QThreadPool.globalInstance()
|
2021-10-18 12:37:11 +13:00
|
|
|
|
2021-12-24 22:09:50 +13:00
|
|
|
if platform.system() == "Windows":
|
2021-11-22 05:57:56 +13:00
|
|
|
self.egl_path_edit_label.setVisible(False)
|
2021-10-17 09:21:36 +13:00
|
|
|
self.egl_path_info_label.setVisible(False)
|
|
|
|
self.egl_path_info.setVisible(False)
|
2021-11-22 05:57:56 +13:00
|
|
|
else:
|
|
|
|
self.egl_path_edit = PathEdit(
|
2022-02-26 06:43:27 +13:00
|
|
|
path=self.core.egl.programdata_path,
|
2022-03-14 05:20:55 +13:00
|
|
|
placeholder=self.tr(
|
2021-12-24 22:09:50 +13:00
|
|
|
"Path to the Wine prefix where EGL is installed, or the Manifests folder"
|
|
|
|
),
|
2021-11-22 05:57:56 +13:00
|
|
|
file_type=QFileDialog.DirectoryOnly,
|
|
|
|
edit_func=self.egl_path_edit_edit_cb,
|
|
|
|
save_func=self.egl_path_edit_save_cb,
|
2021-12-24 22:09:50 +13:00
|
|
|
parent=self,
|
2021-11-22 05:57:56 +13:00
|
|
|
)
|
|
|
|
self.egl_path_edit.textChanged.connect(self.egl_path_changed)
|
|
|
|
self.egl_path_edit_layout.addWidget(self.egl_path_edit)
|
|
|
|
|
2022-02-26 06:43:27 +13:00
|
|
|
if not self.core.egl.programdata_path:
|
2021-12-24 22:09:50 +13:00
|
|
|
self.egl_path_info.setText(self.tr("Updating..."))
|
|
|
|
wine_resolver = WineResolver(
|
2022-03-22 08:56:03 +13:00
|
|
|
PathSpec.egl_programdata, "default"
|
2021-12-24 22:09:50 +13:00
|
|
|
)
|
2021-11-22 05:57:56 +13:00
|
|
|
wine_resolver.signals.result_ready.connect(self.wine_resolver_cb)
|
|
|
|
self.thread_pool.start(wine_resolver)
|
|
|
|
else:
|
|
|
|
self.egl_path_info_label.setVisible(False)
|
|
|
|
self.egl_path_info.setVisible(False)
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2022-02-26 06:43:27 +13:00
|
|
|
self.egl_sync_check.setChecked(self.core.egl_sync_enabled)
|
2021-10-15 11:08:18 +13:00
|
|
|
self.egl_sync_check.stateChanged.connect(self.egl_sync_changed)
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2021-10-17 12:17:54 +13:00
|
|
|
self.import_list = EGLSyncListGroup(export=False, parent=self)
|
|
|
|
self.import_export_layout.addWidget(self.import_list)
|
|
|
|
self.export_list = EGLSyncListGroup(export=True, parent=self)
|
|
|
|
self.import_export_layout.addWidget(self.export_list)
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2021-11-06 11:48:50 +13:00
|
|
|
# self.egl_watcher = QFileSystemWatcher([self.egl_path_edit.text()], self)
|
|
|
|
# self.egl_watcher.directoryChanged.connect(self.update_lists)
|
2021-10-18 10:39:21 +13:00
|
|
|
|
2021-10-15 11:08:18 +13:00
|
|
|
self.update_lists()
|
|
|
|
|
2021-11-06 04:48:56 +13:00
|
|
|
def wine_resolver_cb(self, path):
|
|
|
|
self.egl_path_info.setText(path)
|
2021-11-22 03:59:11 +13:00
|
|
|
if not path:
|
2021-11-06 04:48:56 +13:00
|
|
|
self.egl_path_info.setText(
|
2021-12-24 22:09:50 +13:00
|
|
|
self.tr(
|
|
|
|
"Default Wine prefix is unset, or path does not exist. "
|
|
|
|
"Create it or configure it in Settings -> Linux."
|
|
|
|
)
|
|
|
|
)
|
2021-11-22 03:59:11 +13:00
|
|
|
elif not os.path.exists(path):
|
|
|
|
self.egl_path_info.setText(
|
2021-12-24 22:09:50 +13:00
|
|
|
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."
|
|
|
|
)
|
|
|
|
)
|
2021-11-22 03:59:11 +13:00
|
|
|
else:
|
|
|
|
self.egl_path_edit.setText(path)
|
2021-11-06 04:48:56 +13:00
|
|
|
|
2021-10-15 11:08:18 +13:00
|
|
|
@staticmethod
|
2022-01-14 07:49:39 +13:00
|
|
|
def egl_path_edit_edit_cb(path) -> Tuple[bool, str, str]:
|
2021-10-15 11:08:18 +13:00
|
|
|
if not path:
|
2022-01-14 07:49:39 +13:00
|
|
|
return True, path, ""
|
2021-12-24 22:09:50 +13:00
|
|
|
if os.path.exists(os.path.join(path, "system.reg")) and os.path.exists(
|
2022-01-14 07:49:39 +13:00
|
|
|
os.path.join(path, "dosdevices/c:")
|
2021-12-24 22:09:50 +13:00
|
|
|
):
|
2021-11-22 05:59:37 +13:00
|
|
|
# path is a wine prefix
|
2021-12-24 22:09:50 +13:00
|
|
|
path = os.path.join(
|
|
|
|
path,
|
|
|
|
"dosdevices/c:",
|
|
|
|
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests",
|
|
|
|
)
|
|
|
|
elif not path.rstrip("/").endswith(
|
2022-01-14 07:49:39 +13:00
|
|
|
"ProgramData/Epic/EpicGamesLauncher/Data/Manifests"
|
2021-12-24 22:09:50 +13:00
|
|
|
):
|
2021-11-22 05:59:37 +13:00
|
|
|
# lower() might or might not be needed in the check
|
2022-01-14 07:49:39 +13:00
|
|
|
return False, path, PathEdit.reasons.wrong_path
|
2021-10-15 11:08:18 +13:00
|
|
|
if os.path.exists(path):
|
2022-01-14 07:49:39 +13:00
|
|
|
return True, path, ""
|
|
|
|
return False, path, PathEdit.reasons.dir_not_exist
|
2021-10-13 08:11:52 +13:00
|
|
|
|
2022-02-26 06:43:27 +13:00
|
|
|
def egl_path_edit_save_cb(self, path):
|
2021-11-11 10:07:28 +13:00
|
|
|
if not path or not os.path.exists(path):
|
2021-10-15 11:08:18 +13:00
|
|
|
# This is the same as "--unlink"
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.egl.programdata_path = None
|
|
|
|
self.core.lgd.config.remove_option("Legendary", "egl_programdata")
|
|
|
|
self.core.lgd.config.remove_option("Legendary", "egl_sync")
|
2021-10-15 11:08:18 +13:00
|
|
|
# remove EGL GUIDs from all games, DO NOT remove .egstore folders because that would fuck things up.
|
2022-02-26 06:43:27 +13:00
|
|
|
for igame in self.core.get_installed_list():
|
2021-12-24 22:09:50 +13:00
|
|
|
igame.egl_guid = ""
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.install_game(igame)
|
2021-10-15 11:08:18 +13:00
|
|
|
else:
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.egl.programdata_path = path
|
|
|
|
self.core.lgd.config.set("Legendary", "egl_programdata", path)
|
2021-11-11 10:07:28 +13:00
|
|
|
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.lgd.save_config()
|
2021-10-15 11:08:18 +13:00
|
|
|
|
|
|
|
def egl_path_changed(self, path):
|
|
|
|
if self.egl_path_edit.is_valid:
|
|
|
|
self.egl_sync_check.setEnabled(bool(path))
|
2021-10-17 09:21:36 +13:00
|
|
|
self.egl_sync_check.setCheckState(Qt.Unchecked)
|
2021-11-06 11:48:50 +13:00
|
|
|
# self.egl_watcher.removePaths([p for p in self.egl_watcher.directories()])
|
|
|
|
# self.egl_watcher.addPaths([path])
|
2021-10-17 09:21:36 +13:00
|
|
|
self.update_lists()
|
2021-10-15 11:08:18 +13:00
|
|
|
|
|
|
|
def egl_sync_changed(self, state):
|
|
|
|
if state == Qt.Unchecked:
|
2021-11-07 13:11:38 +13:00
|
|
|
self.import_list.setEnabled(bool(self.import_list.items))
|
|
|
|
self.export_list.setEnabled(bool(self.export_list.items))
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.lgd.config.remove_option("Legendary", "egl_sync")
|
2021-10-15 11:08:18 +13:00
|
|
|
else:
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.lgd.config.set("Legendary", "egl_sync", str(True))
|
2021-10-18 12:37:11 +13:00
|
|
|
# lk: do import/export here since automatic sync was selected
|
|
|
|
self.import_list.mark(Qt.Checked)
|
|
|
|
self.export_list.mark(Qt.Checked)
|
|
|
|
sync_worker = EGLSyncWorker(self.import_list, self.export_list)
|
2021-11-22 03:33:16 +13:00
|
|
|
self.thread_pool.start(sync_worker)
|
2021-10-18 12:37:11 +13:00
|
|
|
self.import_list.setEnabled(False)
|
|
|
|
self.export_list.setEnabled(False)
|
|
|
|
# self.update_lists()
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.lgd.save_config()
|
2021-10-15 11:08:18 +13:00
|
|
|
|
|
|
|
def update_lists(self):
|
2021-11-06 11:48:50 +13:00
|
|
|
# self.egl_watcher.blockSignals(True)
|
2022-02-26 06:43:27 +13:00
|
|
|
if have_path := bool(self.core.egl.programdata_path) and os.path.exists(
|
|
|
|
self.core.egl.programdata_path
|
2021-12-24 22:09:50 +13:00
|
|
|
):
|
2021-10-18 10:39:21 +13:00
|
|
|
# NOTE: need to clear known manifests to force refresh
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core.egl.manifests.clear()
|
2021-11-22 02:57:43 +13:00
|
|
|
self.egl_sync_check_label.setEnabled(have_path)
|
2021-10-16 07:50:05 +13:00
|
|
|
self.egl_sync_check.setEnabled(have_path)
|
2021-10-17 12:17:54 +13:00
|
|
|
self.import_list.populate(have_path)
|
|
|
|
self.import_list.setEnabled(have_path)
|
|
|
|
self.export_list.populate(have_path)
|
|
|
|
self.export_list.setEnabled(have_path)
|
2021-11-06 11:48:50 +13:00
|
|
|
# self.egl_watcher.blockSignals(False)
|
2021-10-17 09:21:36 +13:00
|
|
|
|
|
|
|
|
2021-10-17 12:17:54 +13:00
|
|
|
class EGLSyncListItem(QListWidgetItem):
|
2021-10-12 10:19:32 +13:00
|
|
|
def __init__(self, game, export: bool, parent=None):
|
2021-10-17 12:17:54 +13:00
|
|
|
super(EGLSyncListItem, self).__init__(parent=parent)
|
2021-10-12 10:19:32 +13:00
|
|
|
self.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
|
|
|
|
self.setCheckState(Qt.Unchecked)
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core = LegendaryCoreSingleton()
|
2021-10-12 10:19:32 +13:00
|
|
|
self.game = game
|
|
|
|
self.export = export
|
|
|
|
if export:
|
|
|
|
self.setText(game.title)
|
2021-12-27 11:03:50 +13:00
|
|
|
else: # import
|
2022-02-26 06:43:27 +13:00
|
|
|
self.setText(self.core.get_game(game.app_name).app_title)
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2021-10-18 05:12:02 +13:00
|
|
|
def is_checked(self) -> bool:
|
2021-12-27 11:03:50 +13:00
|
|
|
return self.checkState() == Qt.Checked
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2022-06-26 10:15:30 +12:00
|
|
|
def action(self) -> str:
|
|
|
|
error = ""
|
2021-10-17 12:17:54 +13:00
|
|
|
if self.export:
|
2022-06-26 10:15:30 +12:00
|
|
|
try:
|
|
|
|
self.core.egl_export(self.game.app_name)
|
|
|
|
except LgndrException as ret:
|
|
|
|
error = ret.message
|
2021-10-17 12:17:54 +13:00
|
|
|
else:
|
2022-06-26 10:15:30 +12:00
|
|
|
try:
|
|
|
|
self.core.egl_import(self.game.app_name)
|
|
|
|
except LgndrException as ret:
|
|
|
|
error = ret.message
|
2021-11-07 13:11:38 +13:00
|
|
|
return error
|
2021-10-17 12:17:54 +13:00
|
|
|
|
2021-11-06 04:48:56 +13:00
|
|
|
@property
|
|
|
|
def app_name(self):
|
|
|
|
return self.game.app_name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def app_title(self):
|
|
|
|
return self.game.app_title
|
|
|
|
|
2021-10-17 12:17:54 +13:00
|
|
|
|
|
|
|
class EGLSyncListGroup(QGroupBox, Ui_EGLSyncListGroup):
|
2022-07-02 20:48:33 +12:00
|
|
|
action_errors = pyqtSignal(list)
|
|
|
|
|
2021-10-17 12:17:54 +13:00
|
|
|
def __init__(self, export: bool, parent=None):
|
|
|
|
super(EGLSyncListGroup, self).__init__(parent=parent)
|
|
|
|
self.setupUi(self)
|
2022-02-26 06:43:27 +13:00
|
|
|
self.core = LegendaryCoreSingleton()
|
|
|
|
self.signals = GlobalSignalsSingleton()
|
2021-10-17 12:17:54 +13:00
|
|
|
self.list.setProperty("noBorder", 1)
|
2022-07-02 20:48:33 +12:00
|
|
|
# TODO: Convert the CSS and the code to adhere to NoFrame
|
|
|
|
# self.list.setFrameShape(self.list.NoFrame)
|
2021-10-17 12:17:54 +13:00
|
|
|
|
|
|
|
self.export = export
|
|
|
|
|
|
|
|
if export:
|
2021-12-24 22:09:50 +13:00
|
|
|
self.setTitle(self.tr("Exportable games"))
|
|
|
|
self.label.setText(self.tr("No games to export to EGL"))
|
|
|
|
self.action_button.setText(self.tr("Export"))
|
2022-02-26 06:43:27 +13:00
|
|
|
self.list_func = self.core.egl_get_exportable
|
2021-10-17 12:17:54 +13:00
|
|
|
else:
|
2021-12-24 22:09:50 +13:00
|
|
|
self.setTitle(self.tr("Importable games"))
|
|
|
|
self.label.setText(self.tr("No games to import from EGL"))
|
|
|
|
self.action_button.setText(self.tr("Import"))
|
2022-02-26 06:43:27 +13:00
|
|
|
self.list_func = self.core.egl_get_importable
|
2021-10-17 12:17:54 +13:00
|
|
|
|
|
|
|
self.list.itemDoubleClicked.connect(
|
2021-12-24 22:09:50 +13:00
|
|
|
lambda item: item.setCheckState(Qt.Unchecked)
|
|
|
|
if item.checkState() != Qt.Unchecked
|
|
|
|
else item.setCheckState(Qt.Checked)
|
2021-10-17 12:17:54 +13:00
|
|
|
)
|
2021-10-18 10:39:21 +13:00
|
|
|
self.list.itemChanged.connect(self.has_selected)
|
2021-10-17 12:17:54 +13:00
|
|
|
|
|
|
|
self.select_all_button.clicked.connect(lambda: self.mark(Qt.Checked))
|
|
|
|
self.select_none_button.clicked.connect(lambda: self.mark(Qt.Unchecked))
|
|
|
|
|
|
|
|
self.action_button.clicked.connect(self.action)
|
|
|
|
|
2022-07-02 20:48:33 +12:00
|
|
|
self.action_errors.connect(self.show_errors)
|
|
|
|
|
2021-10-18 10:39:21 +13:00
|
|
|
def has_selected(self):
|
|
|
|
for item in self.items:
|
|
|
|
if item.is_checked():
|
|
|
|
self.action_button.setEnabled(True)
|
|
|
|
return
|
|
|
|
self.action_button.setEnabled(False)
|
|
|
|
|
2021-10-17 12:17:54 +13:00
|
|
|
def mark(self, state):
|
|
|
|
for item in self.items:
|
|
|
|
item.setCheckState(state)
|
|
|
|
|
|
|
|
def populate(self, enabled: bool):
|
|
|
|
if enabled:
|
|
|
|
self.list.clear()
|
|
|
|
for item in self.list_func():
|
2021-12-27 11:03:50 +13:00
|
|
|
try:
|
|
|
|
i = EGLSyncListItem(item, self.export, self.list)
|
|
|
|
except AttributeError:
|
2022-02-02 10:29:34 +13:00
|
|
|
logger.error(f"{item.app_name} does not work. Ignoring")
|
2021-12-27 11:03:50 +13:00
|
|
|
else:
|
|
|
|
self.list.addItem(i)
|
2021-10-17 12:17:54 +13:00
|
|
|
self.label.setVisible(not enabled or not bool(self.list.count()))
|
|
|
|
self.list.setVisible(enabled and bool(self.list.count()))
|
|
|
|
self.buttons_widget.setVisible(enabled and bool(self.list.count()))
|
|
|
|
|
|
|
|
def action(self):
|
2022-07-02 20:48:33 +12:00
|
|
|
imported: List = []
|
|
|
|
errors: List = []
|
2021-10-17 12:17:54 +13:00
|
|
|
for item in self.items:
|
2021-10-18 05:12:02 +13:00
|
|
|
if item.is_checked():
|
2021-11-07 13:11:38 +13:00
|
|
|
if e := item.action():
|
|
|
|
errors.append(e)
|
|
|
|
else:
|
|
|
|
imported.append(item.app_name)
|
|
|
|
self.list.takeItem(self.list.row(item))
|
2021-11-06 04:48:56 +13:00
|
|
|
if not self.export and imported:
|
2022-02-26 06:43:27 +13:00
|
|
|
self.signals.update_gamelist.emit(imported)
|
2021-10-17 12:17:54 +13:00
|
|
|
self.populate(True)
|
2021-11-07 13:11:38 +13:00
|
|
|
if errors:
|
2022-07-02 20:48:33 +12:00
|
|
|
self.action_errors.emit(errors)
|
|
|
|
|
|
|
|
@pyqtSlot(list)
|
|
|
|
def show_errors(self, errors: List):
|
|
|
|
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),
|
|
|
|
)
|
2021-10-12 10:19:32 +13:00
|
|
|
|
2021-10-17 12:17:54 +13:00
|
|
|
@property
|
2021-10-18 10:39:21 +13:00
|
|
|
def items(self) -> Iterable[EGLSyncListItem]:
|
|
|
|
# for i in range(self.list.count()):
|
|
|
|
# yield self.list.item(i)
|
2021-10-17 12:17:54 +13:00
|
|
|
return [self.list.item(i) for i in range(self.list.count())]
|
2021-10-12 10:19:32 +13:00
|
|
|
|
|
|
|
|
2021-10-18 12:37:11 +13:00
|
|
|
class EGLSyncWorker(QRunnable):
|
|
|
|
def __init__(self, import_list: EGLSyncListGroup, export_list: EGLSyncListGroup):
|
|
|
|
super(EGLSyncWorker, self).__init__()
|
|
|
|
self.setAutoDelete(True)
|
|
|
|
self.import_list = import_list
|
|
|
|
self.export_list = export_list
|
|
|
|
|
|
|
|
@pyqtSlot()
|
|
|
|
def run(self):
|
|
|
|
self.import_list.action()
|
|
|
|
self.export_list.action()
|